前端知库

Vue 2 的响应式原理是什么?

Vue 2 在初始化时用 Object.defineProperty 把 data 的属性递归转换为 getter/setter。getter 中由 Dep 收集当前 Watcher,setter 中通知 Watcher 重新执行;组件的渲染 watcher 因此在依赖变化时重新渲染。

参考答案

简答

  • Vue 2 已于 2023-12-31 停止维护,本题以 Vue 2.7 源码为准。
  • 初始化时 Observer 遍历 data,用 defineReactive 通过 Object.defineProperty 把每个属性改成 getter/setter,嵌套对象递归处理。
  • 每个属性闭包里有一个 Dep;getter 执行时,若存在 Dep.target(正在求值的 Watcher),就互相登记依赖。
  • setter 发现新值与旧值不同时调用 dep.notify(),通知所有订阅的 Watcher 更新。
  • 每个组件有一个渲染 watcher,求值函数就是渲染并 patch,它被通知后进入异步队列,统一重新渲染。

三个角色

  • Observer:挂在对象的 __ob__ 上,负责把对象的属性转换为响应式;数组则替换原型并逐项 observe。它自身也带一个 dep,供 Vue.set 和数组方法通知使用。
  • Dep:依赖的容器,subs 保存订阅它的 Watcher。Dep.target 是全局唯一的"当前正在求值的 Watcher",用 pushTarget/popTarget 维护成栈。
  • Watcher:一段需要追踪依赖的求值逻辑。渲染 watcher、computed(lazy watcher)、watch/$watch(user watcher)都是它的实例。

官方文档的描述:Vue 遍历 data 的所有属性并用 Object.defineProperty 转为 getter/setter,每个组件实例有一个对应的 watcher,在渲染时把"接触过"的属性记录为依赖,依赖的 setter 触发后通知 watcher 重新渲染。

依赖收集

Watcher.get() 的流程:

  1. pushTarget(this),把自己设为 Dep.target。
  2. 执行 getter(渲染 watcher 就是 vm._update(vm._render())),途中读到的每个响应式属性都会调用 dep.depend()。
  3. dep.depend() 调用 Dep.target.addDep(dep),Watcher 用 newDepIds 去重后把自己加入 dep.subs。
  4. 求值结束 popTarget(),再 cleanupDeps():本轮没再用到的旧 dep 会把这个 Watcher 移除,所以 v-if 切走的分支不会再触发更新。

属性值是对象或数组时,getter 还会让 childOb.dep.depend(),数组再递归 dependArray。这样 Vue.set 新增属性或调用数组变异方法时,通过 __ob__.dep 也能通知到读过这个对象的 Watcher。

派发更新

setter 中,新值与旧值相同(含 NaN 的情况)直接返回;否则写入新值,对新值重新 observe,然后 dep.notify()。notify 遍历 subs 调用每个 Watcher 的 update():

  • lazy watcher(computed)只把 dirty 设为 true,下次读取时再求值。
  • sync watcher 立即执行。
  • 其他 Watcher 交给 queueWatcher 去重入队,在 nextTick 中按 id 排序后批量执行,所以同一轮里改多次只渲染一次。

示例

JavaScript
// 用 Node 运行,vue@2.7.16
const Vue = require('vue');
const vm = new Vue({ data: { count: 0 } });
vm.$watch('count', (v, old) => console.log(old, '->', v));

vm.count = 1;
vm.count = 2;
// 输出:0 -> 2(同一轮多次修改合并为一次回调)

常见追问