关于前端:Vue2x-的双向绑定原理及实现

4次阅读

共计 6163 个字符,预计需要花费 16 分钟才能阅读完成。

Vue 数据双向绑定原理

Vue 是利用的 Object.defineProperty()办法进行的数据劫持,利用 set、get 来检测数据的读写。

<iframe width=”100%” height=”300″ src=”//jsrun.net/RMIKp/embedded/all/light” allowfullscreen=”allowfullscreen” frameborder=”0″></iframe>

MVVM 框架次要蕴含两个方面,数据变动更新视图,视图变动更新数据。

视图变动更新数据,如果是像 input 这种标签,能够应用 oninput 事件..

数据变动更新视图能够应用 Object.definProperty()的 set 办法能够检测数据变动,当数据扭转就会触发这个函数,而后更新视图。

实现过程

咱们晓得了如何实现双向绑定了,首先要对数据进行劫持监听,所以咱们须要设置一个 Observer 函数,用来监听所有属性的变动。

如果属性产生了变动,那就要通知订阅者 watcher 看是否须要更新数据,如果订阅者有多个,则须要一个 Dep 来收集这些订阅者,而后在监听器 observer 和 watcher 之间进行对立治理。

还须要一个指令解析器 compile,对须要监听的节点和属性进行扫描和解析。

因而,流程大略是这样的:

  1. 实现一个监听器 Observer,用来劫持并监听所有属性,如果产生变动,则告诉订阅者。
  2. 实现一个订阅者 Watcher,当接到属性变动的告诉时,执行对应的函数,而后更新视图,应用 Dep 来收集这些 Watcher。
  3. 实现一个解析器 Compile,用于扫描和解析的节点的相干指令,并依据初始化模板以及初始化相应的订阅器。

<!– –>

显示一个 Observer

Observer 是一个数据监听器,外围办法是利用 Object.defineProperty()通过递归的形式对所有属性都增加 setter、getter 办法进行监听。

var library = {
  book1: {name: "",},
  book2: "",
};
observe(library);
library.book1.name = "vue 权威指南"; // 属性 name 曾经被监听了,当初值为:“vue 权威指南”library.book2 = "没有此书籍"; // 属性 book2 曾经被监听了,当初值为:“没有此书籍”// 为数据增加检测
function defineReactive(data, key, val) {observe(val); // 递归遍历所有子属性
  let dep = new Dep(); // 新建一个 dep
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {if (Dep.target) {
        // 判断是否须要增加订阅者,仅第一次须要增加,之后就不必了,具体看 Watcher 函数
        dep.addSub(Dep.target); // 增加一个订阅者
      }
      return val;
    },
    set: function(newVal) {if (val == newVal) return; // 如果值未产生扭转就 return
      val = newVal;
      console.log("属性" + key + "曾经被监听了,当初值为:“" + newVal.toString() + "”"
      );
      dep.notify(); // 如果数据发生变化,就告诉所有的订阅者。},
  });
}

// 监听对象的所有属性
function observe(data) {if (!data || typeof data !== "object") {return; // 如果不是对象就 return}
  Object.keys(data).forEach(function(key) {defineReactive(data, key, data[key]);
  });
}
// Dep 负责收集订阅者,当属性发生变化时,触发更新函数。function Dep() {this.subs = {};
}
Dep.prototype = {addSub: function(sub) {this.subs.push(sub);
  },
  notify: function() {this.subs.forEach((sub) => sub.update());
  },
};

思路剖析中,须要有一个能够包容订阅者音讯订阅器 Dep,用于收集订阅者,在属性发生变化时执行对应的更新函数。

从代码上看,将订阅器 Dep 增加在 getter 里,是为了让 Watcher 初始化时触发,,因而,须要判断是否须要订阅者。

在 setter 中,如果有数据发生变化,则告诉所有的订阅者,而后订阅者就会更新对应的函数。

到此为止,一个比拟残缺的 Observer 就实现了,接下来开始设计 Watcher.

实现 Watcher

订阅者 Watcher 须要在初始化的时候将本人增加到订阅器 Dep 中,咱们曾经晓得监听器 Observer 是在 get 时执行的 Watcher 操作,所以只须要在 Watcher 初始化的时候触发对应的 get 函数去增加对应的订阅者操作即可。

那给如何触发 get 呢?因为咱们曾经设置了 Object.defineProperty(),所以只须要获取对应的属性值就能够触发了。

咱们只须要在订阅者 Watcher 初始化的时候,在 Dep.target 上缓存下订阅者,增加胜利之后在将其去掉就能够了。

function Watcher(vm, exp, cb) {
  this.cb = cb;
  this.vm = vm;
  this.exp = exp;
  this.value = this.get(); // 将本人增加到订阅器的操作}

Watcher.prototype = {update: function() {this.run();
  },
  run: function() {var value = this.vm.data[this.exp];
    var oldVal = this.value;
    if (value !== oldVal) {
      this.value = value;
      this.cb.call(this.vm, value, oldVal);
    }
  },
  get: function() {
    Dep.target = this; // 缓存本人,用于判断是否增加 watcher。var value = this.vm.data[this.exp]; // 强制执行监听器里的 get 函数
    Dep.target = null; // 开释本人
    return value;
  },
};

到此为止,简略的额 Watcher 设计结束,而后将 Observer 和 Watcher 关联起来,就能够实现一个简略的的双向绑定了。

因为还没有设计解析器 Compile,所以能够先将模板数据写死。

将代码转化为 ES6 构造函数的写法,预览试试。

<iframe width=”100%” height=”400″ src=”//jsrun.net/8SIKp/embedded/all/light” allowfullscreen=”allowfullscreen” frameborder=”0″></iframe>

这段代码因为没有实现编译器而是间接传入了所绑定的变量,咱们只在一个节点上设置一个数据(name)进行绑定,而后在页面上进行 new MyVue,就能够实现双向绑定了。

并两秒后进行值得扭转,能够看到,页面也产生了变动。

// MyVue
proxyKeys(key) {
    var self = this;
    Object.defineProperty(this, key, {
        enumerable: false,
        configurable: true,
        get: function proxyGetter() {return self.data[key];
        },
        set: function proxySetter(newVal) {self.data[key] = newVal;
        }
    });
}

下面这段代码的作用是将 this.data 的 key 代理到 this 上,使得我能够不便的应用 this.xx 就能够取到 this.data.xx。

实现 Compile

尽管下面实现了双向数据绑定,然而整个过程都没有解析 DOM 节店,而是固定替换的,所以接下来要实现一个解析器来做数据的解析和绑定工作。

解析器 compile 的实现步骤:

  1. 解析模板指令,并替换模板数据,初始化视图。
  2. 将模板指定对应的节点绑定对应的更新函数,初始化相应的订阅器。

为了解析模板,首先须要解析 DOM 数据,而后对含有 DOM 元素上的对应指令进行解决,因而整个 DOM 操作较为频繁,能够新建一个 fragment 片段,将须要的解析的 DOM 存入 fragment 片段中在进行解决。

function nodeToFragment(el) {var fragment = document.createDocumentFragment();
  var child = el.firstChild;
  while (child) {
    // 将 Dom 元素移入 fragment 中
    fragment.appendChild(child);
    child = el.firstChild;
  }
  return fragment;
}

接下来须要遍历各个节点,对含有相干指令和模板语法的节点进行非凡解决,先进行最简略模板语法解决,应用正则解析“{{变量}}”这种模式的语法。

function compileElement (el) {
    var childNodes = el.childNodes;
    var self = this;
    [].slice.call(childNodes).forEach(function(node) {var reg = /\{\{(.*)\}\}/; // 匹配{{xx}}
        var text = node.textContent;
        if (self.isTextNode(node) && reg.test(text)) {// 判断是否是合乎这种模式 {{}} 的指令
            self.compileText(node, reg.exec(text)[1]);
        }
        if (node.childNodes && node.childNodes.length) {self.compileElement(node);  // 持续递归遍历子节点
        }
    });
},
function compileText (node, exp) {
    var self = this;
    var initText = this.vm[exp];
    updateText(node, initText);  // 将初始化的数据初始化到视图中
    new Watcher(this.vm, exp, function (value) {  // 生成订阅器并绑定更新函数
        self.updateText(node, value);
    });
},
function updateText (node, value) {node.textContent = typeof value == 'undefined' ? '' : value;}

获取到最外层的节点后,调用 compileElement 函数,对所有的子节点进行判断,如果节点是文本节点切匹配 {{}} 这种模式的指令,则进行编译解决,初始化对应的参数。

而后须要对以后参数生成一个对应的更新函数订阅器,在数据发生变化时更新对应的 DOM。

这样就实现了解析、初始化、编译三个过程了。

接下来革新一个 myVue 就能够应用模板变量进行双向数据绑定了。

<iframe width=”100%” height=”400″ src=”//jsrun.net/K4IKp/embedded/all/light” allowfullscreen=”allowfullscreen” frameborder=”0″></iframe>

增加解析事件

增加完 compile 之后,一个数据双向绑定就根本实现了,接下来就是在 Compile 中增加更多指令的解析编译,比方 v-model、v-on、v-bind 等。

增加一个 v-model 和 v-on 解析:

function compile(node) {
  var nodeAttrs = node.attributes;
  var self = this;
  Array.prototype.forEach.call(nodeAttrs, function(attr) {
    var attrName = attr.name;
    if (isDirective(attrName)) {
      var exp = attr.value;
      var dir = attrName.substring(2);
      if (isEventDirective(dir)) {
        // 事件指令
        self.compileEvent(node, self.vm, exp, dir);
      } else {
        // v-model 指令
        self.compileModel(node, self.vm, exp, dir);
      }
      node.removeAttribute(attrName); // 解析结束,移除属性
    }
  });
}
// v- 指令解析
function isDirective(attr) {return attr.indexOf("v-") == 0;
}
// on: 指令解析
function isEventDirective(dir) {return dir.indexOf("on:") === 0;
}

下面的 compile 函数是用于遍历以后 dom 的所有节点属性,而后判断属性是否是指令属性,如果是在做对应的解决(事件就去监听事件、数据就去监听数据..)

完整版 myVue

在 MyVue 中增加 mounted 办法,在所有操作都做完时执行。

class MyVue {constructor(options) {
    var self = this;
    this.data = options.data;
    this.methods = options.methods;
    Object.keys(this.data).forEach(function(key) {self.proxyKeys(key);
    });
    observe(this.data);
    new Compile(options.el, this);
    options.mounted.call(this); // 所有事件解决好后执行 mounted 函数
  }
  proxyKeys(key) {
    // 将 this.data 属性代理到 this 上
    var self = this;
    Object.defineProperty(this, key, {
      enumerable: false,
      configurable: true,
      get: function getter() {return self.data[key];
      },
      set: function setter(newVal) {self.data[key] = newVal;
      },
    });
  }
}

而后就能够测试应用了。

<iframe width=”100%” height=”400″ src=”//jsrun.net/Y4IKp/embedded/all/light” allowfullscreen=”allowfullscreen” frameborder=”0″></iframe>

总结一下流程,回头在哪看一遍这个图,是不是分明很多了。

参考

  • Vue.js 技术揭秘
  • 掘金 - 分析 Vue.js 外部运行机制
  • 博客园 -vue 的双向绑定原理及实现
  • KKB-vue 源码解析
  • vue-study

正文完
 0