前言
vue2 在开发的时候,es6 并未完全普及,因此 vue2 对对象的属性的变化监听采用了 es5 的 Object.defineProperty方法来实现。但是由于 Object.defineProperty的局限性,只能对对象已有的属性进行改造成 get / set实现监听,无法监听到对象的属性新增和属性删除操作。
在对象属性的 get 操作时进行依赖收集,在对象属性的 set 操作时进行依赖的触发。收集的依赖其实就是 Watcher 的实例。而且对象收集和建立 Watcher 二者之间本来毫无关系。是巧妙地让 Watcher 实例化的时候获取了值,触发 get 操作。get 就会知道要收集依赖了。但是这个依赖也就是 Watcher 实例是无法通过 get 的时候传递的,因此,vue 就在 Watcher 实例化的时候将自己放到了一个约定的地方,即window.target处,在触发 get 操作时会去该地方取依赖,即可完成依赖的收集。
实现
class Dep {constructor() {// subs 是依赖,依赖就是 Watcher 的实例this.subs = []}addSub(sub) {this.subs.push(sub);}removeSub(sub) {const index = this.subs.indexOf(sub);if(index > -1) {this.subs.splice(index, 1);}}depend() {if(window.target) {this.addSub(window.target);}}notify() {// 遍历通知全部依赖const subs = this.subs;for(let i = 0; i < subs.length; i++) {subs[i].update();}}}function defineReactive(data, key, val) {// 对象嵌套时,递归子属性if(typeof val === "object") {new Observer(val);}let dep = new Dep();Object.defineProperty(data, key, {enumerable: true,configurable: true,get: function() {// 收集依赖dep.depend();return val;},set: function (newVal) {if(newVal === val) return;val = newVal;// 通知依赖dep.notify();}})}class Watcher {constructor(data, exp, cb) {this.data = data;// 执行 this.getter 就可以从 data 读取值this.getter = this.parsePath(exp);this.cb = cb;// 获取依赖的实际值this.value = this.get();}get() {window.target = this;let value = this.getter(this.data);window.target = undefined;return value;}update() {const oldVal = this.value;// 获取新的值,不会再收集一遍依赖,因为没有改变 window.targetthis.value = this.getter(this.data);this.cb(this.value, oldVal);}parsePath(path) {const regex = /[^\w.&]/if(regex.test(path)) return;const keys = path.split(".");return function(obj) {for(let i = 0; i < keys.length; i++) {if(!obj) return;obj = obj[keys[i]];}return obj;}}}class Observer {constructor(data) {this.data = data;if(!Array.isArray(this.data)) {this.walk(this.data);}}walk(obj) {const keys = Object.keys(obj);for(let i = 0; i < keys.length; i++) {defineReactive(obj, keys[i], obj[keys[i]]);}}}
使用上述的变化侦测。
const data = {a: {b: 123,c: 456}}new Observer(data);new Watcher(data, 'a.b', (val, oldVal) => {console.log(`监听到 a.b 变化了,变化后的值为 ${val},旧的值为 ${oldVal}`);})setTimeout(() => {console.log("修改数据")data.a.b = 789;}, 3000)
