15 分钟掌握vue-next响应式原理


Posted in Javascript onOctober 13, 2019

写在前面

最新 vue-next 的源码发布了,虽然是 pre-alpha 版本,但这时候其实是阅读源码的比较好的时机。在 vue 中,比较重要的东西当然要数它的响应式系统,在之前的版本中,已经有若干篇文章对它的响应式原理和实现进行了介绍,这里就不赘述了。在 vue-next 中,其实现原理和之前还是相同的,即通过观察者模式和数据劫持,只不过对其实现方式进行了改变。

对于解析原理的文章,我个人是比较喜欢那种“小白”风格的文章,即不要摘录特别多的代码,也不要阐述一些很深奥的原理与概念。在我刚接触 react 的时候,还记得有一篇利用 jquery 来介绍 react 的文章,从简入繁,面面俱到,其背后阐述的知识点对我后来学习 react 起到很多的帮助。

因此,这篇文章我也打算按这种风格来写一下利用最近空闲时间阅读 vue-next 响应式模块的源码的一些心得与体会,算是抛砖引玉,同时实现一个极简的响应式系统。

如有错误,还望指正。

预备知识

无论是阅读这篇文章,还是阅读 vue-next 响应式模块的源码,首先有两个知识点是必备的:

  • Proxy:es6 中新的代理内建工具类
  • Reflect:es6 中新的反射工具类

由于篇幅有限,这里也不详细赘述这两个类的用途与使用方法了,推荐三篇我认为不错的文章,仅供参考:

  • ES6 Proxies in Depth
  • ES6 Proxy Traps in Depth 
  • ES6 Reflection in Depth

接口

对于 vue-next 响应式系统的 RFC,可以参考这里。虽然距离现在有一段时间了,但是通过阅读源码,可以发现一些影子。

我们大体要实现的效果如下面的代码所示:

// 实现两个方法 reactive 和 effect

const state = reactive({
  count: 0
})

effect(() => {
  console.log('count: ', state.count)
})

state.count++ // 输入 count: 1

可以发现我们熟悉的依赖收集阶段(同时也是观察者模式的订阅过程),是在 effect 中进行的,依赖收集的准备工作(即数据劫持逻辑),是在 reactive 中进行的,而数据变化的触发响应的逻辑在后面的 state.count++ 代码执行时进行(同时也是观察者模式的发布过程),之后便会执行之前传入 effect 内部的回调函数并输入 count: 1。

类型与公共变量

由于 vue-next 用 ts 进行了重写,这里我也使用 ts 来实现这个极简版本的响应式系统。主要涉及到的类型和公共变量如下:

type Effect = Function;
type EffectMap = Map<string, Effect[]>;

let currentEffect: Effect;
const effectMap: EffectMap = new Map();
  • currentEffect:用来储存当前正在收集依赖的 effect
  • effectMap:代表目标对象每个 key 所对应的依赖于它的 effect 数组,也可以把它理解为观察者模式中的订阅者字典

利用 Proxy 实现数据劫持

在之前的版本中,vue 利用 Object.defineProperty 中的 setter 和 getter 来对数据对象进行劫持,vue-next 则通过 Proxy。众所周知,Object.defineProperty 所实现的数据劫持是有一定限制的,而 Proxy 就会强大很多。

首先,我们在脑后中,设想一下如何使用 Proxy 来实现数据劫持呢?很简单,大体结构如下所示:

export function reactive(obj) {
 const proxied = new Proxy(obj, handlers);

 return proxied;
}

这里的 handlers 是声明如何处理各个 trap 的逻辑,比如:

const handlers = {
  get: function(target, key, receiver) {
    ...
  },
  set: function(target, key, value, receiver) {
    ...
  },
  deleteProperty(target, key) {
    ...
  }
  // ...以及其他 trap
 }

由于这里是极简版本的实现,那么我们就仅仅实现 get 和 set 两个 trap 就可以了,分别对应依赖收集和触发响应的逻辑。

依赖收集

对于依赖收集的实现,由于是极简版本,实现的前提如下:

  • 不考虑对象的嵌套
  • 不考虑集合类型
  • 不考虑基础类型
  • 不考虑对代理对象的处理

哈哈,基本这四点排除之后,这个依赖收集函数就会很轻很薄,如下:

function(target, key: string, receiver) {
    // 仅仅在某个 effect 内部进行依赖收集
    if (currentEffect) {
     if (effectMap.has(key)) {
      const effects = effectMap.get(key);
      if (effects.indexOf(currentEffect) === -1) {
       effects.push(currentEffect);
      }
     } else {
      effectMap.set(key, [currentEffect]);
     }
    }

   return Reflect.get(target, key, receiver);
}

实现的逻辑很简单,其实就是观察者模式中注册订阅者的实现逻辑,值得注意的是,这里对于 target 的赋值逻辑,我们委托给 Reflect 来完成,虽然 target[key] 也是可以工作的,但是使用 Reflect 是更提倡的方式。

触发响应

触发响应的逻辑就比较简单了,其实是对应观察者模式中,发布事件的逻辑,如下:

function(target, key: string, value, receiver) {
    const result = Reflect.set(target, key, value, receiver);
    
    if (effectMap.has(key)) {
     effectMap.get(key).forEach(effect => effect());
    }

    return result;
}

同样,这里使用 Reflect 来对 target 进行赋值操作,因为它会返回一个 boolean 值代表是否成功,而 set 这个 trap 也需要代表相同含义的值。

通过 reactive 方法来初始化代理对象

实现了数据劫持的代理逻辑之后,我们只需要在 reactive 这个方法中,返回一个代理对象的实例即可,还记的上文中我们在实现之前脑海中浮现的大致代码框架吗?

如下:

export function reactive(obj: any) {
 const proxied = new Proxy(obj, {
  get: function(target, key: string, receiver) {
   if (currentEffect) {
    if (effectMap.has(key)) {
     const effects = effectMap.get(key);
     if (effects.indexOf(currentEffect) === -1) {
      effects.push(currentEffect);
     }
    } else {
     effectMap.set(key, [currentEffect]);
    }
   }

   return Reflect.get(target, key, receiver);
  },
  set: function(target, key: string, value, receiver) {
   const result = Reflect.set(target, key, value, receiver);

   if (effectMap.has(key)) {
    effectMap.get(key).forEach(effect => effect());
   }

   return result;
  }
 });

 return proxied;
}

依赖收集的准备工作

上文中提到了,对于依赖收集的工作,我们是有条件地进行的,即在一个 effect 中,我们才会进行收集,其他情况下的取值逻辑,我们则不会进行依赖收集,因此,effect 方法正式为了实现这点而存在的,如下:

export function effect(fn: Function) {
 const effected = function() {
  fn();
 };

 currentEffect = effected;
 effected();
 currentEffect = undefined;

 return effected;
}

之所以实现如此简单,是因为我们这里是极简版本,不需要考虑诸如 readOnly 、异常以及收集时机等因素。可以发现,就是将传入的回调函数包裹在另一个方法中,然后将这个方法用 currentEffect 这个变量暂存,之后尝试运行一下即可。当 effect 运行完毕之后,再将 currentEffect 置空,这样就可以达到只在 effect 下进行依赖收集的目的。

运行效果

我在 codepen 上简单写了一个计数器 demo,链接如下:
https://codepen.io/littlelyon1/pen/mddVPgo

写在最后

这个极简的响应式系统虽然能用,但是有很多未考虑的因素,其实就是在上文中被我们忽略的那些前提条件,这里再列举一下,并给出源代码中的解法:

  • 基础数据类型的处理:可以将基础数据类型封装为一个 ref 对象,其 value 指向基础数据类型的值
  • 嵌套对象:递归进行执行代理过程即可
  • 集合对象:编写专门的 trap 处理逻辑
  • 代理实例:缓存这些代理实例,下次遇到直接返回即可

但我仍然推荐你直接去阅读一下源码,因为你会发现,源码会在这个极简版本基础上,利用了更加复杂数据结构以及流程,来控制依赖收集和触发响应的流程,同时各种特殊情况也有更加明细的考虑。

另外,这仅仅是 vue-next 响应式系统的简易实现,诸如其他功能模块,比如指令、模板解析、vdom 等,我也准备利用最近的空闲时间再去看看,有时间的话,最近也整理出来,分享给大家。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript版代码高亮
Jun 26 Javascript
制作高质量的JQuery Plugin 插件的方法
Apr 20 Javascript
jquery实现div阴影效果示例代码
Sep 16 Javascript
js使用正则实现ReplaceAll全部替换的方法
Aug 22 Javascript
TypeError document.getElementById(...) is null错误原因
May 18 Javascript
BootStrap Table 分页后重新搜索问题的解决办法
Aug 08 Javascript
Vue.js实现一个SPA登录页面的过程【推荐】
Apr 29 Javascript
Vue2.0 axios前后端登陆拦截器(实例讲解)
Oct 27 Javascript
微信小程序实现tab切换效果
Nov 21 Javascript
js实现导航跟随效果
Nov 17 Javascript
解决vue组件中click事件失效的问题
Nov 09 Javascript
如何手写简易的 Vue Router
Oct 10 Javascript
Vue3.x源码调试的实现方法
Oct 13 #Javascript
使用webpack将ES6转化ES5的实现方法
Oct 13 #Javascript
vue中uni-app 实现小程序登录注册功能
Oct 12 #Javascript
Jquery 动态添加元素并添加点击事件实现过程解析
Oct 12 #jQuery
微信小程序 scroll-view 水平滚动实现过程解析
Oct 12 #Javascript
微信小程序iOS下拉白屏晃动问题解决方案
Oct 12 #Javascript
vue中添加与删除关键字搜索功能
Oct 12 #Javascript
You might like
vs中通过剪切板循环来循环粘贴不同内容
2011/04/30 PHP
Erlang的运算符(比较运算符,数值运算符,移位运算符,逻辑运算符)
2012/07/23 PHP
Yii中Model(模型)的创建及使用方法
2015/12/28 PHP
extjs fckeditor集成代码
2009/05/10 Javascript
jQuery AJAX实现调用页面后台方法和web服务定义的方法分享
2012/03/01 Javascript
js特效,页面下雪的小例子
2013/06/17 Javascript
jQuery实现行文字链接提示效果的方法
2015/03/10 Javascript
纯javascript移动优先的幻灯片效果
2015/11/02 Javascript
AngularJS表单基本操作
2017/01/09 Javascript
Bootstrap表单控件学习使用
2017/03/07 Javascript
vue与vue-i18n结合实现后台数据的多语言切换方法
2018/03/08 Javascript
Vue+Element UI+Lumen实现通用表格分页功能
2019/02/02 Javascript
0基础学习前端开发的一些建议
2020/07/14 Javascript
带你使用webpack快速构建web项目的方法
2020/11/12 Javascript
[01:06:59]完美世界DOTA2联赛PWL S2 Magma vs FTD 第一场 11.29
2020/12/02 DOTA
python批量同步web服务器代码核心程序
2014/09/01 Python
win10系统下Anaconda3安装配置方法图文教程
2018/09/19 Python
对Pandas MultiIndex(多重索引)详解
2018/11/16 Python
Python去除字符串前后空格的几种方法
2019/03/04 Python
详解pandas.DataFrame中删除包涵特定字符串所在的行
2019/04/04 Python
python区分不同数据类型的方法
2019/10/14 Python
python使用openpyxl操作excel的方法步骤
2020/05/28 Python
在pycharm中关掉ipython console/PyDev操作
2020/06/09 Python
Keras 中Leaky ReLU等高级激活函数的用法
2020/07/05 Python
基于HTML5的WebGL实现json和echarts图表展现在同一个界面
2017/10/26 HTML / CSS
html2 canvas生成清晰的图片实现打印功能
2019/09/23 HTML / CSS
什么是组件架构
2016/05/15 面试题
中学老师的自我评价
2013/11/07 职场文书
分公司经理岗位职责
2013/11/11 职场文书
《生命的药方》教学反思
2014/04/08 职场文书
计算机网络专业自荐信
2014/07/04 职场文书
化验员岗位职责
2015/02/14 职场文书
前台接待岗位职责范本
2015/04/03 职场文书
爱国电影观后感
2015/06/19 职场文书
申论不会写怎么办?教您掌握这6点思维和原则
2019/07/17 职场文书
Java使用jmeter进行压力测试
2021/07/09 Java/Android