声明 target 全局变量作为依赖(观察者)的中转站,myWatch 函数执行时用 target 缓存依赖,然后调用 data[key] 触发对应的 get 函数以收集依赖,set 函数被触发时会将 dep 里的依赖(观察者)都执行一遍。这里的 get set 函数形成闭包引用了上面的 dep 常量,这样一来,data 对象的每个属性都有了对应的依赖收集器。
且这一实现方式不需要通过 myWatch 函数显式地将 data 里的属性一一转为访问器属性。
但运行以下代码,会发现仍有问题:
console.log(data.box) data.box = 2 // '我是box的观察者' // '我是box的另一个观察者' // '我是bar的观察者'
四个 myWatch 执行完之后 target 缓存的值变成了最后一个 myWatch 方法调用时所传递的依赖(观察者),故执行 console.log(data.box) 读取 box 属性的值时,会将最后缓存的依赖存入 box 属性所对应的依赖收集器,故而再修改 box 的值时,会打印出 '我是bar的观察者'。
我想可以在每次收集完依赖之后,将全局变量 target 设置为空函数来解决这问题:
const data = { box: 1, foo: 1, bar: 1 } let target = null for (let key in data) { const dep = [] let value = data[key] Object.defineProperty(data, key, { set (newVal) { if (newVal === value) return value = newVal dep.forEach(f => { f() }) }, get () { dep.push(target) target = () => {} return value } }) } function myWatch(key, fn) { target = fn data[key] } myWatch('box', () => { console.log('我是box的观察者') }) myWatch('box', () => { console.log('我是box的另一个观察者') }) myWatch('foo', () => { console.log('我是foo的观察者') }) myWatch('bar', () => { console.log('我是bar的观察者') })
经测无误。
但开发过程中,还常碰到需观测嵌套对象的情形:
const data = { box: { gift: 'book' } }
这时,上述实现未能观测到 gift 的修改,显出不足。
如何进行深度观测?
——递归
通过递归将各级属性均转为响应式属性即可:
const data = { box: { gift: 'book' } } let target = null function walk(data) { for (let key in data) { const dep = [] let value = data[key] if (Object.prototype.toString.call(value) === '[object Object]') { walk(value) } Object.defineProperty(data, key, { set (newVal) { if (newVal === value) return value = newVal dep.forEach(f => { f() }) }, get () { dep.push(target) target = () => {} return value } }) } } walk(data) function myWatch(key, fn) { target = fn data[key] } myWatch('box', () => { console.log('我是box的观察者') }) myWatch('box.gift', () => { console.log('我是gift的观察者') }) data.box = {gift: 'basketball'} // '我是box的观察者' data.box.gift = 'guitar'
这时 gift 虽已是访问器属性,但 myWatch 方法执行时 data[box.gift] 未能触发相应 getter 以收集依赖, data[box.gift] 访问不到 gift 属性,data[box][gift] 才可以,故 myWatch 须改写如下:
function myWatch(exp, fn) { target = fn let pathArr, obj = data if (/\./.test(exp)) { pathArr = exp.split('.') pathArr.forEach(p => { obj = obj[p] }) return } data[exp] }
如果要读取的字段包括 . ,那么按照 . 将其分为数组,然后使用循环读取嵌套对象的属性值。
这时执行代码后发现,data.box.gift = 'guitar' 还是未能触发相应的依赖,即打印出 '我是gift的观察者' 这句信息。调试之后找到问题:
myWatch('box.gift', () => { console.log('我是gift的观察者') })
执行以上代码时,pathArr 即 ['box', 'gift'],循环内 obj = obj[p] 实际上就是 obj = data[box],读取了一次 box,触发了 box 对应的 getter,收集了依赖:
() => { console.log('我是gift的观察者') }
收集完将全局变量 target 置为空函数,而后,循环继续执行,又读取了 gift 的值,但这时,target 已是空函数,导致属性 gift 对应的 getter 收集了一个“空依赖”,故,data.box.gift = 'guitar' 的操作不能触发期望的依赖。
以上代码有两个问题:
修改 box 会触发“我是gift的观察者”这一依赖
修改 gift 未能触发“我是gift的观察者”的依赖
第一个问题,读取 gift 时,必然经历读取 box 的过程,故触发 box 对应的 getter 无可避免,那么,box 对应 getter 收集 gift 的依赖也就无可避免。但想想也算合理,因为 box 修改时,隶属于 box 的 gift 也算作修改,从这一点看,问题一也不算作问题,划去。
第二个问题,我想可以这样解决: