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 相关文章推荐
prototype 1.5 &amp; scriptaculous 1.6.1 学习笔记
Sep 07 Javascript
Mootools 1.2教程 排序类和方法简介
Sep 15 Javascript
SlideView 图片滑动(扩展/收缩)展示效果
Aug 01 Javascript
jquery png 透明解决方案(推荐)
Aug 21 Javascript
JavaScript 验证码的实例代码(附效果图)
Mar 22 Javascript
css配合jquery美化 select
Nov 29 Javascript
jQuery中:disabled选择器用法实例
Jan 04 Javascript
javascript判断网页是关闭还是刷新
Sep 12 Javascript
浅谈javascript中关于日期和时间的基础知识
Jul 13 Javascript
vue-cli的webpack模板项目配置文件分析
Apr 01 Javascript
layui表格数据复选框回显设置方法
Sep 13 Javascript
vue如何使用外部特殊字体的操作
Jul 30 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
PHP实现对站点内容外部链接的过滤方法
2014/09/10 PHP
javascript 网页跳转的方法
2008/12/24 Javascript
从零开始学习jQuery (十) jQueryUI常用功能实战
2011/02/23 Javascript
js时间日期和毫秒的相互转换
2013/02/22 Javascript
JQuery异步获取返回值中文乱码的解决方法
2015/01/29 Javascript
JavaScript实现自动消除按钮功能的方法
2015/08/05 Javascript
不用一句js代码初始化组件
2016/01/27 Javascript
基于jQuery实现歌词滚动版音乐播放器的代码
2016/09/17 Javascript
Windows环境下npm install 报错: operation not permitted, rename的解决方法
2016/09/26 Javascript
JavaScript数组操作详解
2017/02/04 Javascript
为Jquery EasyUI 组件加上清除功能的方法(详解)
2017/04/13 jQuery
详解Angular4 路由设置相关
2017/08/26 Javascript
ES6解构赋值实例详解
2017/10/31 Javascript
vue.js使用代理和使用Nginx来解决跨域的问题
2018/02/03 Javascript
详解Express笔记之动态渲染HTML(新手入坑)
2018/12/13 Javascript
使用weixin-java-miniapp配置进行单个小程序的配置详解
2019/03/29 Javascript
Node.js一行代码实现静态文件服务器的方法步骤
2019/05/07 Javascript
[52:26]完美世界DOTA2联赛决赛 FTD vs Phoenix 第一场 11.08
2020/11/11 DOTA
Python中使用select模块实现非阻塞的IO
2015/02/03 Python
python实现简单温度转换的方法
2015/03/13 Python
多版本Python共存的配置方法
2017/05/22 Python
python队列通信:rabbitMQ的使用(实例讲解)
2017/12/22 Python
TensorFlow实现Batch Normalization
2018/03/08 Python
Python list列表中删除多个重复元素操作示例
2019/02/27 Python
Python如何筛选序列中的元素的方法实现
2019/07/15 Python
python实现邮件发送功能
2019/08/10 Python
Python代码块及缓存机制原理详解
2019/12/13 Python
django Layui界面点击弹出对话框并请求逻辑生成分页的动态表格实例
2020/05/12 Python
PIL.Image.open和cv2.imread的比较与相互转换的方法
2020/06/03 Python
Selenium alert 弹窗处理的示例代码
2020/08/06 Python
Django静态文件加载失败解决方案
2020/08/26 Python
Python使用grequests并发发送请求的示例
2020/11/05 Python
四年的大学生生活自我评价
2013/12/09 职场文书
2015年上半年党建工作总结
2015/03/30 职场文书
导游词之凤凰古城
2019/10/22 职场文书
腾讯云服务器部署前后分离项目之前端部署
2022/06/28 Servers