系列文章
分析
前面我们已经实现了对于 html 的解析和渲染,能够从数据到视图了(这里有一个缺陷:如果文本节点是诸如:{{ var1 }} - {{ var2 }} 之类的格式,测无法成功解析和渲染,这个问题暂且略过,等到整个系统实现之后再来解决。)
现在摆在我们眼前的问题是,实现更改数据,能够更新视图的效果。想要实现这样的效果,我们可以利用发布订阅模式:
- 实现一个 Observer 模块:用于监视所有的数据,当数据发生改变的时候发出通知
- 实现一个 Watcher 模块:接收所有通知,当接到通知的时候触发对应的 Updater 模块去更新视图
实现
Observer
我们先来看看 Observer 模块的实现。
既然我们要对所有的数据进行劫持监听,那自然在 wm 实例创建的时候,就要对其进行初始化:
import Compile from "./compile.js";
import Observer from "./observer.js";
class Wvue {
constructor(options = {}) {
this.$options = options;
this.$el = options.el;
this.$data = options.data;
if (!this.$el) throw new Error("请指定挂载点");
+ this._initObserve();
this._initCompile();
}
_initCompile() {
new Compile(this.$el, this);
}
+ _initObserve() {
+ new Observer(this.$data);
+ }
}
export default Wvue;
+class Observer {
+ constructor(data) {
+ this.data = data;
+ }
+}
+export default Observer;
接下来我们遍历 data 对象,对其中的每一个属性进行监听:
class Observer {
constructor(data) {
this.data = data;
+ this.observe(this.data);
}
+ observe(data) {
+ if (data && typeof (data === "object")) {
+ Object.keys(data).forEach(key => {
+ this.defineReactive();
+ });
+ }
+ }
+ defineReactive() {}
}
export default Observer;
这里有一个小问题,那就是 data 中的属性,有可能自身也是一个 object,不过有了前面的经验,很容易想到,这里使用递归即可:
class Observer {
constructor(data) {
this.data = data;
this.observe(this.data);
}
observe(data) {
if (data && typeof data === "object") {
Object.keys(data).forEach(key => {
this.defineReactive(data[key]);
});
}
}
defineReactive(value) {
+ this.observe(value);
}
}
export default Observer;
值得一提的是,这里的结束条件在 observe 中,而不是在 defineReactive 中。
接下来我们对数据进行劫持,vue 2.x 中的方法是 Object.defineproperty,3.0 中的方法是 Proxy。(至于为什么要从 defineproperty 换成 Proxy,我们可以放在后面聊聊),这里我们就先通过最早的 defineproperty 来实现数据劫持,关于这个方法,还不太熟悉的同学可以看看官方文档:Object.defineproperty
class Observer {
constructor(data) {
this.data = data;
this.observe(this.data);
}
observe(data) {
if (data && typeof data === "object") {
Object.keys(data).forEach(key => {
M this.defineReactive(data, key, data[key]);
});
}
}
M defineReactive(obj, key, value) {
this.observe(value);
+ Object.defineProperty(obj, key, {
+ enumerable: true,
+ configurable: false,
+ // 利用访问器完成数据劫持
+ get() {
+ console.log("get Value");
+ return value;
+ },
+ set(newVal) {
+ console.log(`set value, new value = ${newVal}`);
+ if (newVal !== value) {
+ value = newVal;
+ }
+ }
+ });
}
}
export default Observer;
效果如下:

可以看到,这里我们已经完成了数据的劫持,每当数据发生变化,或者被访问的的时候我们都能做出相应的反应。不过当前我们的劫持还有一个问题,比如用户直接将 user 这个结构体进行重新赋值:user = { name : "Alice" },那么这个 user 将不再被劫持:效果如下:

并且从结构也可以看出来,新的 user 已经没有访问器了:

所以我们需要在新的值被设定之前,对其进行劫持监听:
class Observer {
constructor(data) {
this.data = data;
this.observe(this.data);
}
observe(data) {
if (data && typeof data === "object") {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
});
}
}
defineReactive(obj, key, value) {
+ const _this = this;
this.observe(value);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
// 利用访问器完成数据劫持
get() {
return value;
},
set(newVal) {
+ _this.observe(newVal);
if (newVal !== value) {
value = newVal;
}
+ return true;
}
});
}
}
export default Observer;
当然,这里也可以使用箭头函数来修复 this 指向:
set: newVal => {
_this.observe(newVal);
if (newVal !== value) {
value = newVal;
}
return true;
}
效果如下:

依赖收集
完成了数据劫持,当下摆在眼前的问题变成了视图更新。那么这里我们要考虑一个问题:我们对所有的数据都进行了劫持,那么每一个数据发生改变的时候,是不是都有必要去通知监视器触发更新呢?
显然,问题的答案是否定的。如果我们修改的数据,在页面中任何地方都没有被用到,当它发生修改的时候,自然没有必要去触发视图的更新。所以这里我们需要一个模块,实现依赖收集,只有视图中使用到的数据,才添加依赖,当数据发生变化的时候去触发更新。那么这个模块核心功能有两个:
- 依赖收集
- 通知更新
代码如下:
+class Dep {
+ constructor() {
+ this.watchers = [];
+ }
+ addWater(w) {
+ this.watchers.push(w);
+ }
+ notify() {
+ this.watchers.forEach(w => {
+ w.updater();
+ });
+ }
+}
+export default Dep;
那么接下来就是上面提到的灵魂拷问,那些数据需要添加依赖呢?其实换个角度,这个问题很简单,页面上需要用到的数据需要添加依赖,而页面上需要用到的数据一定是在 compile 阶段去 data 中获取值的数据。那么就很明显了,我们只要在 get 访问器中添加依赖即可:
import Dep from "./dep.js";
class Observer {
constructor(data) {
this.data = data;
this.observe(this.data);
}
observe(data) {
if (data && typeof data === "object") {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
});
}
}
defineReactive(obj, key, value) {
this.observe(value);
+ const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
// 利用访问器完成数据劫持
get: _ => {
+ dep.addWater("watcher");
return value;
},
set: newVal => {
this.observe(newVal);
if (newVal !== value) {
value = newVal;
}
return true;
}
});
}
}
export default Observer;
Watcher
为什么这里添加的 watcher 是字符串呢?
原因自然是......watcher 还没写得嘛。
所以我们现在开始分析 wathcer。wathcer 的核心功能是当收到数据变化的通知的时候,去更新视图,那么自然 watcher 要和 updater 绑定起来,而前面的 compile 也是和 updater 绑定起来的,显然,当初始化视图结束之后就与建立 watcher 联系是不错的方案:
import Watcher from "./watcher.js";
const compileUtil = {
text(node, exp, wm) {
const value = this._getValue(exp, wm);
updaterUtil.text(node, value);
new Watcher();
},
html(node, exp, wm) {
const value = this._getValue(exp, wm);
updaterUtil.html(node, value);
new Watcher();
},
module(node, exp, wm) {
const value = this._getValue(exp, wm);
updaterUtil.module(node, value);
new Watcher();
},
on(node, exp, wm, event) {
const fn = wm.$options.methods && wm.$options.methods[exp];
node.addEventListener(event, fn.bind(wm), false);
new Watcher();
},
_getValue(exp, wm) {
return exp.split(".").reduce((d, c) => {
return d[c];
}, wm.$data);
}
};
上面代码冗余部分较多,这里可以抽离一个 bind 方法来进行绑定和统一的 updater:
import Watcher from "./watcher.js";
const compileUtil = {
text(node, exp, wm) {
M this.bind(node, exp, wm, "text");
},
html(node, exp, wm) {
M this.bind(node, exp, wm, "html");
},
module(node, exp, wm) {
M this.bind(node, exp, wm, "module");
},
on(node, exp, wm, event) {
const fn = wm.$options.methods && wm.$options.methods[exp];
node.addEventListener(event, fn.bind(wm), false);
},
+ bind(node, exp, wm, dir) {
+ const value = this.getValue(exp, wm);
+ updaterUtil[dir](node, value);
+ new Watcher();
+ },
getValue(exp, wm) {
return exp.split(".").reduce((d, c) => {
return d[c];
}, wm.$data);
}
};
接下来我们来实现 watcher。首先分析一下,watcher 需要什么参数呢?我们知道,watcher 的核心功能是当接到通知的时候触发视图更新,那么自然它需要 exp,和 wm 来找到节点变量和值,接下来由于我们在 updater 中绑定 watcher,那就可以通过回调的方式获取到 watcher 中的新值,那么 watcher 需要的参数就定下来了:
import Watcher from "./watcher.js";
const compileUtil = {
text(node, exp, wm) {
this.bind(node, exp, wm, "text");
},
html(node, exp, wm) {
this.bind(node, exp, wm, "html");
},
module(node, exp, wm) {
this.bind(node, exp, wm, "module");
},
on(node, exp, wm, event) {
const fn = wm.$options.methods && wm.$options.methods[exp];
node.addEventListener(event, fn.bind(wm), false);
},
bind(node, exp, wm, dir) {
const value = this.getValue(exp, wm);
+ const updaterFn = updaterUtil[dir];
+ updaterFn && updaterFn(node, value);
new Watcher(wm, exp, value => {
+ updaterFn && updaterFn(node, value);
});
},
getValue(exp, wm) {
return exp.split(".").reduce((d, c) => {
return d[c];
}, wm.$data);
}
};
const updaterUtil = {
text(node, value) {
node.textContent = value;
},
html(node, value) {
node.innerHTML = value;
},
module(node, value) {
node.value = value;
}
};
export default compileUtil;
+class Watcher {
+ constructor(wm, exp, cb) {
+ this.wm = wm;
+ this.exp = exp;
+ this.cb = cb;
+ }
+}
+export default Watcher;
watcher 能在数据发生变化的时候触发视图更新,自然我们要去获取老值,然后从通知中获取新值,如果二者不同则触发回掉更新视图:
import compileUtil from "./utils.js";
class Watcher {
constructor(wm, exp, cb) {
this.wm = wm;
this.exp = exp;
this.cb = cb;
+ this.oldVal = this._getOldVal();
}
+ _getOldVal() {
+ return compileUtil.getValue(this.exp, this.wm);
+ }
+ updater() {
+ const newVal = compileUtil.getValue(this.exp, this.wm);
+ if (this.oldVal !== newVal) {
+ this.cb(newVal)
+ }
+ }
}
export default Watcher;
watcher 的核心功能到这里基本差不多了,前面提到,我们在数据劫持的过程中,利用访问器将 watcher 收集起来,那么怎么获取这个 watcher 呢?new 肯定是不行的,因为是新的实例了,所以这里有个小技巧,在 _getOldVal 的时候,将 watcher 绑定在 Dep 上,这样就可以获取到 Watcher 了:
_getOldVal() {
+ Dep.target = this;
M const oldVal = compileUtil.getValue(this.exp, this.wm);
+ Dep.target = null;
+ return oldVal;
}
这样,我们在依赖收集的时候,就可以利用 Dep.target 将 watcher 添加进数组中了:
get: _ => {
M Dep.target && dep.addWater(Dep.target);
return value;
}
有了监视器,有了更新方法,按理来说应该已经能实现数据驱动视图的更新了,结果如下:
