vue-next 响应式原理

更新日期: 2019-07-12 阅读: 2.6k 标签: 原理

写在前面

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

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

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

如有错误,还望指正。


预备知识

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

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

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


接口

对于 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 处理逻辑
  • 代理实例:缓存这些代理实例,下次遇到直接返回即可

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

原文:https://segmentfault.com/a/1190000020660741

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/5855

相关推荐

React Native之原理浅析

讲React Native之前,了解JavaScriptCore会有帮助,也是必要的。React Native的核心驱动力就来自于JS Engine. 你写的所有JS和JSX代码都会被JS Engine来执行, 没有JS Engine的参与,你是无法享受ReactJS给原生应用开发带来的便利的

连v-show都不会你还敢说熟悉 Vue 原理?

Vue 作为最主流的前端框架,中文资料齐全、入门简单、生态活跃,可以说是工作中最常用的,如今对 Vue 原理的熟悉基本上是简历的标配了。之前参与了部分 2019 校园招聘的面试工作,发现很多简历上都写了:

Angular ZoneJS 原理

如果你阅读过关于Angular 2变化检测的资料,那么你很可能听说过zone。Zone是一个从Dart中引入的特性并被Angular 2内部用来判断是否应该触发变化检测

js中flat方法的实现原理

Array.prototype.flat()在Array的显示原型下有一个flat方法,可以将多维数组,降维,传的参数是多少就降多少维,自定义flat的步骤1、第一步是类型判断,需要判断当前调用方法的this是否为一个数组,若不是数组则返回undefined

JavaScript 中的函数式编程原理

做了一些研究,我发现了函数式编程概念,如不变性和纯函数。 这些概念使你能够构建无副作用的功能,而函数式编程的一些优点,也使得系统变得更加容易维护。我将通过 JavaScript 中的大量代码示例向您详细介绍函数式编程和一些重要概念。

理解Vue的Watch原理

watch 是由用户定义的数据监听,当监听的属性发生改变就会触发回调,这项配置在业务中是很常用。在面试时,也是必问知识点,一般会用作和 computed 进行比较。

写一个简单的vue-router来剖析原理

随着前端业务的发展, 我们一般在写一个较为大型的vue项目时候,会使用到vue-router,来根据指定的url或者hash来进行内容的分发,可以达到不像服务端发送请求,就完成页面内容的切换,能够减少像服务器发送的请求

CSS定位之BFC背后的神奇原理

BFC已经是一个耳听熟闻的词语了,网上有许多关于 BFC 的文章,介绍了如何触发 BFC 以及 BFC 的一些用处(如清浮动,防止 margin 重叠等)。BFC直译为\"块级格式化上下文\"。它是一个独立的渲染区域,只有Block-level box参与

前端手写代码原理实现

现在的前端门槛越来越高,不再是只会写写页面那么简单。模块化、自动化、跨端开发等逐渐成为要求,但是这些都需要建立在我们牢固的基础之上。不管框架和模式怎么变,把基础原理打牢才能快速适应市场的变化。下面介绍一些常用的源码实现

关于vue过滤器的原理解析

Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的尾部

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!