vue3源码解析:响应式机制

一、示例组件

以下面这个简单的 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 执行更新

这个发布订阅模式通过以下步骤建立:

  1. 创建响应式代理,使数据可以被监听
  2. 创建 effect 封装更新函数
  3. 执行 effect 时自动完成订阅
  4. 数据变化时通过代理触发发布
  5. 创建更新任务(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 */
);

这个过程中的关键点:

  1. 响应式代理准备

    • 使用 Proxy 对 props 对象进行代理
    • 通过 shallowReactive 只代理对象的第一层属性
    • 为 title 属性设置 get/set 拦截器
    • 将代理对象保存到 Map 中以便复用
  2. get 拦截器的作用

    • 当后续渲染过程中访问 this.title 时会触发 get
    • get 触发时会调用 track 收集当前正在执行的 effect 作为依赖
    • 这样在 title 更新时就知道需要通知哪些 effect 重新执行
  3. set 拦截器的作用

    • 当父组件修改 title 属性时会触发 set
    • set 触发时会调用 trigger 通知之前收集的依赖进行更新
    • 确保值变化时可以触发组件的重新渲染
  4. 为后续阶段做准备

    • 这个代理过程是为了后续的依赖收集做准备
    • 在组件渲染时会访问 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 如何建立组件的响应式更新机制:

  1. 作用域控制

    • instance.scope.on() 开启作用域收集
    • instance.scope.off() 关闭作用域收集
    • 这确保了副作用的收集范围限定在组件内
  2. 响应式效果创建

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();
    }
  }

  // 其他函数...
}

当创建响应式效果时:

  1. 通过 new ReactiveEffect(componentUpdateFn) 创建实例
  2. 设置调度器 effect.scheduler = () => queueJob(job)
  3. 生成 updatejob 函数:
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 渲染过程中响应式机制的建立是一个完整的链路:

  1. 组件初始化时创建响应式数据
  2. 渲染过程中通过 effect 建立响应式包装
  3. 渲染函数执行过程中进行依赖收集
  4. 数据变化时触发更新,创建更新任务
  5. 调度系统统一处理更新任务

整个过程中涉及的数据结构(effect、dep、job、queue 等)都是为了服务于这个响应式更新流程,确保数据变化能够精确且高效地触发组件更新。后续我们就来分析依赖收集系统的具体实现。

相关推荐
群联云防护小杜17 分钟前
构建分布式高防架构实现业务零中断
前端·网络·分布式·tcp/ip·安全·游戏·架构
ohMyGod_1231 小时前
React16,17,18,19新特性更新对比
前端·javascript·react.js
前端小趴菜051 小时前
React-forwardRef-useImperativeHandle
前端·vue.js·react.js
@大迁世界1 小时前
第1章 React组件开发基础
前端·javascript·react.js·前端框架·ecmascript
Hilaku1 小时前
从一个实战项目,看懂 `new DataTransfer()` 的三大妙用
前端·javascript·jquery
爱分享的程序员2 小时前
前端面试专栏-算法篇:20. 贪心算法与动态规划入门
前端·javascript·node.js
我想说一句2 小时前
事件委托与合成事件:前端性能优化的"偷懒"艺术
前端·javascript
爱泡脚的鸡腿2 小时前
Web第二次笔记
前端·javascript
良辰未晚2 小时前
Canvas 绘制模糊?那是你没搞懂 DPR!
前端·canvas
Dream耀2 小时前
React合成事件揭秘:高效事件处理的幕后机制
前端·javascript