(深入浅出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. 【Alpha】第一次Daily Scrum Meeting

    一.今日站立式会议照片 二.会议内容 1.调研市场现有礼物挑选软件,分析优势,亮点,劣势 2.确立开发环境和安装调试 三.燃尽图 四.遇到的困难 在准备开发环境和安装调试时遇到系统和开发环境不要兼容, ...

  2. Git和Github使用

    什么是Git? Git 是一个快速.可扩展的分布式版本控制系统,它具有极为丰富的命令集,对内部系统提供了高级操作和完全访问. 版本控制 简单地说,就是将在本地开发的代码,定时推送到服务器.每一次修改, ...

  3. 团队作业9——展示博客(Bata版本)

    1.团队成员介绍及项目地址 团队的源码仓库地址:https://coding.net/u/app24dian/p/app24dian/git 陈麟凤:(http://www.cnblogs.com/c ...

  4. 【Beta】 第三次Daily Scrum Meeting

    一.本次会议为第三次meeting会议 二.时间:10:00AM-10:20AM 地点:禹州楼 三.会议站立式照片 四.今日任务安排 成员 昨日任务 今日任务 林晓芳 查询app提醒功能模块和用户登录 ...

  5. java课程设计--We Talk(201521123061)

    java课程设计--We Talk(201521123061) 团队博客链接:http://www.cnblogs.com/slickghost/ 数据库 一.通过Dao模式建立与数据库的连接 1.数 ...

  6. 201521123053《Java程序设计》第3周学习总结

    ---恢复内容开始--- 1. 本周学习总结 2. 书面作业 1. 代码阅读 以上代码可否编译通过?哪里会出错?为什么?尝试改正?        如果创建3个Test1对象,有内存中有几个i,几个j? ...

  7. 201521123059 《Java程序设计》第十二周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多流与文件相关内容. 2. 书面作业 将Student对象(属性:int id, String name,int age,doubl ...

  8. Java课程设计-随机密码生成器

    1.团队课程设计博客链接 团队课程设计博客地址 2.个人负责模板 随即密码生成器算法 3.自己的代码提交记录截图 4.自己负责模块或任务详细说明 负责随机密码算法设计实现 通过不同种类选择下生成密码, ...

  9. iOS内购 服务端票据验证及漏单引发的思考.

    因业务需要实现了APP内购处理,但在过程中出现了部分不可控的因素,导致部分用户反映有充值不成并漏单的情况. 仔细考虑了几个付费安全上的问题,凡是涉及到付费的问题都很敏感,任何一方出现损失都是不能接受的 ...

  10. linux 编辑文件时 E45: 'readonly' option is set (add ! to override) 隐藏属性 chattr lsattr

    在改一个系统当中的文件参数时, vim config.php 时,提示 E45: 'readonly' option is set (add ! to override) ,同时不能编辑不能删除不能设 ...