Part4
Vue2 的响应式原理
简述
首先 Object.defineProperty()
,将每个数据转成对象并提供 getter
setter
函数。
当 render
函数运行时,会调用其 getter
方法,而 getter
方法会记录函数的调用,也就是 依赖收集。
这里用的是发布订阅模式,watcher
会观察到调用,而当数据更新时调用 setter
则会派发更新,通知 watcher
数据发生变化,就会重新触发 render
函数生成虚拟 dom 树。
响应式就是想达到对象本身或属性发生变化时运行一些函数。-> 数据响应式的终极目标
Observer:
每个属性获得了 getter, setter,它发生于 beforeCreate
之后,created
之前
不能直接调用,但可以使用 Vue.obserable()
达到相似效果,在浏览器中响应式数据显示为 {...}
但是这里我们不能监测以后要添加的属性(在 Vue3 使用了 ES6 的 proxy 才能解决这个问题),因此 Vue 提供了 $set()
$delete()
来编辑属性,这样添加删除属性时候 Vue 可以“收到通知”
而对于数组,Vue 把数组的隐式原型改成了自己创建的一个对象,提供了一些方法,这样可以在修改时候通知 Vue,而为了让这个数组有原生数组的方法,它自己构建的对象的隐式原型又设置为了 Array.prototype。
关于数组:
其实这里就是形成了一个原型链:
数组-> Vue 自定义的对象-> Array.prototype
但是这里监测不到数组属性变化,比如 arr[0] = 100
,这里得用 $set
,但是数组里面的对象还是响应式的。
Dep:
Dep(Dependency)解决的是读属性和修改属性的时候要做什么。
模板中用到什么属性,什么属性就进入依赖(仅是渲染时用到的),这里如果用了上级依赖也会变化
记录依赖 dep.depend()
,派发更新 dep.notify()
Watcher
解决的是知道哪个函数在调用。
这是一个观察者,其实调用的 this.$watch()
本质上就是创建 Watcher,通过一个类似全局变量进行收集。
currentWatcher = this;
render(); // get(){ dep.depend() }
currentWatcher = null;
每个 Vue 实例至少对应一个 watcher(在组件的 _watcher
上),记录了这个组件的 render 函数。
watcher 函数首先会运行一次 render 以收集依赖,这样响应式数据就会记录这个 watcher,随后,当数据产生更新的时候,dep 就会通知这个 watcher 重新运行 render 函数。
Scheduler:
用来调度函数执行,维护了一个执行队列
关于 Vue 和 React 的 Scheduler:
Vue 和 React 中的 Scheduler(调度器)在概念上有相似之处(都是为了提高渲染性能而设计的任务调度机制),但它们的实现方式、设计目标和具体行为有显著区别,并不是同一个东西。以下是两者的主要差异:
设计目标不同
- Vue 的 Scheduler
Vue 的调度器核心目标是实现 响应式更新的批量处理 和 异步更新队列。
- 当一个响应式数据变化时,Vue 不会立即更新 DOM,而是将相关的组件更新推入一个队列中,并在下一个事件循环中批量执行这些更新。
- 通过这种方式避免不必要的重复渲染(例如,同一事件循环中多次修改数据,只会触发一次渲染)。
- 使用微任务(Microtask,如
Promise.then
)或宏任务(Macrotask,如setTimeout
)来延迟更新,具体取决于环境。
- React 的 Scheduler
React 的调度器(尤其是自 React 16 引入的并发模式后)更复杂,目标是实现 任务优先级调度 和 时间切片(Time Slicing)。
- 将渲染任务拆分为多个小任务,并根据优先级(如用户交互、动画、数据加载等)动态调度它们的执行顺序。
- 通过
requestIdleCallback
(或 polyfill)在浏览器空闲时间执行低优先级任务,确保高优先级任务(如用户输入)不被阻塞。 - 支持任务的中断与恢复,这是 React 并发模式的核心机制。
实现机制不同
- Vue 的调度器
- 批量更新:通过
queueWatcher
方法将需要更新的组件 Watcher 推入队列,并在nextTick
中一次性执行。 - 微任务优先:默认使用
Promise.then
(微任务)延迟执行队列,确保更新在同步代码执行完毕后、浏览器渲染之前完成。 - 简单高效:设计更简单,适合 Vue 的自动依赖追踪和“细粒度更新”模型。
- 批量更新:通过
- React 的调度器
- 优先级模型:任务被分为多个优先级(如
Immediate
、UserBlocking
、Normal
、Low
、Idle
),并通过小顶堆(Min Heap)管理任务队列。 - 时间切片:将长任务拆分为多个 5ms 的块,在每一帧的空闲时间执行,避免阻塞主线程。
- 中断与恢复:如果某个任务执行时间过长,React 可以暂停它,先执行更高优先级的任务,之后再恢复。
- 优先级模型:任务被分为多个优先级(如
API 和暴露程度
- Vue
- Vue 的调度器对开发者是隐式的,通过
this.$nextTick
或全局的Vue.nextTick
暴露,开发者通常无需直接操作调度逻辑。 - 调度行为由框架内部管理,开发者只需关注数据变化。
- Vue 的调度器对开发者是隐式的,通过
- React
- React 的调度器更底层,虽然不直接暴露给普通开发者,但通过
useTransition
、useDeferredValue
等 API 间接提供优先级控制能力。 - 在并发模式下,开发者可以显式指定某些任务的优先级(如标记为非紧急更新)。
- React 的调度器更底层,虽然不直接暴露给普通开发者,但通过
适用场景
- Vue 适合需要简单、自动化的更新批处理场景,通过微任务优化高频数据变更的性能(例如表单输入、实时数据流)。
- React 适合复杂交互应用,需要细粒度控制渲染优先级(如保持动画流畅的同时加载数据),并通过时间切片避免主线程阻塞。
首先是 watcher 去重,每个仅会存在一次,然后通过 nextTick
方法,将其放在时间循环微队列。
当响应式数据变化时,render
函数的执行是异步的。