Compile.prototype.compileElement = function(el) { let childNodes = Array.from(el.childNodes), self = this; childNodes.forEach(function(node) { let text = node.textContent, reg = /\{\{(.*)\}\}/; // 若为 textNode 元素,且匹配 reg 正则 // 在上例中会匹配 '{{user.name}}' 及 '{{user.age}}' if (self.isTextNode(node) && reg.test(text)) { // 解析 textContent,RegExp.$1 为匹配到的内容,在上例中为 'user.name' 及 'user.age' self.compileText(node, RegExp.$1); } // 递归 if (node.childNodes && node.childNodes.length) { self.compileElement(node); } }); }; Compile.prototype.compileText = function(node, exp) { // this.$vm 即为 Hue 实例,exp 为正则匹配到的内容,即 'user.name' 或 'user.age' compileUtil.text(node, this.$vm, exp); }; let compileUtil = { text: function(node, vm, exp) { this.bind(node, vm, exp, 'text'); }, bind: function(node, vm, exp, dir) { // 获取更新视图的回调函数 let updaterFn = updater[dir + 'Updater']; // 先调用一次 updaterFn,更新视图 updaterFn && updaterFn(node, this._getVMVal(vm, exp)); // 添加 Watcher 订阅 new Watcher(vm, exp, function(value, oldValue) { updaterFn && updaterFn(node, value, oldValue); }); }, // 根据 exp,获得其值,在上例中即 'vm.user.name' 或 'vm.user.age' _getVMVal: function(vm, exp) { let val = vm; exp = exp.trim().split('.'); exp.forEach(function(k) { val = val[k]; }); return val; } }; let updater = { // Watcher 订阅的回调函数 // 在此即更新 node.textContent,即 update view textUpdater: function(node, value) { node.textContent = typeof value === 'undefined' ? '' : value; } };
正如代码中所看到的,Compile 在解析到 {{xxx}} 后便添加了 xxx 属性的订阅,即 new Watcher(vm, exp, callback)。理解了这一步后,接下来就需要了解怎么实现相关属性的订阅了。先从 Observer 开始谈起。
Observer
从最简单的情况来考虑,即不考虑数组元素的变化。暂时先不考虑 Dep 与 Observer 的联系。先看看 Observer 构造函数:
function Observer(data) { this.data = data; this.walk(data); } Observer.prototype.walk = function(data) { const keys = Object.keys(data); // 遍历 data 的所有属性 for (let i = 0; i < keys.length; i++) { // 调用 defineReactive 添加 getter 和 setter defineReactive(data, keys[i], data[keys[i]]); } };
接下来通过 Object.defineProperty 方法给所有属性添加 getter 和 setter,就达到了我们的目的。属性有可能也是对象,因此需要对属性值进行递归调用。
function defineReactive(obj, key, val) { // 对属性值递归,对应属性值为对象的情况 let childObj = observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function() { // 直接返回属性值 return val; }, set: function(newVal) { if (newVal === val) { return; } // 值发生变化时修改闭包中的 val, // 保证在触发 getter 时返回正确的值 val = newVal; // 对新赋的值进行递归,防止赋的值为对象的情况 childObj = observe(newVal); } }); }
最后补充上 observe 函数,也即 Hue 构造函数中调用的 observe 函数:
function observe(val) { // 若 val 是对象且非数组,则 new 一个 Observer 实例,val 作为参数 // 简单点说:是对象就继续。 if (!Array.isArray(val) && typeof val === "object") { return new Observer(val); } }
这样一来就对 data 的所有子孙属性(不知有没有这种说法。。)都进行了“劫持”。显然到目前为止,这并没什么用,或者说如果只做到这里,那么和什么都不做没差别。于是 Dep 上场了。我认为理解 Dep 与 Observer 和 Watcher 之间的联系是最重要的,先来谈谈 Dep 在 Observer 里做了什么。
Observer & Dep
在每一次 defineReactive 函数被调用之后,都会在闭包中新建一个 Dep 实例,即 let dep = new Dep()。Dep 提供了一些方法,先来说说 notify 这个方法,它做了什么事?就是在属性值发生变化的时候通知 Dep,那么我们的代码可以增加如下:
function defineReactive(obj, key, val) { let childObj = observe(val); const dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function() { return val; }, set: function(newVal) { if (newVal === val) { return; } val = newVal; childObj = observe(newVal); // 发生变动 dep.notify(); } }); }
如果仅考虑 Observer 与 Dep 的联系,即有变动时通知 Dep,那么这里就算完了,然而在 vue.js 的源码中,我们还可以看到一段增加在 getter 中的代码:
// ... get: function() { if (Dep.target) { dep.depend(); } return val; } // ...