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脚本
Aug 04 Javascript
Javascript 按位左移运算符使用介绍(
Feb 04 Javascript
Javascript aop(面向切面编程)之around(环绕)分析
May 01 Javascript
原生javascript实现分享到朋友圈功能 支持ios和android
May 11 Javascript
使用bat打开多个cmd窗口执行gulp、node
Feb 17 Javascript
微信小程序 跳转传参数与传对象详解及实例代码
Mar 14 Javascript
jQuery插件HighCharts实现的2D堆条状图效果示例【附demo源码下载】
Mar 14 Javascript
jquery图片放大镜效果
Jun 23 jQuery
jQuery实现动态添加和删除input框实例代码
Mar 26 jQuery
jQuery操作元素追加内容示例
Jan 10 jQuery
vscode中Vue别名路径提示的实现
Jul 31 Javascript
three.js 利用uv和ThreeBSP制作一个快递柜功能
Aug 18 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
漫威DC御用漫画家去世 他的表情包曾走红网络
2020/04/09 欧美动漫
php简单获取文件扩展名的方法
2015/03/24 PHP
ThinkPHP+EasyUI之ComboTree中的会计科目树形菜单实现方法
2017/06/09 PHP
JavaScript事件处理器中的event参数使用介绍
2013/05/24 Javascript
jQuery aminate方法定位到页面具体位置
2013/12/26 Javascript
jQuery如何将选中的对象转化为原始的DOM对象
2014/06/09 Javascript
jQuery中的jQuery()方法用法分析
2014/12/27 Javascript
JavaScript中reduce()方法的使用详解
2015/06/09 Javascript
Nodejs获取网络数据并生成Excel表格
2020/03/31 NodeJs
基于JS判断iframe是否加载成功的方法(多种浏览器)
2016/05/13 Javascript
浅谈javascript中的Function和Arguments
2016/08/30 Javascript
js点击按钮实现水波纹效果代码(CSS3和Canves)
2016/09/15 Javascript
jQuey将序列化对象在前台显示地实现代码(方法总结)
2016/12/13 Javascript
javascript基本数据类型和转换
2017/03/17 Javascript
node.js 抓取代理ip实例代码
2017/04/30 Javascript
在vue项目中使用md5加密的方法
2018/09/14 Javascript
谈谈React中的Render Props模式
2018/12/06 Javascript
node全局变量__dirname与__filename的区别
2019/01/14 Javascript
详解Next.js页面渲染的优化方案
2019/01/27 Javascript
Vue配置marked链接添加target=&quot;_blank&quot;的方法
2019/07/19 Javascript
vue+webpack 更换主题N种方案优劣分析
2019/10/28 Javascript
vue proxy 的优势与使用场景实现
2020/06/15 Javascript
javascript实现移动端轮播图
2020/12/09 Javascript
python提取字典key列表的方法
2015/07/11 Python
Python实现读取Properties配置文件的方法
2018/03/29 Python
Django rest framework工具包简单用法示例
2018/07/20 Python
Python迭代器与生成器基本用法分析
2018/07/26 Python
pycharm new project变成灰色的解决方法
2019/06/27 Python
教你使用Canvas处理图片的方法
2017/11/28 HTML / CSS
英语专业个人求职自荐信
2013/09/21 职场文书
小学生获奖感言范文
2014/02/02 职场文书
利群广告词
2014/03/20 职场文书
团日活动总结
2014/04/28 职场文书
银行会计主管岗位职责
2014/10/01 职场文书
中国汉字听写大会观后感
2015/06/02 职场文书
XX部保密工作制度范本
2019/08/27 职场文书