一、示例组件
以下面这个简单的 Vue 组件为例,分析其在渲染过程中响应式机制的建立:
vue
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
import { ref } from "vue";
const count = ref(0);
const increment = () => {
count.value++;
};
</script>
<style lang="scss" scoped>
</style>
二、响应式机制建立流程
在深入分析具体流程之前,我们需要理解 Vue 的响应式机制本质上是一个发布订阅模式:
- 订阅者(Subscriber) : ReactiveEffect 实例,即包装了组件更新函数的响应式效果
- 发布者(Publisher) : 依赖集合(deps),存储了所有订阅该数据变化的效果
- 订阅过程: 当 effect 执行时(运行组件的渲染函数render或者用户定义的副作用函数),会访问响应式数据,触发代理的 get 拦截器,此时 effect 就订阅了该数据的 deps
- 发布过程: 当数据变化时,触发代理的 set 拦截器,找到对应的 deps,deps 通知所有订阅的 effect 执行更新
这个发布订阅模式通过以下步骤建立:
- 创建响应式代理,使数据可以被监听
- 创建 effect 封装更新函数
- 执行 effect 时自动完成订阅
- 数据变化时通过代理触发发布
- 创建更新任务(job)并加入调度队列
让我们详细看看这个过程是如何实现的:
1. 组件初始化与渲染入口
当执行 app.mount('#app')
时,渲染器开始工作:
js
// 源码位置: packages/runtime-core/src/renderer.ts
// 大约在 1248 行附近的 baseCreateRenderer 函数内
const render: RootRenderFunction = (vnode, container, namespace) => {
patch(
container._vnode || null,
vnode,
container,
null,
null,
null,
namespace
);
container._vnode = vnode;
};
2. 组件实例创建与响应式数据初始化
js
// 组件挂载过程调用链:
// packages/runtime-core/src/renderer.ts -> patch -> processComponent -> mountComponent -> setupComponent
// 1. 创建组件实例
// 源码位置: packages/runtime-core/src/component.ts 中的 createComponentInstance 函数
const instance = createComponentInstance(vnode, parent);
// 2. 初始化 props (变为 shallowReactive)
// 源码位置: packages/runtime-core/src/componentProps.ts 中的 initProps 函数
instance.props = shallowReactive({
title: "Hello Vue",
});
// 3. 执行 setup 函数,创建响应式状态
// 源码位置: packages/runtime-core/src/component.ts 中的 setupStatefulComponent 函数
const setupResult = setupStatefulComponent(instance);
这里的 props 初始化过程实际上是响应式系统建立的重要一环。让我们详细分析这个过程:
js
// 源码位置: packages/reactivity/src/reactive.ts
export function shallowReactive<T extends object>(
target: T
): ShallowReactive<T> {
return createReactiveObject(
target,
false,
shallowReactiveHandlers,
shallowCollectionHandlers,
shallowReactiveMap
);
}
// 创建响应式对象的核心过程
function createReactiveObject(
target,
isReadonly,
baseHandlers,
collectionHandlers,
proxyMap
) {
// 1. 创建代理对象
const proxy = new Proxy(
target,
baseHandlers // 这里使用 shallowReactiveHandlers
);
// 2. 存入响应式对象 Map
proxyMap.set(target, proxy);
return proxy;
}
baseHandlers 实现(简化版):
js
// 源码位置: packages/reactivity/src/baseHandlers.ts
class BaseReactiveHandler implements ProxyHandler<Target> {
constructor(
protected readonly _isReadonly = false,
protected readonly _isShallow = false
) {}
// 属性读取时收集依赖
get(target: Target, key: string | symbol, receiver: object) {
// ... 一些特殊 key 的处理 ...
const res = Reflect.get(target, key, receiver);
// 追踪依赖
if (!isReadonly) {
track(target, TrackOpTypes.GET, key);
}
// shallow 模式下不递归处理
if (isShallow) {
return res;
}
return res;
}
// 属性设置时触发更新
set(
target: Target,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
// 如果值发生变化,触发更新
if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue);
}
return result;
}
}
// shallowReactive 使用的处理器
const shallowReactiveHandlers = new MutableReactiveHandler(
true /* isShallow */
);
这个过程中的关键点:
-
响应式代理准备:
- 使用 Proxy 对 props 对象进行代理
- 通过 shallowReactive 只代理对象的第一层属性
- 为 title 属性设置 get/set 拦截器
- 将代理对象保存到 Map 中以便复用
-
get 拦截器的作用:
- 当后续渲染过程中访问
this.title
时会触发 get - get 触发时会调用 track 收集当前正在执行的 effect 作为依赖
- 这样在 title 更新时就知道需要通知哪些 effect 重新执行
- 当后续渲染过程中访问
-
set 拦截器的作用:
- 当父组件修改 title 属性时会触发 set
- set 触发时会调用 trigger 通知之前收集的依赖进行更新
- 确保值变化时可以触发组件的重新渲染
-
为后续阶段做准备:
- 这个代理过程是为了后续的依赖收集做准备
- 在组件渲染时会访问 props.title 从而收集渲染 effect
- 当 title 变化时就能触发这个渲染 effect 进行更新
这样,props 的响应式代理就为整个组件的响应式更新机制打下了基础:
- 通过 get 收集谁用到了这个属性
- 通过 set 在属性变化时通知依赖更新
- 与后续的 effect 和调度系统配合,形成完整的响应式更新链路
3. 建立渲染函数的响应式包装
js
// 源码位置: packages/runtime-core/src/renderer.ts
// 大约在 setupRenderEffect 函数内(约 2500 行附近)
// setupRenderEffect 调用链:
// packages/runtime-core/src/renderer.ts -> patch -> processComponent -> mountComponent -> setupRenderEffect
// setupRenderEffect 中创建渲染的响应式效果
// 1. 开启作用域收集
instance.scope.on();
// 2. 创建响应式效果
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn // 组件的更新函数
));
instance.scope.off();
// 3. 创建更新函数和任务
const update = (instance.update = effect.run.bind(effect));
const job: SchedulerJob = (instance.job = effect.runIfDirty.bind(effect));
job.i = instance;
job.id = instance.uid;
// 4. 设置调度器
effect.scheduler = () => queueJob(job);
// componentUpdateFn 的具体实现
function componentUpdateFn() {
if (!instance.isMounted) {
// 首次挂载
const subTree = (instance.subTree = renderComponentRoot(instance));
patch(null, subTree, container, anchor, instance, parentSuspense);
instance.isMounted = true;
} else {
// 更新流程...
}
}
这段代码展示了 Vue 如何建立组件的响应式更新机制:
-
作用域控制:
instance.scope.on()
开启作用域收集instance.scope.off()
关闭作用域收集- 这确保了副作用的收集范围限定在组件内
-
响应式效果创建:
js
// 源码位置: packages/reactivity/src/effect.ts
export class ReactiveEffect<T = any>
implements Subscriber, ReactiveEffectOptions
{
// 依赖链表的头部
deps?: Link = undefined;
// 依赖链表的尾部
depsTail?: Link = undefined;
// 效果的状态标志位
flags: EffectFlags = EffectFlags.ACTIVE | EffectFlags.TRACKING;
// 构造函数,接收要执行的副作用函数
constructor(public fn: () => T) {}
// 运行效果,执行副作用函数并收集依赖
run(): T {
// 如果效果已停止,直接执行函数不收集依赖
if (!(this.flags & EffectFlags.ACTIVE)) {
return this.fn();
}
// 设置运行标记
this.flags |= EffectFlags.RUNNING;
// 清理旧依赖并准备新的依赖收集
cleanupEffect(this);
prepareDeps(this);
// 保存当前上下文
const prevEffect = activeSub;
const prevShouldTrack = shouldTrack;
activeSub = this;
shouldTrack = true;
try {
// 执行副作用函数,这个过程中会重新收集依赖
return this.fn();
} finally {
// 恢复上下文并清理依赖
cleanupDeps(this);
activeSub = prevEffect;
shouldTrack = prevShouldTrack;
// 清除运行标记
this.flags &= ~EffectFlags.RUNNING;
}
}
// 触发更新,根据不同状态决定如何执行更新
trigger(): void {
if (this.flags & EffectFlags.PAUSED) {
// 如果被暂停,加入暂停队列
pausedQueueEffects.add(this);
} else if (this.scheduler) {
// 有调度器则使用调度器执行
this.scheduler();
} else {
// 否则直接检查并运行
this.runIfDirty();
}
}
// 条件运行,只在"脏"的状态下才执行更新
runIfDirty(): void {
if (isDirty(this)) {
this.run();
}
}
// 其他函数...
}
当创建响应式效果时:
- 通过
new ReactiveEffect(componentUpdateFn)
创建实例 - 设置调度器
effect.scheduler = () => queueJob(job)
- 生成
update
和job
函数:
js
const update = effect.run.bind(effect); // 直接运行更新
const job = effect.runIfDirty.bind(effect); // 条件运行更新
这个结构设计的优点:
- 通过依赖链表(deps)高效管理依赖关系
- 使用标志位(flags)控制效果的状态
- 支持可配置的调度器实现更新队列
4. 依赖收集过程
当执行渲染函数时,会访问响应式数据,触发依赖收集:
js
// 源码位置: packages/reactivity/src/effect.ts 中的 trackEffects 函数
// 和 packages/reactivity/src/ref.ts 中的 RefImpl 类
// 1. 模板中访问数据,触发代理
_ctx.count; // 通过 proxy 访问 ref
_ctx.title; // 访问 props
// 2. 依赖收集的数据结构
// 源码位置: packages/reactivity/src/dep.ts
type Dep = Set<ReactiveEffect>;
type KeyToDepMap = Map<any, Dep>;
// 3. 依赖收集过程
count.dep.add(activeEffect); // activeEffect 就是组件的 update 函数
props.__dep__.add(activeEffect);
5. 更新任务的创建与调度
js
// 源码位置: packages/runtime-core/src/scheduler.ts
// 1. 更新任务的数据结构
type SchedulerJob = Function & {
id?: number;
active?: boolean;
computed?: boolean;
allowRecurse?: boolean;
ownerInstance?: ComponentInternalInstance;
};
// 2. 任务队列
const queue: SchedulerJob[] = [];
// 3. 当数据变化时创建更新任务
function queueJob(job: SchedulerJob) {
if (!queue.includes(job)) {
queue.push(job);
queueFlush();
}
}
三、响应式机制运作示例
以点击按钮更新计数为例:
js
// 1. 点击按钮,触发 increment 函数
increment() {
count.value++ // 触发 ref 的 set
}
// 2. ref 的 set 触发更新
set value(newVal) {
if (newVal !== this._value) {
this._value = newVal
trigger(this.dep) // 触发依赖
}
}
// 3. 触发依赖,创建更新任务
trigger(dep) {
const effects = new Set(dep)
for (const effect of effects) {
if (effect.scheduler) {
effect.scheduler() // 调用 queueJob
}
}
}
// 4. 将更新任务加入队列
queueJob(instance.update) // instance.update 是之前创建的 effect
// 5. 在下一个微任务中执行更新
nextTick(() => {
flushJobs() // 执行所有更新任务
})
四、总结
通过这个具体示例,我们可以看到 Vue 渲染过程中响应式机制的建立是一个完整的链路:
- 组件初始化时创建响应式数据
- 渲染过程中通过 effect 建立响应式包装
- 渲染函数执行过程中进行依赖收集
- 数据变化时触发更新,创建更新任务
- 调度系统统一处理更新任务
整个过程中涉及的数据结构(effect、dep、job、queue 等)都是为了服务于这个响应式更新流程,确保数据变化能够精确且高效地触发组件更新。后续我们就来分析依赖收集系统的具体实现。