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 的调度器
    • 优先级模型:任务被分为多个优先级(如 ImmediateUserBlockingNormalLowIdle),并通过小顶堆(Min Heap)管理任务队列。
    • 时间切片:将长任务拆分为多个 5ms 的块,在每一帧的空闲时间执行,避免阻塞主线程。
    • 中断与恢复:如果某个任务执行时间过长,React 可以暂停它,先执行更高优先级的任务,之后再恢复。

API 和暴露程度

  • Vue
    • Vue 的调度器对开发者是隐式的,通过 this.$nextTick 或全局的 Vue.nextTick 暴露,开发者通常无需直接操作调度逻辑。
    • 调度行为由框架内部管理,开发者只需关注数据变化。
  • React
    • React 的调度器更底层,虽然不直接暴露给普通开发者,但通过 useTransitionuseDeferredValue 等 API 间接提供优先级控制能力。
    • 在并发模式下,开发者可以显式指定某些任务的优先级(如标记为非紧急更新)。

适用场景

  • Vue 适合需要简单、自动化的更新批处理场景,通过微任务优化高频数据变更的性能(例如表单输入、实时数据流)。
  • React 适合复杂交互应用,需要细粒度控制渲染优先级(如保持动画流畅的同时加载数据),并通过时间切片避免主线程阻塞。

首先是 watcher 去重,每个仅会存在一次,然后通过 nextTick 方法,将其放在时间循环微队列。

当响应式数据变化时,render 函数的执行是异步的。