Zoom's Blog

Vuejs Reactivity 源码分析

Reactivity (Vue 中文文档中翻译为响应式) 是 Vue 的非常关键的一部分。通过 Model 与视图的双向绑定,可以让使用者通过操作 Model 直接操作页面,不再需要直接操作 DOM。

除了 Reactivity,Vue 中还有很多其他的内容,包括:模版解析、VDOM、Components 逻辑等。
为了更好的学习 Vue 中 Reactivity 的实现,将 Vue.js 中 Reactivity 相关部分拿出来成为了单独的部分。这样更容易单独的理解这一部分的实现。
相关的代码在 vue-observer

运行:

1
2
npm install;
npm run dev;

访问 example/base 下的 index.html,并修改源码,了解其中的实现。

Reactivity 监控变化的处理流程参考 Vue 文档中给出的图片:

vue-reactivity

下面从文件功能和处理流程两个方面,介绍 Vue 中 Reactivity 的具体实现。

文件功能介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.
├── instance 初始化调用相关
│   ├── index.js
│   ├── init.js
│   ├── render.js `render` 方法(其实是 `new Watcher()`)。
│   └── state.js 处理 Vue 中初始化过程中的 `props`,`data`,`compute`,`watch`
├── observer Reactivity 的具体实现
│   ├── index.js 包含类 `Observer`,主要是为各个需要观察的目标埋点 (`new Dep()`)
│   ├── array.js 辅助方法,用于重写 Array 的一些方法,使 Array 的变化可以被观察。
│   ├── dep.js 包含类 `Dep` (观察点 类)
│   ├── scheduler.js 观察者的调度逻辑,主要负责处理,多个 Watcher 需要被调用的情况。防止多次被调用以及在特定情况下按顺序调用。
│   └── watcher.js 包含类 `Watcher` (观察者 类)
├── shared
│   └── util.js
└── util
├── debug.js
├── env.js
├── index.js
├── lang.js
└── props.js

为了减少其他功能的影响,vue-observer 示例中将 Vue 的 render 方法作了精简,并将 render 方法的调用(其实是 new Watcher())从 lifecycle 移到了 render.js 文件中。

其中的两个重要的类,属性方法如下:

Dep 类

属性

  • target 静态属性,全局唯一,标识当前正在处理的 Watcher
  • id ID
  • subs Watcher 的列表

方法

  • addSub 添加 Watcher
  • removeSub 移除 Watcher
  • depend 为自己添加当前 target Watcher,并更新 Watcher 的观察列表
  • notify 通知所有 Watcher 更新

Watcher

根据传入的 expression,得到 value,传给 cb 进行处理。

属性

  • deep 用于决定是否完整的轮训,来保证每一个属性都被重新计算依赖
  • user 是否是用户创建的,是的话会尝试捕获 cb 调用时产生的异常,并提醒
  • lazy 是否 new 之后不立即执行
  • sync 是否需要在每次需要 update 的时候立即执行 (不设置的话会交由 scheduler 处理)
  • expression 传入的方法或表达式
  • cb 执行之后的回调函数
  • id
  • active 是否当前激活状态
  • dirty 是否是脏数据
  • deps 与下面的三个一起,用来做 观察目标 (Dep)的动态更新
  • newDeps
  • depIds
  • newDepIds
  • getter 从 expression 来的 getter 方法
  • value 当前的取值

方法

  • get 默认初始化之后调用,调用传入的 expression,收集依赖,并调用 cleanupDeps 更新依赖关系
  • addDep 把自身添加到制定 Dep 的观察列表中,另外如果依赖有变化则记录
  • cleanupDeps 更新依赖关系
  • update 更新
  • run 具体的执行方法,负责调用 get 然后根据情况决定是否调用 cb
  • evaluate 只是调用 get,用于清除 dirty 状态
  • depend 把收集的 deps 统一进行处理,跟 evaluate 一样,都是为 lazy 的 Watcher 服务的
  • teardown 销毁

util + share 一些工具代码

一些工具方法

Reactivity 相关处理流程

大概了解了各个文件的功能之后,我们从处理流程的角度,了解下 Vue 中 Reactivity 的具体处理过程。

Reactivity 的处理包含三个部分:

  • 初始化观察目标,负责埋点
  • 初始化 Watcher 并进行依赖收集,等待变更出现
  • 出现变更,进行更新

初始化观察目标

初始化的时候(在 Vue 中 new Vue 或者创建 Component),根据传入的 $options 进行初始化处理。

  • 初始化 $options.props
  • 初始化 $options.data

    通过调用 observer/index.js 文件中的 observer 方法,进行埋点。

    • 每个对象(Object 和 Array)都会被分配个观察者 Observer

      主要是用于对象变更的观察,例如 Array 的 push 操作;另外一个使用的地方是 Vue.set ,可以在初始化之后依然提供观察的能力。
      这里有一个很厉害的细节,Observer 对象被放到了 Object 的 __ob__ 属性中,既可以在拿到对象的时候都可以回去到对应的 Observer,还起到缓存的作用,防止多次观察同一对象,一举两得。

    • 每个属性也会被分配个观察者

    这里的观察者通过 Object.defineProperty 来处理 getter 和 setter。在 get 的时候,处理观察关系,将当前 Watcher 加入到属性的观察者列表中。set 的时候,通过 dep.notify() 触发观察者进行更新。

    这里也有个 childOb 的概念,如果当前的属性存在 childOb,那么 childOb 也要同时被当前 Watcher 观察。

初始化 Watcher 并进行依赖收集,等待变更出现

初始化 Watcher 分为三个部分:

  • 初始化 $options.computed 的 Watcher

    Vue 中可以通过给 $options 传入 computed 对象来处理一些比较复杂的显示逻辑。这个时候就要根据所依赖的属性,进行观察和重新计算。computed 对应的 Watcher 通过 makeComputedGetter 方法创建,他的特点是,创建的为 Lazy Watcher,new 之后不会立即调用,在 computed 的属性被 get 的时候才需要重新计算。

    对于这类 Watcher,处理的流程为

    1. 初始化的时候,只设置 dirty 为 true,不直接执行传入的 expression。
    2. 每次 Dep 通过 update 的时候只设置 Watcher 的 dirty 为 true,标识需要重新计算。
    3. 在需要获取最新值得时候(这里就是 computed 属性被 get 的时候)调用 watcher.evaluate() 方法,计算 watcher 的最新值,并且手动调用 watcher.depend() 方法来重新计算依赖。
  • 初始化 $options.watch 的 Watcher

    $options.watch 提供观察 data 的功能,在 data 被修改之后,触发提供的回调函数。

    具体的实现过程中,使用了 Vue 暴露出的全局 API:Vue.$watch,创建一个普通的 watcher,在指定的 expression 的发生改变的时候触发 cb。

  • 初始化 render Watcher

    在 Vue 的处理流程中,会把 temple 编译为 render 函数(具体的实现会更加复杂),在 Vue Component 初始化完成的时候,会创建一个 Watcher(render, patchVDom),根据传入的 render 函数创建依赖关系,如果发现变化,则 PatchVDom, 实现 View 的更新。

出现变更,进行更新

经过 Watcher 的初始化,Vue 中的 Watcher,分为三类(不包括用户调用 Vue.$watch 传入 sync 参数的情况)

  • Computed Watcher(lazy Watcher),update 的时候只标记 dirty 状态,不会进入 queue(后面具体讲队列),属性被 get 的时候直接调用 watcher.evaluate() 进行取值。
  • 普通 Watcher,包含用户通过 $options.watchVue.$watch 生成的 watch,在 update 的时候会进入 queue
  • Component Watcher,render 方法作为 expOrFn 的 watcher,实现 Model -> DOM 的关键 Watcher,在 update 的时候会进入 queue

在 Model 被改变的时候,对应的 dep 被触发 notify 方法。dep 会调用所有 watcher 的 update 方法。

  • Computed Watcher 的处理逻辑在 初始化 $options.computed 的 Watcher 部分已经做了描述
  • 其他的 watcher (不包括设置 sync 为 true,下同)都会交由 queueWatcher 进行处理

想过逻辑存在于文件 observer/scheduler.js

queueWatcher 的作用主要有以下几点:

  1. 同一个 watcher 只会被添加到列表中一次,防止被多次调用
  2. 如果正在执行队列,则会判断当前插入的 watcher id,如果已经过了,则立即执行,否则插入到合适位置
  3. 当前处理的所有 watcher 被插入到队列之后进行执行

执行队列的时候,会先对需要执行的 watcher 进行排序处理:

  1. 大的方面,从父组件到子组件的方向执行,因为父组件总是先被创建的。
  2. 用户定义的 watcher 先于 render watcher 被处理(用户定义的 watcher 可能会影响 Model 的取值)
  3. 如果子组件被父组件干掉了,那么它会被跳过。

这个顺序也是 watcher 被创建的顺序,这样的话,按照 watcher 的 id 从小到大进行排列就可以了。

然后执行 排重 + 排序 之后的 watcher 队列,这样就保证了每次数据变更,只会按顺序处理一次需要更新的 watcher


可以看到,这部分的整体的实现真的是非常的好。

其中 Dep 和 Watcher 这两个类的设计非常优秀。在 Watcher 中存储了 Dep 的列表,也在 Dep 中存储了 Watcher 的列表。通过这样的设计,既可以在 Watcher 实例中,初始化及更新依赖关系(通过 Deps 和 newDeps),也可以在 dep 中直接通知所有的 watcher 进行更新。

scheduler 的引入很好的解决了什么时候调用 watcher 的问题,最大化的减少了 DOM 操作,再加上 VDOM,就完成了 Model 到 View 的高性能的映射。