深入浅出基于“依赖收集”的响应式原理
每当问到VueJS响应式原理,大家可能都会脱口而出“Vue通过 一、使数据对象变得“可观测”首先,我们定义一个数据对象,就以王者荣耀里面的其中一个英雄为例子: const hero = { health: 3000,IQ: 150 } 我们定义了这个英雄的生命值为3000,IQ为150。但是现在还不知道他是谁,不过这不重要,只需要知道这个英雄将会贯穿我们整篇文章,而我们的目的就是通过这个英雄的属性,知道这个英雄是谁。 现在我们可以通过 关于
在本文中,我们只使用这个方法使对象变得“可观测”,更多关于这个方法的具体内容,请参考https://developer.mozilla.org...,就不再赘述了。 那么如何让这个英雄主动通知我们其属性的读写情况呢?首先改写一下上面的例子: let hero = {} let val = 3000 Object.defineProperty(hero,'health',{ get () { console.log('我的health属性被读取了!') return val },set (newVal) { console.log('我的health属性被修改了!') val = newVal } }) 我们通过 console.log(hero.health) // -> 3000 // -> 我的health属性被读取了! hero.health = 5000 // -> 我的health属性被修改了 可以看到,英雄已经可以主动告诉我们其属性的读写情况了,这也意味着,这个英雄的数据对象已经是“可观测”的了。为了把英雄的所有属性都变得可观测,我们可以想一个办法: /** * 使一个对象转化成可观测对象 * @param { Object } obj 对象 * @param { String } key 对象的key * @param { Any } val 对象的某个key的值 */ function defineReactive (obj,key,val) { Object.defineProperty(obj,{ get () { // 触发getter console.log(`我的${key}属性被读取了!`) return val },set (newVal) { // 触发setter console.log(`我的${key}属性被修改了!`) val = newVal } }) } /** * 把一个对象的每一项都转化成可观测对象 * @param { Object } obj 对象 */ function observable (obj) { const keys = Object.keys(obj) keys.forEach((key) => { defineReactive(obj,obj[key]) }) return obj } 现在我们可以把英雄这么定义: const hero = observable({ health: 3000,IQ: 150 }) 读者们可以在控制台自行尝试读写英雄的属性,看看它是不是已经变得可观测的。 二、计算属性现在,英雄已经变得可观测,任何的读写操作他都会主动告诉我们,但也仅此而已,我们仍然不知道他是谁。如果我们希望在修改英雄的生命值和IQ之后,他能够主动告诉他的其他信息,这应该怎样才能办到呢?假设可以这样: watcher(hero,'type',() => { return hero.health > 4000 ? '坦克' : '脆皮' }) 我们定义了一个 那么,我们应该怎样才能正确构造这个监听器呢?可以看到,在设想当中,监听器接收三个参数,分别是被监听的对象、被监听的属性以及回调函数,回调函数返回一个该被监听属性的值。顺着这个思路,我们尝试着编写一段代码: /** * 当计算属性的值被更新时调用 * @param { Any } val 计算属性的值 */ function onComputedUpdate (val) { console.log(`我的类型是:${val}`); } /** * 观测者 * @param { Object } obj 被观测对象 * @param { String } key 被观测对象的key * @param { Function } cb 回调函数,返回“计算属性”的值 */ function watcher (obj,cb) { Object.defineProperty(obj,{ get () { const val = cb() onComputedUpdate(val) return val },set () { console.error('计算属性无法被赋值!') } }) } 现在我们可以把英雄放在监听器里面,尝试跑一下上面的代码: watcher(hero,() => { return hero.health > 4000 ? '坦克' : '脆皮' }) hero.type hero.health = 5000 hero.type // -> 我的health属性被读取了! // -> 我的类型是:脆皮 // -> 我的health属性被修改了! // -> 我的health属性被读取了! // -> 我的类型是:坦克 现在看起来没毛病,一切都运行良好,是不是就这样结束了呢?别忘了,我们现在是通过手动读取 三、依赖收集我们知道,当一个可观测对象的属性被读写时,会触发它的getter/setter方法。换个思路,如果我们可以在可观测对象的getter/setter里面,去执行监听器里面的 由于监听器内的 这个第三方就做一件事情——收集监听器内的回调函数的值以及 现在我们把这个第三方命名为“依赖收集器”,一起来看看应该怎么写: const Dep = { target: null } 就是这么简单。依赖收集器的target就是用来存放监听器里面的 定义完依赖收集器,我们回到监听器里,看看应该在什么地方把 function watcher (obj,cb) { // 定义一个被动触发函数,当这个“被观测对象”的依赖更新时调用 const onDepUpdated = () => { const val = cb() onComputedUpdate(val) } Object.defineProperty(obj,{ get () { Dep.target = onDepUpdated // 执行cb()的过程中会用到Dep.target, // 当cb()执行完了就重置Dep.target为null const val = cb() Dep.target = null return val },set () { console.error('计算属性无法被赋值!') } }) } 我们在监听器内部定义了一个新的 重新看一下我们的watcher实例: watcher(hero,() => { return hero.health > 4000 ? '坦克' : '脆皮' }) 在它的回调函数中,调用了英雄的 function defineReactive (obj,val) { const deps = [] Object.defineProperty(obj,{ get () { if (Dep.target && deps.indexOf(Dep.target) === -1) { deps.push(Dep.target) } return val },set (newVal) { val = newVal deps.forEach((dep) => { dep() }) } }) } 可以看到,在这个方法里面我们定义了一个空数组 至于为什么这里的 完成了这些步骤,基本上我们整个响应式系统就已经搭建完成,下面贴上完整的代码: /** * 定义一个“依赖收集器” */ const Dep = { target: null } /** * 使一个对象转化成可观测对象 * @param { Object } obj 对象 * @param { String } key 对象的key * @param { Any } val 对象的某个key的值 */ function defineReactive (obj,{ get () { console.log(`我的${key}属性被读取了!`) if (Dep.target && deps.indexOf(Dep.target) === -1) { deps.push(Dep.target) } return val },set (newVal) { console.log(`我的${key}属性被修改了!`) val = newVal deps.forEach((dep) => { dep() }) } }) } /** * 把一个对象的每一项都转化成可观测对象 * @param { Object } obj 对象 */ function observable (obj) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj,keys[i],obj[keys[i]]) } return obj } /** * 当计算属性的值被更新时调用 * @param { Any } val 计算属性的值 */ function onComputedUpdate (val) { console.log(`我的类型是:${val}`) } /** * 观测者 * @param { Object } obj 被观测对象 * @param { String } key 被观测对象的key * @param { Function } cb 回调函数,返回“计算属性”的值 */ function watcher (obj,set () { console.error('计算属性无法被赋值!') } }) } const hero = observable({ health: 3000,IQ: 150 }) watcher(hero,() => { return hero.health > 4000 ? '坦克' : '脆皮' }) console.log(`英雄初始类型:${hero.type}`) hero.health = 5000 // -> 我的health属性被读取了! // -> 英雄初始类型:脆皮 // -> 我的health属性被修改了! // -> 我的health属性被读取了! // -> 我的类型是:坦克 上述代码可以直接在code pen或者浏览器控制台上执行。 四、代码优化在上面的例子中,依赖收集器只是一个简单的对象,其实在 class Dep { constructor () { this.deps = [] } depend () { if (Dep.target && this.deps.indexOf(Dep.target) === -1) { this.deps.push(Dep.target) } } notify () { this.deps.forEach((dep) => { dep() }) } } Dep.target = null 同样的道理,我们对observable和watcher都进行一定的封装与优化,使这个响应式系统变得模块化: class Observable { constructor (obj) { return this.walk(obj) } walk (obj) { const keys = Object.keys(obj) keys.forEach((key) => { this.defineReactive(obj,obj[key]) }) return obj } defineReactive (obj,val) { const dep = new Dep() Object.defineProperty(obj,{ get () { dep.depend() return val },set (newVal) { val = newVal dep.notify() } }) } } class Watcher { constructor (obj,cb,onComputedUpdate) { this.obj = obj this.key = key this.cb = cb this.onComputedUpdate = onComputedUpdate return this.defineComputed() } defineComputed () { const self = this const onDepUpdated = () => { const val = self.cb() this.onComputedUpdate(val) } Object.defineProperty(self.obj,self.key,{ get () { Dep.target = onDepUpdated const val = self.cb() Dep.target = null return val },set () { console.error('计算属性无法被赋值!') } }) } } 然后我们来跑一下: const hero = new Observable({ health: 3000,IQ: 150 }) new Watcher(hero,() => { return hero.health > 4000 ? '坦克' : '脆皮' },(val) => { console.log(`我的类型是:${val}`) }) console.log(`英雄初始类型:${hero.type}`) hero.health = 5000 // -> 英雄初始类型:脆皮 // -> 我的类型是:坦克 代码已经放在code pen,浏览器控制台也是可以运行的~ 五、尾声看到上述的代码,是不是发现和VueJS源码里面的很像?其实VueJS的思路和原理也是类似的,只不过它做了更多的事情,但核心还是在这里边。 在学习VueJS源码的时候,曾经被响应式原理弄得头昏脑涨,并非一下子就看懂了。后在不断的思考与尝试下,同时参考了许多其他人的思路,才总算把这一块的知识点完全掌握。希望这篇文章对大家有帮助,如果发现有任何错漏的地方,也欢迎向我指出,谢谢大家~ (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |