function myWatch(exp, fn) { let pathArr, obj = data if (/\./.test(exp)) { pathArr = exp.split('.') pathArr.forEach(p => { target = fn obj = obj[p] }) return } target = fn data[exp] } data.box.gift = 'guitar' // '我是gift的观察者' data.box = {gift: 'basketball'} // '我是box的观察者' // '我是gift的观察者'
保证属性读取时 target = fn 即可。
那么:
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(exp, fn) { let pathArr, obj = data if (/\./.test(exp)) { pathArr = exp.split('.') pathArr.forEach(p => { target = fn obj = obj[p] }) return } target = fn data[exp] } myWatch('box', () => { console.log('我是box的观察者') }) myWatch('box.gift', () => { console.log('我是gift的观察者') })
现在我想,假如我有以下数据:
const data = { player: 'James Harden', team: 'Houston Rockets' }
执行以下代码:
function render() { document.body.innerText = `The last season's MVP is ${data.player}, he's from ${data.team}` } render() myWatch('player', render) myWatch('team', render) data.player = 'Kobe Bryant' data.team = 'Los Angeles Lakers'
是不是就可以将数据映射到页面,并响应数据的变化?
执行代码发现,data.player = 'Kobe Bryant' 报错,究其原因,render 方法执行时,会去获取 data.player 和 data.team 的值,但此时,target 为 null,那么读取 player 时对应的依赖收集器 dep 便收集了 null,导致 player 的 setter 调用依赖时报错。
那么我想,在 render 执行时便主动去收集依赖,就不会导致 dep 里收集了 null。
细看 myWatch,这方法做的事情其实就是帮助 getter 收集依赖,它的第一个参数就是要访问的属性,要触发谁的 getter,第二个参数是相应要收集的依赖。
这么看来,render 方法既可以帮助 getter 收集依赖(render 执行时会读取 player team),而且它本身就是要收集的依赖。那么,我能不能修改一下 myWatch 的实现,以支持这样的写法:
myWatch(render, render)
第一个参数作为函数执行一下便有了之前第一个参数的作用,第二个参数还是需要被收集的依赖,嗯,想来合理。
那么,myWatch 改写如下:
function myWatch(exp, fn) { target = fn if (typeof exp === 'function') { exp() return } let pathArr, obj = data if (/\./.test(exp)) { pathArr = exp.split('.') pathArr.forEach(p => { target = fn obj = obj[p] }) return } data[exp] }
但,对 team 的修改未能触发页面更新,想来因为 render 执行读取 player 收集依赖后 target 变为空函数,导致读取 team 收集依赖时收集到了空函数。这里大家的依赖都是 render,故可将 target = () => {} 这句删去。
myWatch 这样实现还有个好处,假如 data 中有许多属性都需要通过 render 渲染至页面,一句 myWatch(render, render) 便可,无须如此这般繁复:
myWatch('player', render) myWatch('team', render) myWatch('number', render) myWatch('height', render) ...
那么最终:
const data = { player: 'James Harden', team: 'Houston Rockets' } 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) return value } }) } } walk(data) function myWatch(exp, fn) { target = fn if (typeof exp === 'function') { exp() return } let pathArr, obj = data if (/\./.test(exp)) { pathArr = exp.split('.') pathArr.forEach(p => { target = fn obj = obj[p] }) return } data[exp] } function render() { document.body.innerText = `The last season's MVP is ${data.player}, he's from ${data.team}` } myWatch(render, render)