(深入浅出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

 const heroxy = new Proxy(hero,{
set (target, key, value, receiver) {
console.log(`我的${key}属性从$(target[key]) 变为 $(value)`);
return Reflect.set(target, key, value, receiver);//同样也是ES6新特性,比Object.defineProperty 更强大,更适合元编程,大家顺便一起学一下
}
};

我们来执行下:

      heroxy.health = 5000;
heroxy.IQ = 200;
//-> 我的health属性从3000 变为 5000
//-> 我的200属性从150变为 200

代码确实简洁

二、计算属性

 
 const heroxy = new Proxy(hero,{
set (target, key, value, receiver) {
      console.log(`我的${key}属性从$(target[key]) 变为 $(value)`);
      return Reflect.set(target, key, value, receiver);
        }       get (target, key, receiver) {           if(key == "type"){             const _val = target.health > 4000 ? '坦克' : '脆皮';
             console.log(`我的类型是:${val}`);
            return _val ;
          }
else{
Reflect.get(target, key, receiver);
}
}
};
 
 
我们来执行下:
 heroxy.health = 5000
heroxy.IQ = 200
heroxy.type
//-> 我的health属性从3000 变为 5000
//-> 我的200属性从150变为 200
//-> 坦克
通过上面两个例子,基本知道Proxy的用法了,接下去是依赖收集的核心,不过在这个之前我们先改下代码,首先定义两个 全局字典:
 
const computerDict = {

"type": {

       computer(target){
return target.health > 4000 ? '坦克' : '脆皮';
},
onDepUpdated(val){
console.log(`我的类型是:${val}`);
}
},
} const proDict = {
"health":[] , //为什么要定义集合,下面再说
"IQ":[]
}
 
修改Proxy
 const heroxy = new Proxy(hero,{
set (target, key, value, receiver) {
const _pro = proDict[key];
if(_pro){ console.log(`我的${key}属性从$(target[key]) 变
为 $(value)`); }
const _com = computerDict[key];
if(_com){
console.error('计算属性无法被赋值!')
}
return Reflect.set(target, key, value, receiver);
}, get (target, key, receiver) {
const _com = computerDict[key];
if(_com){
const _val = _com.computer(target);
_com.onDepUpdated(_val);
return _val ; }
else
{ return Reflect.get(target, key, receiver);
}
}
});
 
这样我们代码变得更加声明式,更容易扩展
 

三、依赖收集

 
同样的 ,定义一个依赖收集器
 /**
* 定义一个“依赖收集器”
*/
const Dep = {
target: null
}
当我们第一次调用计算属性的时候,改写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部分:
 get (target, key, receiver) {
      const _com = computerDict[key];
       if(_com){             Dep.target = ()=> {_com.onDepUpdated(_com.computer(heroxy)); };//这里要用heroxy
            const _val = _com.computer(target);
           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);//明白了 proDict[key]定义为数组就是为了存放触发的函数集合 } }
return Reflect.get(target, key, receiver);
}
最后修改set 部分:
 
 set (target, key, value, receiver) {
const _pro = proDict[key];
if(_pro){ console.log(`我的${key}属性从${target[key]} 变为 ${value}`);
Reflect.set(target, key, value, receiver);
_pro.forEach((dep) =>{
dep();
});
return true ;
}
const _com = computerDict[key];
if(_com){
console.error('计算属性无法被赋值!')
}
return Reflect.set(target, key, value, receiver);
},
 
我们来跑一下:
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 方法:
 set (target, key, value, receiver) {
const _pro = proDict[key];
if(_pro){ console.log(`我的${key}属性从${target[key]} 变为 ${value}`);
Reflect.set(target, key, value, receiver);
_pro.forEach((dep) =>{
Dep.UpdateIndex ++ ;//新增标记
dep(Dep.UpdateIndex);
});
return true ;
}
const _com = computerDict[key];
if(_com){
console.error('计算属性无法被赋值!')
}
return Reflect.set(target, key, value, receiver);
}
重新执行:
 英雄初始类型:脆皮
我的health属性从3000 变为 5000
我的health属性从5000 变为 100
我的类型是:脆皮
 
大功告成,这个就是我们要的结果。
 
所有代码以下,优化就不做了.....
 const hero = {
health: 3000,
IQ: 150
} const computerDict = { "type": { computer(target){
return target.health > 4000 ? '坦克' : '脆皮';
},
onDepUpdated(val,updateIndex){
new Promise(a=>a()).then(a=> {
if(updateIndex == Dep.UpdateIndex){
Dep.UpdateIndex = 0 ;
console.log(`我的类型是:${val}`);
}
});
} },
} const proDict = {
"health":[],
"IQ":[]
} const Dep = {
target: null,
UpdateIndex:0
} const heroxy = new Proxy(hero,{
set (target, key, value, receiver) {
const _pro = proDict[key];
if(_pro){ console.log(`我的${key}属性从${target[key]} 变为 ${value}`);
Reflect.set(target, key, value, receiver);
_pro.forEach((dep) =>{
Dep.UpdateIndex ++ ;
dep(Dep.UpdateIndex);
});
return true ;
}
const _com = computerDict[key];
if(_com){
console.error('计算属性无法被赋值!')
}
return Reflect.set(target, key, value, receiver);
}, 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);
}
});

参考链接:

(深入浅出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

使用 Proxy + Promise 实现 依赖收集的更多相关文章

  1. 读Vue源码 (依赖收集与派发更新)

    vue的依赖收集是定义在defineReactive方法中,通过Object.defineProperty来设置getter,红字部分主要做依赖收集,先判断了Dep.target如果有的情况会执行红字 ...

  2. 三、vue依赖收集

    Vue 会把普通对象变成响应式对象,响应式对象 getter 相关的逻辑就是做依赖收集,这一节我们来详细分析这个过程 Dep Dep 是整个 getter 依赖收集的核心,它的定义在 src/core ...

  3. 【Vue源码学习】依赖收集

    前面我们学习了vue的响应式原理,我们知道了vue2底层是通过Object.defineProperty来实现数据响应式的,但是单有这个还不够,我们在data中定义的数据可能没有用于模版渲染,修改这些 ...

  4. Vue.js依赖收集

    写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出.文章的原地址:https://github.com/an ...

  5. Vue依赖收集引发的问题

    问题背景 在我们的项目中有一个可视化配置的模块,是通过go.js生成canvas来实现的.但是,我们发现这个模块在浏览器中经常会引起该tab页崩溃.开启chrome的任务管理器一看,进入该页面内存和c ...

  6. 深入浅出Vue基于“依赖收集”的响应式原理(转)

    add by zhj: 文章写的很通俗易懂,明白了Object.defineProperty的用法 原文:https://zhuanlan.zhihu.com/p/29318017 每当问到VueJS ...

  7. Vue 依赖收集原理分析

    此文已由作者吴维伟授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. Vue实例在初始化时,可以接受以下几类数据: 模板 初始化数据 传递给组件的属性值 computed wat ...

  8. vue的双向绑定和依赖收集

    在掘金上买了一个关于解读vue源码的小册,因为是付费的,所以还比较放心 在小册里看到了关于vue双向绑定和依赖收集的部分,总感觉有些怪怪的,然后就自己跟着敲了一遍. 敲完后,发现完全无法运行,  坑啊 ...

  9. webpack源码-依赖收集

    webpack源码-依赖收集 version:3.12.0 程序主要流程: 触发make钩子 Compilation.js 执行EntryOptionPlugin 中注册的make钩子 执行compi ...

随机推荐

  1. 看懂c/c++ 函数、指针、数组定义

    读懂 函数 + 指针 + 数组 c语言运算符机器优先级,看这里 结合运算符优先级,我们试着读懂函数和指针 优先级简单看 表达式提升():一级优先 函数():二级优先 数组[]:二级优先 指针定义*:三 ...

  2. 201521123049 《JAVA程序设计》 第1周学习总结

    1. 本章学习总结 1.认识了新的一门计算机编程语言JAVA: 2.JAVA的编写与C语言类似,都是不能利用指针进行编写: 3.在实验课上初步认识JAVA并利用JAVA进行简单的编程,在实践上得到进一 ...

  3. 201521123117 《Java程序设计》第1周学习总结

    第一周学习总结: 这周开始学习了Java,因为之前C语言没学好所以看部分东西还是一头雾水,但是在查阅资料和同学的帮助开始对Java有了一些了解,这周主要学习了Java的诞生发展以及运用包括JVN/JR ...

  4. 关闭Sublime Text的自动更新的方法

    每次打开Sublime text 软件都会提示我让我更新软件,如图: 经过仔细的研究发现可以通过以下途径关闭软件的自动更新 打开Submine Text,找到Preferences -> Set ...

  5. 201521123027 <java程序设计>第11周学习总结

    1.本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多线程相关内容. 2.书面作业 1.互斥访问与同步访问 完成题集4-4(互斥访问)与4-5(同步访问) 1.1 除了使用synchro ...

  6. 201521123066 《Java程序设计》第十四周学习总结

    1. 本周学习总结 2. 书面作业 1. MySQL数据库基本操作 建立数据库,将自己的姓名.学号作为一条记录插入.(截图,需出现自己的学号.姓名) 在自己建立的数据库上执行常见SQL语句(截图) - ...

  7. python函数式编程,列表生成式

    1.python 中常见的集中存储数据的结构: 列表 集合 字典 元组 字符串 双队列 堆 其中最常见的就是列表,字典. 2.下面讲一些运用循环获取字典列表的元素 >>> dic={ ...

  8. POJ--3258 River Hopscotch (最小值最大化C++)

    River Hopscotch Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 15273   Accepted: 6465 ...

  9. java基础知识3--如何获取资源文件(Java中获取资源文件的url)

    java开发中,常见的resource文件有:.xml,.properties,.txt文件等,后台开发中经常用到读取资源文件,处理业务逻辑,然后返回结果. 获取资源文件的方法说明getResourc ...

  10. Dstl Satellite Imagery Feature Detection-Data Processing Tutorial

    如何读取WKT格式文件 我们找到了这些有用的包: Python - shapely.loads() R - rgeos 如何读取geojson格式文件 我们找到了这些有用的包: Python -  j ...