使用 Proxy + Promise 实现 依赖收集
(深入浅出Vue基于“依赖收集”的响应式原理) ,这篇文章讲的是通过一个通俗易懂例子,介绍了 如何用Object.defineProperty 实现的“依赖收集”的原理。Object.defineProperty
属于ES5的特性,而ES6 带来了Proxy特性。这里先介绍一下:
看到这个让我想起了几年前我看过的印象深刻的一句话“ 框架是语法的补充”(网上搜索了下,居然找不到出处了),看起来, Javascript 现在居然自带Aspect Oriented Programming(AOP) 框架了。
Proxy 实现动态代理类似于middleware(中间件),可以对 对象 的基本操作进行统一处理,很适合实现 “依赖收集“。
而Vue.js 2.0 的响应式主要依赖 Object.defineProperty,其具有较好地浏览器兼容性,但是其无法直接监听数组下标方式变更以及动态添加的属性;而 Vue.js 3 中则计划
使用 ES6 Proxy 来实现响应式监听,其能够简化源代码、易于学习,并且还能带来更好地性能表现。(https://blog.cloudboost.io/reactivity-in-vue-js-2-vs-vue-js-3-
这边顺便提一下,我认为“依赖收集”的终极方案应该是ES7 的 Object.observe,不过被发起人自己撤除了。
下面通过简单的代码我们用 Proxy 来实现“依赖收集”的核心原理,为了让大家更好理解,我举了跟Object.defineProperty 相同的例子:
这是一个王者荣耀里的英雄:
const hero = { health: 3000, IQ: 150 }
一、使数据对象变得“可观测”
我们把原对象变成了可观测的代理对象 heroxy
1 const heroxy = new Proxy(hero,{ 2 set (target, key, value, receiver) { 3 console.log(`我的${key}属性从$(target[key]) 变为 $(value)`); 4 return Reflect.set(target, key, value, receiver);//同样也是ES6新特性,比Object.defineProperty 更强大,更适合元编程,大家顺便一起学一下 5 } 6 };
我们来执行下:
1 heroxy.health = 5000; 2 heroxy.IQ = 200; 3 //-> 我的health属性从3000 变为 5000 4 //-> 我的200属性从150变为 200
代码确实简洁
二、计算属性
1 const heroxy = new Proxy(hero,{ 2 set (target, key, value, receiver) { 3 console.log(`我的${key}属性从$(target[key]) 变为 $(value)`); 4 return Reflect.set(target, key, value, receiver); 5 } 6 7 get (target, key, receiver) { 8 9 if(key == "type"){ 10 11 const _val = target.health > 4000 ? '坦克' : '脆皮'; 12 console.log(`我的类型是:${val}`); 13 return _val ; 14 } 15 else{ 16 Reflect.get(target, key, receiver); 17 } 18 } 19 };
1 heroxy.health = 5000 2 heroxy.IQ = 200 3 heroxy.type 4 //-> 我的health属性从3000 变为 5000 5 //-> 我的200属性从150变为 200 6 //-> 坦克 7
const computerDict = { "type": { computer(target){ return target.health > 4000 ? '坦克' : '脆皮'; }, onDepUpdated(val){ console.log(`我的类型是:${val}`); } }, } const proDict = { "health":[] , //为什么要定义集合,下面再说 "IQ":[] }
1 const heroxy = new Proxy(hero,{ 2 set (target, key, value, receiver) { 3 const _pro = proDict[key]; 4 if(_pro){ 5 6 console.log(`我的${key}属性从$(target[key]) 变 7 为 $(value)`); 8 9 } 10 const _com = computerDict[key]; 11 if(_com){ 12 console.error('计算属性无法被赋值!') 13 } 14 return Reflect.set(target, key, value, receiver); 15 }, 16 17 18 get (target, key, receiver) { 19 const _com = computerDict[key]; 20 if(_com){ 21 const _val = _com.computer(target); 22 _com.onDepUpdated(_val); 23 return _val ; 24 25 } 26 else 27 { 28 29 return Reflect.get(target, key, receiver); 30 } 31 } 32 });
三、依赖收集
1 /** 2 * 定义一个“依赖收集器” 3 */ 4 const Dep = { 5 target: null 6 }
get (target, key, receiver) { const _com = computerDict[key]; if(_com){ Dep.target = _com.onDepUpdated ; const _val = _com.computer(target); Dep.target = null;//临时存放一下而已 / / _com.onDepUpdated(_val); return _val ; } else { return Reflect.get(target, key, receiver); } }
1 get (target, key, receiver) { 2 const _com = computerDict[key]; 3 if(_com){ 4 5 Dep.target = ()=> {_com.onDepUpdated(_com.computer(heroxy)); };//这里要用heroxy 6 const _val = _com.computer(target); 7 Dep.target = null; 8 // _com.onDepUpdated(_val); 9 return _val ; 10 11 } 12 const _pro = proDict[key]; 13 if(_pro){ 14 if (Dep.target && _pro.indexOf(Dep.target) === -1) { 15 _pro.push(Dep.target);//明白了 proDict[key]定义为数组就是为了存放触发的函数集合 16 17 } 18 19 } 20 return Reflect.get(target, key, receiver); 21 }
1 set (target, key, value, receiver) { 2 const _pro = proDict[key]; 3 if(_pro){ 4 5 console.log(`我的${key}属性从${target[key]} 变为 ${value}`); 6 Reflect.set(target, key, value, receiver); 7 _pro.forEach((dep) =>{ 8 dep(); 9 }); 10 return true ; 11 } 12 const _com = computerDict[key]; 13 if(_com){ 14 console.error('计算属性无法被赋值!') 15 } 16 return Reflect.set(target, key, value, receiver); 17 },
console.log(`英雄初始类型:${hero.type}`) hero.health = 5000 hero.health = 100 ->英雄初始类型:脆皮 ->我的health属性从100 变为 5000 ->我的类型是:坦克 ->我的health属性从5000 变为 100 ->我的类型是:脆皮
四、自动更新的问题.....
hero.health = 5000
hero.health = 100
五、Promise 一次更新
"type": { computer(target){ return target.health > 4000 ? '坦克' : '脆皮'; }, onDepUpdated(val){ new Promise(a=>a()).then(a=> {console.log(`我的类型是:${val}`);}); } },
console.log(`英雄初始类型:${heroxy.type}`) heroxy.health = 5000 heroxy.health = 100
英雄初始类型:脆皮 -> 我的health属性从3000 变为 5000 -> 我的health属性从5000 变为 100 -> 我的类型是:坦克 -> 我的类型是:脆皮
const Dep = { target: null, UpdateIndex:0 }
onDepUpdated(val,updateIndex){ new Promise(a=>a()).then(a=> { if(updateIndex == Dep.UpdateIndex){ console.log(`我的类型是:${val}`); Dep.UpdateIndex = 0 ;//记住操作完要把全局变量更新回去 } } ); }
get (target, key, receiver) { const _com = computerDict[key]; if(_com){ Dep.target = (updateIndex)=> { _com.onDepUpdated(_com.computer(heroxy),Dep.UpdateIndex); };//传入参数 const _val = _com.computer(heroxy); Dep.target = null; // _com.onDepUpdated(_val); return _val ; } const _pro = proDict[key]; if(_pro){ if (Dep.target && _pro.indexOf(Dep.target) === -1) { _pro.push(Dep.target) } } return Reflect.get(target, key, receiver); }
1 set (target, key, value, receiver) { 2 const _pro = proDict[key]; 3 if(_pro){ 4 5 console.log(`我的${key}属性从${target[key]} 变为 ${value}`); 6 Reflect.set(target, key, value, receiver); 7 _pro.forEach((dep) =>{ 8 Dep.UpdateIndex ++ ;//新增标记 9 dep(Dep.UpdateIndex); 10 }); 11 return true ; 12 } 13 const _com = computerDict[key]; 14 if(_com){ 15 console.error('计算属性无法被赋值!') 16 } 17 return Reflect.set(target, key, value, receiver); 18 } 19
1 英雄初始类型:脆皮 2 我的health属性从3000 变为 5000 3 我的health属性从5000 变为 100 4 我的类型是:脆皮
1 const hero = { 2 health: 3000, 3 IQ: 150 4 } 5 6 7 const computerDict = { 8 9 "type": { 10 11 computer(target){ 12 return target.health > 4000 ? '坦克' : '脆皮'; 13 }, 14 onDepUpdated(val,updateIndex){ 15 new Promise(a=>a()).then(a=> { 16 if(updateIndex == Dep.UpdateIndex){ 17 Dep.UpdateIndex = 0 ; 18 console.log(`我的类型是:${val}`); 19 } 20 }); 21 } 22 23 }, 24 } 25 26 27 const proDict = { 28 "health":[], 29 "IQ":[] 30 } 31 32 const Dep = { 33 target: null, 34 UpdateIndex:0 35 } 36 37 38 const heroxy = new Proxy(hero,{ 39 set (target, key, value, receiver) { 40 const _pro = proDict[key]; 41 if(_pro){ 42 43 console.log(`我的${key}属性从${target[key]} 变为 ${value}`); 44 Reflect.set(target, key, value, receiver); 45 _pro.forEach((dep) =>{ 46 Dep.UpdateIndex ++ ; 47 dep(Dep.UpdateIndex); 48 }); 49 return true ; 50 } 51 const _com = computerDict[key]; 52 if(_com){ 53 console.error('计算属性无法被赋值!') 54 } 55 return Reflect.set(target, key, value, receiver); 56 }, 57 58 59 get (target, key, receiver) { 60 const _com = computerDict[key]; 61 if(_com){ 62 63 Dep.target = (updateIndex)=> {_com.onDepUpdated(_com.computer(heroxy),Dep.UpdateIndex); }; 64 const _val = _com.computer(heroxy); 65 Dep.target = null; 66 // _com.onDepUpdated(_val); 67 return _val ; 68 69 } 70 const _pro = proDict[key]; 71 if(_pro){ 72 if (Dep.target && _pro.indexOf(Dep.target) === -1) { 73 _pro.push(Dep.target) 74 } 75 76 } 77 return Reflect.get(target, key, receiver); 78 } 79 });
参考链接:
(深入浅出Vue基于“依赖收集”的响应式原理) https://zhuanlan.zhihu.com/p/29318017
http://es6.ruanyifeng.com/#docs/proxy)
Aspect Oriented Programming(AOP) 框架https://baike.baidu.com/item/AOP/1332219?fr=aladdin。
https://blog.cloudboost.io/reactivity-in-vue-js-2-vs-vue-js-3-dcdd0728dcdf
https://esdiscuss.org/topic/an-update-on-object-observe
EventLoop (http://www.ruanyifeng.com/blog/2014/10/event-loop.html)