当前位置 主页 > 网站技术 > 代码类 >

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

    栏目:代码类 时间:2019-11-06 09:02

    写在前面

    最新 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
     }