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 相关文章推荐
利用JQuery的load函数动态加载其它页面的内容的实现代码
Dec 14 Javascript
javascript nextSibling 与 getNextElement(node) 使用介绍
Oct 13 Javascript
扩展JavaScript功能的正确方法(译文)
Apr 12 Javascript
谈谈encodeURI和encodeURIComponent以及escape的区别与应用
Nov 24 Javascript
你有必要知道的25个JavaScript面试题
Dec 29 Javascript
JS中对象与字符串的互相转换详解
May 20 Javascript
JS实现课堂随机点名和顺序点名
Mar 09 Javascript
详解VUE自定义组件中用.sync修饰符与v-model的区别
Jun 26 Javascript
微信小程序缓存过期时间的使用详情
May 12 Javascript
express + jwt + postMan验证实现持久化登录
Jun 05 Javascript
Vue Router的手写实现方法实现
Mar 02 Javascript
vue动态绑定style样式
Apr 20 Vue.js
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的引用原因分析
2012/09/06 PHP
php中将一段数据存到一个txt文件中并显示其内容
2014/08/15 PHP
WordPress中编写自定义存储字段的相关PHP函数解析
2015/12/25 PHP
php array_values 返回数组的值实例详解
2016/11/17 PHP
PHP程序员简单的开展服务治理架构操作详解(一)
2020/05/14 PHP
JavaScript高级程序设计 读书笔记之十 本地对象Date日期
2012/02/27 Javascript
ExtJS 刷新后如何默认选中刷新前最后一次选中的节点
2014/04/03 Javascript
AngularJS基础知识笔记之过滤器
2015/05/10 Javascript
js制作带有遮罩弹出层实现登录注册表单特效代码分享
2015/09/05 Javascript
简单谈谈javascript中this的隐式绑定
2016/02/22 Javascript
Node.js编写组件的三种实现方式
2016/02/25 Javascript
基于node实现websocket协议
2016/04/25 Javascript
Bootstrap每天必学之标签页(Tab)插件
2020/08/09 Javascript
AngularJS包括详解及示例代码
2016/08/17 Javascript
纯JS实现可拖拽表单的简单实例
2016/09/02 Javascript
JavaScript实现获取远程的html到当前页面中
2017/03/26 Javascript
win系统下nodejs环境安装配置
2017/05/04 NodeJs
详解50行代码,Node爬虫练手项目
2019/04/22 Javascript
详解小程序退出页面时清除定时器
2019/04/28 Javascript
手把手教你使用TypeScript开发Node.js应用
2019/05/06 Javascript
JS异步宏队列与微队列原理区别详解
2020/07/02 Javascript
基于VUE实现简单的学生信息管理系统
2021/01/13 Vue.js
Python中实现对list做减法操作介绍
2015/01/09 Python
python使用nntp读取新闻组内容的方法
2015/05/08 Python
Python中的pass语句使用方法讲解
2015/05/14 Python
Python中的with语句与上下文管理器学习总结
2016/06/28 Python
Python语言的变量认识及操作方法
2018/02/11 Python
Python多线程正确用法实例解析
2020/05/30 Python
python 使用递归的方式实现语义图片分割功能
2020/07/16 Python
湖南卫视在线视频媒体平台:芒果TV
2019/10/30 全球购物
3.15国际消费者权益日主题活动活动总结
2014/03/16 职场文书
廉洁自律演讲稿
2014/05/22 职场文书
员工薪酬激励方案
2014/06/13 职场文书
县政府办公室领导班子对照检查材料思想汇报
2014/09/28 职场文书
2016年大学迎新晚会工作总结
2015/10/15 职场文书
vscode远程免密登入Linux服务器的配置方法
2022/06/28 Servers