使用 Proxy + Promise 实现 依赖收集

     (深入浅出Vue基于“依赖收集”的响应式原理) ,这篇文章讲的是通过一个通俗易懂例子,介绍了 如何用Object.defineProperty 实现的“依赖收集”的原理。Object.defineProperty

属于ES5的特性,而ES6 带来了Proxy特性。这里先介绍一下:

    “ Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。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-

dcdd0728dcdf)。

这边顺便提一下,我认为“依赖收集”的终极方案应该是ES7 的 Object.observe,不过被发起人自己撤除了。

下面通过简单的代码我们用 Proxy 来实现“依赖收集”的核心原理,为了让大家更好理解,我举了跟Object.defineProperty 相同的例子:

这是一个王者荣耀里的英雄:

 const hero = {
                  health: 3000,
                  IQ: 150
                }

  

应用场景:
        当我 设置 /变更 hero.health 的值,会触发 hero.type 这个计算属性发生变化,而在vue里面就是导致视图的更新。

一、使数据对象变得“可观测”

    我们把原对象变成了可观测的代理对象 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 =  2003 //->  我的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  

 

通过上面两个例子,基本知道Proxy的用法了,接下去是依赖收集的核心,不过在这个之前我们先改下代码,首先定义两个 全局字典:
 
const computerDict = {
 
"type": {
 
       computer(target){
                 return target.health > 4000 ? '坦克' : '脆皮';
       },
     onDepUpdated(val){
              console.log(`我的类型是:${val}`);
       }
        },
}
 
 
const proDict = {
         "health":[] , //为什么要定义集合,下面再说
         "IQ":[]
}

 

 
修改Proxy
 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 部分:
 
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);
          }
}
 

 

在它的回调函数中,调用了英雄的health属性,也就是触发了对应的getter函数
继续修改get部分:
 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     }

 

最后修改set 部分:
 
 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

 

的时候,health应该要以最后一次设置,也就是100为准,hero.health = 5000
这时候不应该触发变更事件,视图不应该绘制,太浪费性能了。
但是我们又不可以手动调用更新,否则就太不“响应式了”。
我们又想自动更新,又想把收集到的依赖只做一次最终的批量更新,怎么办呢?
答案是 EventLoop 
简单说一下,我们可以把更新操作放到队列里面去,当我们主线程执行完的时候,才回去
调用队列里面的方法,ajax callback ,settimeout 就是这么干的,当然 promise 也是。
 

五、Promise 一次更新

 
我们改一下 onDepUpdated 函数
  
"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方法:
 
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);
}

 

 
 
修改set 方法:
 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

   

 

posted @ 2017-10-16 01:45  罗素  阅读(1289)  评论(0编辑  收藏  举报