Vue3 源码解析
1 前置知识
1.1 Object.defineProperty
Object.defineProperty() 用于在对象上定义一个新属性,或者修改现有属性的特性。它接受三个参数:
- 对象:要在其上定义或修改属性的对象。
- 属性名:要定义或修改的属性名。
- 描述符:属性的特性,定义为一个对象,包含属性的各种配置,如可写性、可枚举性、可配置性等。
const obj = {};
// 定义一个新属性
Object.defineProperty(obj, "name", {
value: "Alice",
get, // 读取时内部调用的函数
set, // 写入时内部调用的函数
writable: true, // 是否可以修改
enumerable: true, // 是否可枚举
configurable: true, // 是否可删除或修改特性
});
console.log(obj.name); // 输出 'Alice'
// 修改属性值
obj.name = "Bob";
console.log(obj.name); // 输出 'Bob'常见的属性特性:
value:属性值。get:读取时内部调用的函数set:写入时内部调用的函数writable:如果为false,则该属性的值不能修改。enumerable:如果为false,则该属性不会出现在for...in或Object.keys()中。configurable:如果为false,则不能删除该属性,也不能再修改属性特性。
案例:
// vue2响应式原理
// 定义一个商品
let quantity = 2;
let product = {
price: 10,
quantity: quantity,
};
// 总价格
let total = 0;
// 计算总价格的函数
let effect = () => {
total = product.price * product.quantity;
};
// 第一次打印
effect();
console.log(`第一次总价格:${total}`);
// quantity属性具备了响应性
Object.defineProperty(product, "quantity", {
set(newVal) {
console.log("setter");
quantity = newVal;
// 触发effect
effect();
},
get() {
console.log("getter");
return quantity;
},
});
product.quantity = 15;
console.log(`第二次总价格:${total}`);
Vue2 中实现响应式的 弊端:
Object.defineProperty只可以监听 指定对象的指定属性的** **getter和setter。- 被监听了
getter和setter的属性,就被叫做该属性具备了响应性。 - 由于 JavaScript 的限制,没有办法监听到指定对象新增了一个属性,所以新增的属性就没有办法通过
Object.defineProperty来监听getter和setter,所以新增的属性就失去了响应性。
1.2 Proxy
Proxy 是 JavaScript 中的一种功能强大的特性,它可以用来定义一个对象的行为,拦截对该对象的访问操作。你可以通过 Proxy 对象拦截和定制对目标对象的读写、函数调用、属性访问等行为。
语法:
Proxy 构造函数接收两个参数:
- 目标对象(target):要代理的对象。
- 处理器对象(handler):包含各种操作的拦截方法,如
get、set、deleteProperty等。
常见的拦截方法:
get(target, prop):拦截属性的读取。set(target, prop, value):拦截属性的赋值。has(target, prop):拦截in操作符。deleteProperty(target, prop):拦截delete操作符。apply(target, thisArg, argumentsList):拦截函数调用。construct(target, argumentsList):拦截对象实例化。
案例:
let product = {
price: 10,
quantity: 2,
};
// new Proxy接受两个参数(被代理对象,handler对象)
// 生成 proxy 代理对象实例,该实例拥有《被代理对象的所有属性》,并且可以监听setter和getter
// 此时product被称为《被代理对象》,proxyProduct被称为《代理对象》
const proxyProduct = new Proxy(product, {
// 监听proxyProduct的set方法,在proxyProduct.xx = xx 时被触发
// 接受4个参数:被代理对象target,指定的属性名key,新值newVal,最初被调用的对象receiver
// 返回值为一个boolean类型,true表示属性设置成功
set(target, key, newVal, receiver) {
console.log(target, key, newVal, receiver);
// 为target赋新值
target[key] = newVal;
// 触发effect重新计算
effect();
return true;
},
// 监听proxyProduct的get方法发,在proxyProduct.xx时,辈出啊
// 接收3个参数:被代理对象target,指定的属性名key,最初被调用的对象receiver
// 返回值为proxyProduct.xx的结果
get(target, key, receiver) {
console.log(target, key, receiver);
return target[key];
},
});
// 总价格
let total = 0;
let effect = () => {
total = proxyProduct.price * proxyProduct.quantity;
};
// 第一次打印
effect();
console.log(`第1次总价格:${total}`);
proxyProduct.quantity = 5;
console.log(`第2次总价格:${total}`);
Proxy 的优势:
Proxy 的优势:
- 可拦截整个对象,无需逐个定义属性,
Proxy一次性拦截整个对象的所有操作,包括读取、写入、删除、判断、函数调用等。 - 支持新增属性的拦截,即使属性是后添加的,也能被
get、set等拦截器捕捉,适合动态数据场景。
总结:
总结:
- Proxy
Proxy将代理一个对象(被代理对象),得到一个新的对象(代理对象),同时拥有被代理对象中的所有属性。- 当想要修改对象的指定属性时,应该使用代理对象进行修改。
- 代理对象的任何一个属性都可以触发 handler(可理解为代理规则) 的
getter和setter。
- Object.defineProperty
Object.defineProperty为指定对象的指定属性设置属性描述符。- 当想要修改对象的指定属性时,可以使用原来对象进行修改。
- 通过属性描述符,只有被监听的指定属性,才可以触发
getter和setter。
1.3 Reflect
Reflect.get(target, property[, receiver])
const obj = { name: "Alice" };
console.log(Reflect.get(obj, "name")); // 输出 'Alice'Reflect.set(target, property, value[, receiver])返回true表示属性设置成功,false表示失败
Reflect.set(obj, "name", "Bob");
console.log(obj.name); // 输出 'Bob'
// 等同于
obj.name = "Bob";Reflect.has(target, property)
console.log(Reflect.has(obj, "name")); // true,相当于 'name' in objReflect.deleteProperty(target, property)
Reflect.deleteProperty(obj, "name");
console.log(obj.name); // undefinedReflect.ownKeys(target)
const user = { name: "Alice", [Symbol("id")]: 123 };
console.log(Reflect.ownKeys(user)); // 输出 ['name', Symbol(id)]在 Proxy 中配合使用 Reflect(推荐做法):
const target = { name: "Alice" };
const proxy = new Proxy(target, {
get(target, prop, receiver) {
console.log(`正在读取 ${prop}`);
return Reflect.get(target, prop, receiver); // 推荐:保持原始行为 !!!
},
set(target, prop, value, receiver) {
console.log(`正在设置 ${prop} = ${value}`);
return Reflect.set(target, prop, value, receiver); // 推荐 !!!
},
});
proxy.name; // 输出:正在读取 name → 'Alice'
proxy.age = 20; // 输出:正在设置 age = 20总结:
当我们期望监听代理对象的 getter 和 setter 时,不应该使用 target[key],因为他在某些时刻时不可靠的(缺少了监听)。而应该使用 Reflect,借助他的 get 和 set 发方法发方法,使用 receiver(proxy 实例)作为 this,已达到期望的结果。
| Reflect 方法 | 相当于 | 用途说明 |
|---|---|---|
Reflect.get() | obj[prop] | 读取属性 |
Reflect.set() | obj[prop] = val | 设置属性 |
Reflect.has() | 'prop' in obj | 判断属性是否存在 |
Reflect.deleteProperty() | delete obj[prop] | 删除属性 |
Reflect.ownKeys() | bject.keys() + Object.getOwnPropertySymbols() | 获取所有键(含 Symbol) |
1.4 WeakMap
概念:
- 弱引用:不会影响垃圾回收机制。即:WeakMap 的 key 不再存在任何引用时,会被直接回收。
- 强引用:会影响垃圾回收机制。存在强引用的对象永远不会被回收。
案例:
- 使用 Map:
let obj = {
name: "hui",
};
const map = new Map();
map.set(obj, "value");
obj = null;
console.log(map);
- 使用 WeakMap:
let obj = {
name: "hui",
};
const map = new Map();
map.set(obj, "value");
obj = null;
console.log(map);
此时 WeakMap 中不存在任何的值,即:obj 不存在其他引用时,WeakMap 不会阻止垃圾回收,基于 obj 的而引用将会被清除。这就证明了 WeapMap 的弱引用性。
总结:由以上可知,对于 WeakMap 而言,它存在两个比较严重的特性:
- key 必须是对象
- key 是弱引用的
2 响应式系统
2.1 reactive
测试案例
const { reactive, effect } = Vue;
// 1. 创建响应式对象
const obj = reactive({
name: "hui",
});
// 2. effect执行过程
effect(() => {
document.querySelector("#app").innerHTML = `<h1>${obj.name}</h1>`;
});
setTimeout(() => {
obj.name = "幼儿园国王";
});
当使用 reactive 创建响应式对象时,底层首先调用的是 reactive(),而 reactive() 函数内部调用的是 createReactiveObject() 函数。

reactive 函数的源码比较简单,首先判断是否是一个只读的代理,如果是就直接返回,否则调用 createReactiveObject 函数。

createReactiveObject 函数源码:
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// * 判断是否是对象类型,不是对象类型发出警告,直接返回
if (!isObject(target)) {
if (__DEV__) {
warn(
`value cannot be made ${
isReadonly ? "readonly" : "reactive"
}: ${String(target)}`
);
}
return target;
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
// * 如果已经是proxy,直接return回去
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target;
}
// target already has corresponding Proxy
// * proxyMap已经存在了target对象的proxy对象,那么就直接返回
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
// only specific value types can be observed.
const targetType = getTargetType(target);
// * 类型无效也直接返回
if (targetType === TargetType.INVALID) {
return target;
}
// * 创建target对象对应的Proxy对象,并且传入baseHandlers(就是get、set等)
// * 什么是Collection? Map、Set、WeakMap、WeakSet
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
);
// * 将target和proxy在proxyMap中存储一份
proxyMap.set(target, proxy);
return proxy;
}createReactiveObject 函数内部首先判断以下几种情况:
- 判断是否是对象类型,不是对象类型发出警告,直接返回
- 判断是不是已经是
proxy,是proxy就直接返回(避免嵌套代理) - 判断
proxyMap是不是已经有了target,有了就直接返回(确保对同一个target不会重复代理) - ......
以上几种情况判断完之后就会创建 Proxy 代理,并将 target 和 proxy 在 proxyMap 中存储一份。
在创建 Proxy 代理时,判断是否是集合类型,这里就先忽略吧,碰到了再分析。
那么就说一下 baseHandlers 吧,baseHanlers 是在调用 createReactiveObject 函数时传入的 mutableHandlers(集合那个就先忽略吧),实际上还是 new 了一个 MutableReactiveHandler 实例。
export const mutableHandlers: ProxyHandler<object> =
/*#__PURE__*/ new MutableReactiveHandler();MutableReactiveHandler 类:这个代码里挺多的,这里就直接放个框架吧,从图中可以看出来,缺少了一个 get 函数,不难发现,MutableReactiveHandler 继承了 BaseReactiveHandler 类,那 get 函数就是在这个类中了。

BaseReactiveHandler 类:

触发 get 和 set 操作见下一步骤,到这里创建响应式对象这一步骤已经结束啦!
创建 reactive 响应式对象的的过程如下图所示:

补充:这个手动调用的 effect 函数相当于:
- 组件的响应式渲染:Vue 组件模板的渲染本质上是一个被 effect 包裹的副作用函数,当依赖的响应式数据变化时,会重新执行渲染。例如:
<div></div>等同于effect(() => { renderTemplate(component) }) - computed 计算属性:计算属性的实现基于 effect,它会跟踪依赖并在依赖变化时重新计算值。例如:
computed(() => count.value * 2)等同于effect(() => { doubled.value = count.value * 2 }) - watch 监听器:watch 的内部使用 effect 来监听响应式数据的变化并执行回调。例如:
watch(count, (newVal) => { console.log('Count changed:', newVal); })等同于effect(() => { track(count); triggerCallbackOnChange(count) })
:::
首先会触发内部的 effect 函数,(前面省略了一点)创建一个新的 ReactiveEffect 实例 _effect,并传入副作用函数 fn,以及调度器 scheduler,传入函数、空操作函数和调度器,如果传入了选项 options,就将其添加到 _effect 实例上,如果没有传入选项或者没有设置 lazy 为 true,就立即执行副作用函数。(剩余的就等会用到再说吧!)

effect 函数源码:
/**
* 注册给定的函数以跟踪响应式更新。
* 给定的函数会立即执行一次。每当在该函数中访问的任何响应式属性更新时,函数都会再次运行。
*
* @param fn - 用于跟踪响应式更新的函数
* @param options - 用于控制副作用行为的选项。
* @returns 一个运行器,可用于在创建后控制副作用。
*/
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
// * 检查传入的 fn 是否已经是一个 ReactiveEffect的副作用函数了,如果是,则提取其内部的原始函数
if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
fn = (fn as ReactiveEffectRunner).effect.fn;
}
// * 调用一次effect函数,会根据传入的fn,创建一个新的ReactiveEffect对象:_effect
// * 根据 fn -> _effect对象
// * fn会变成_effect对象的fn属性
// * _effect对象内部的scheduler就是
// * () => {
// * if(_effect.dirty){
// * _effect.run()
// * }
// * }
// * 那么就意味着,当内部执行scheduler的时候,它会回头调用_effect的run,而run方法内部,会调用fn
// * 如何执行:那么之后如果想要重新执行fn函数,只需要执行scheduler就可以了
// * 创建一个新的 ReactiveEffect 实例,传入函数、空操作函数和调度器
const _effect = new ReactiveEffect(fn, NOOP, () => {
// * 若副作用函数标记为脏数据,则重新运行该副作用函数
if (_effect.dirty) {
_effect.run();
}
});
// * 如果传入了选项对象,则将选项合并到 _effect 实例上
if (options) {
extend(_effect, options);
// * 若选项中指定了作用域,则记录副作用函数的作用域
if (options.scope) recordEffectScope(_effect, options.scope);
}
// * 如果没有传入选项,或者选项中没有设置 lazy 为 true,则立即运行副作用函数
if (!options || !options.lazy) {
_effect.run();
}
// * 创建一个绑定到 _effect.run 方法的运行器,并将其转换为 ReactiveEffectRunner 类型
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner;
// * 将创建的 ReactiveEffect 实例挂载到运行器的 effect 属性上
runner.effect = _effect;
// * 返回运行器
return runner;
}接下来进入到 _effect 实例的 run 方法(ReactiveEffect 类的 run 方法),这个脏状态对计算属性尤为重要,这里就先不说了,如果 active 为 false,就直接执行 fn() 不需要进行依赖收集,运行了 this._runnings 就会进行++操作,表示正在运行,在执行真正 effect 函数之前,会将上一次清除掉,然后执行 fn(),最后清除掉不需要的依赖,运行完之后 this._runnings 进行--操作(默认值为 0)。
实际上的副作用函数执行过程就是:(这个调度是在创建 _effect 实例时传入的)

fn 函数与 _effect 的关系如下图所示:


ReactiveEffect 类的 run 方法源码:(注:删除了非重点代码)
// * 这是ReactiveEffect的run方法
run() {
// * computed:运行过一次就变成不是脏值了(不再是脏状态)
// * 这对于 computed尤为重要,,因为它们的值只在依赖发生时才需要重新计算
this._dirtyLevel = DirtyLevels.NotDirty;
// * 不是active的(active=false),直接执行即可,不需要做依赖收集
if (!this.active) {
return this.fn();
}
// 省略了代码.....................
try {
shouldTrack = true;
// * 这里是将当前的reactiveEffect赋值给了activeEffect
// * 所以全局的activeEffect就有值了,那么我们收集以来的时候就可以使用activeEffect了
activeEffect = this; // ! 当前fn对应的reactiveEffect
// * 记录是否在运行,运行完会--
this._runnings++;
// * 在执行真正的effect函数之前,先将上一次清除掉
// * 为什么? 因为如果我们使用v-if/else以来的是不同的数据,获取某些数据在执行后就被移除了
preCleanupEffect(this);
// * 执行过程会重新收集依赖
return this.fn();
} finally {
// * 如果后续还有多余的不再使用的依赖,那么直接清除掉
// * 第一次的依赖:{name,age,height,address}
// * 第二次的依赖:{name,age},那么height/address就需要清除掉
postCleanupEffect(this);
this._runnings--;
// * 执行完操作后再赋值给activeEffect
activeEffect = lastEffect;
shouldTrack = lastShouldTrack;
}
}目前的执行流程如下图所示:

接下来,执行 fn 函数(也就是调用 effect 传入的函数),obj.name就要触发 get 行为了。首先判断是否是响应式对象、只读、浅层代理等,然后判断是不是数组类型;从原生对象上获取结果(res="hui"),如果不是只读属性,那么就调用 track 函数进行跟踪依赖。再进行判断是不是浅层代理、ref、对象等,最后返回结果,这个函数里面还是挺简单的。

get 函数源码:
get(target: Target, key: string | symbol, receiver: object) {
const isReadonly = this._isReadonly,
isShallow = this._isShallow;
// * 判断key是否是响应式对象、只读、浅层响应等
// ........
// * 判断是否是数组类型
// ........
// * 直接从原生对象上获取结果,获取target对象上的key属性
const res = Reflect.get(target, key, receiver);
// * 如果是Symbol并且时builtInSymbols(内建符号),或者其他不可跟踪的key,那么直接返回,不收集依赖
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res;
}
// * 不是只读属性
if (!isReadonly) {
// * 调用track跟踪依赖
track(target, TrackOpTypes.GET, key);
}
// * 如果是isShallow,那么不进行深层代理
if (isShallow) {
return res;
}
// * 是ref,并且key是一个数字,那么返回res或者res.value
if (isRef(res)) {
// ref unwrapping - skip unwrap for Array + integer key.
return targetIsArray && isIntegerKey(key) ? res : res.value;
}
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res);
}
return res;(重点)接下来就进入到了 track 函数,函数功能(收集依赖,构建一一对应的关系)主要是构建 targetMap、depsMap、depMap,最后调用 trackEffect 函数。这里的 activeEffect 是全局对象,是通过 _effect 赋值给它的,_effect == acitveEffect == fn。

track 函数源码:
export function track(target: object, type: TrackOpTypes, key: unknown) {
// * 需要跟踪依赖,并且activeEffect是有值的(activeEffect是当前的的ReactiveEffect对象实例)
if (shouldTrack && activeEffect) {
// * 根据target对象取出对应的depsMap
let depsMap = targetMap.get(target);
// * 如果为空,那么就会创建一个新的Map,并且设置到targetMap中
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// * 根据key去获取对应的依赖,dep的本质是另外一个Map对象
let dep = depsMap.get(key);
// * 没有对应的依赖,那么就会添加依赖
if (!dep) {
// * 这里有一个细节 dep = createDep(()=>depsMap!.delete(key))
// * 并且传入了一个清理函数,某一个key不再需要依赖响应时,调用它的clean方法就可以了
depsMap.set(key, (dep = createDep(() => depsMap!.delete(key))));
}
// * 将activeEffect添加到dep中
trackEffect(
activeEffect,
dep,
__DEV__
? {
target,
type,
key,
}
: void 0
);
}
}targetMap、depsMap、depMap关系如下图所示:

接下来就进入到了 trackEffect 函数了,主要就是建立起 effect 与 _trackId 的一一对应的关系,effect 的 dep是一个数组,因为可能会有多个不同的依赖。
Map(Dep)的作用:属性 → Effect 的映射effect.deps的作用:Effect → 属性的反向记录
单向记录的缺陷:如果仅用 Map(属性 → effect),当 effect的依赖关系变化时(如条件分支改变),无法高效清理不再依赖的属性,可能导致内存泄漏或冗余更新。


trackEffect 函数源码:
export function trackEffect(
// * 当前活跃的effect
effect: ReactiveEffect,
// * 当前的依赖项集合,类型为 Dep dep:{_effect: trackId}
dep: Dep,
// * 调试事件的额外信息,可选参数,类型为 DebuggerEventExtraInfo
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// * 检查依赖项中记录的副作用函数的跟踪 ID 是否与当前副作用函数的跟踪 ID(_trackId) 不一致
// * 如果相等说明已经收集了,比如{{message}},{{message}}多次使用的插值语法的情况
if (dep.get(effect) !== effect._trackId) {
// * 将effect添加到dep中,并且给依赖项的effect设置_trackId
dep.set(effect, effect._trackId);
// * 获取当前依赖项数组的最后一个dep(这是上一次fn函数在执行时收集的dep)
const oldDep = effect.deps[effect._depsLength];
// * 如果数组最后一个dep不是当前dep,说明dep的依赖发生了变化
// * 删除旧的dep,添加新的dep {name:"hui",nickname:"幼儿园国王"}
// * 根据情况判断到底时展示name或者nickname其中之一,那么切换时,就需要切换dep
if (oldDep !== dep) {
if (oldDep) {
// * 清除旧的依赖关系
cleanupDepEffect(oldDep, effect);
}
// * 将当前依赖项添加到副作用函数的 deps 数组中,并将 _depsLength 加 1
effect.deps[effect._depsLength++] = dep;
// * 如果旧依赖项与当前依赖项相同,仅将 _depsLength 加 1
effect._depsLength++;
}
// * 开发环境下,触发副作用函数的 onTrack 回调
if (__DEV__) {
// eslint-disable-next-line no-restricted-syntax
effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!));
}
}
}到目前位置,effect 函数的执行逻辑已经结束啦!!!还是挺复杂的。
effect 函数的执行流程图如下所示:

接下来就是紧张刺激的 set 执行过程了!计时器到期之后就会执行,obj.name = "幼儿园国王",触发 set 行为。首先从 target 获取旧值,然后经过一系列判断(这里先省略了),通过 Reflect.set() 设置新值,设置成功就是返回 true,否则就是 false,再判断是否是新添加属性还是修改已有属性的值,调用 trigger() 。


set 函数源码:(删除了部分代码,只关注核心)
set(target: object, key: string | symbol, value: unknown, receiver: object): boolean {
// * 先从target中获取旧值(为了后续判断新值和就只是否发生了变化,还有ref、只读属性的特殊情况)
let oldValue = (target as any)[key];
// * 判断是否是浅层响应
// ...........(省略了浅层判断)
// * 判断key是否已经在target中了
const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key);
// * 通过Reflect.set设置新的值
const result = Reflect.set(target, key, value, receiver);
// don't trigger if target is something up in the prototype chain of original
// * 如果目标对象是receiver的原始对象,那么才会进行更新
if (target === toRaw(receiver)) {
if (!hadKey) {
// * 新增属性,触发ADD
trigger(target, TriggerOpTypes.ADD, key, value);
} else if (hasChanged(value, oldValue)) {
// * 修改属性值,触发SET
trigger(target, TriggerOpTypes.SET, key, value, oldValue);
}
}
return result;
}接下来就要进入到 trigger 函数了,首先就是获取 target 对应的 depsMap,不存在就直接返回,然后初始化一个 deps 列表(可能会有多个依赖),存放每一个要执行的 dep, 然后经过各种判断(这里不是重点,省略了),最后遍历 deps 列表,调用 triggerEffects 触发每一个 dep,这里的 dep 是一个 Map 类型。


trigger 函数源码:
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
// * 获取target对应的depsMap
const depsMap = targetMap.get(target);
if (!depsMap) {
// never been tracked
// * 没有获取到直接返回
return;
}
// * 初始化一个列表,用来存放需要执行的dep(有可能不是一个依赖)
let deps: (Dep | undefined)[] = [];
// ..........(省略了)
// * 暂停调度
pauseScheduling();
// * 遍历deps,触发每个依赖的更新效果
for (const dep of deps) {
if (dep) {
triggerEffects(
dep,
DirtyLevels.Dirty,
__DEV__
? {
target,
type,
key,
newValue,
oldValue,
oldTarget,
}
: void 0
);
}
}
// * 恢复调度
resetScheduling();
}接下来进入到了 triggerEffects 函数内部了,遍历 depMap 中的所有 key ,因为 key 对应的才是 effect,将其添加到任务调度队列中,遍历完之后执行恢复调度,最后从队列中弹出并执行,这里执行的函数其实就是创建 _effect 实例时传入的函数(流程图中有标注),然后进入到 run 方法,在 run 方法中执行 this.fn(),紧接着触发 get 行为,获取到结果值,触发页面更新,后续的逻辑和 get 行为一样,这里就不再阐述了。

triggerEffect 函数源码:
export function triggerEffects(
dep: Dep,
dirtyLevel: DirtyLevels,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// * 暂停调度,防止在触发副作用函数时进行不必要的调度操作
pauseScheduling();
// * 遍历所有的keys,因为keys就是effect,执行它们
for (const effect of dep.keys()) {
// * dep.get(effect) 操作开销较大,因此延迟计算并复用结果
let tracking: boolean | undefined;
// * 检查副作用函数的脏数据级别是否小于传入的脏数据级别,并且确认跟踪状态
if (
effect._dirtyLevel < dirtyLevel &&
(tracking ??= dep.get(effect) === effect._trackId)
) {
// * 如果副作用函数的脏数据级别为 NotDirty,则标记需要调度
effect._shouldSchedule ||=
effect._dirtyLevel === DirtyLevels.NotDirty;
// * 更新副作用函数的脏数据级别
effect._dirtyLevel = dirtyLevel;
}
// * 检查是否需要调度,并且确认跟踪状态
if (
effect._shouldSchedule &&
(tracking ??= dep.get(effect) === effect._trackId)
) {
// * 开发环境下,触发副作用函数的 onTrigger 回调
if (__DEV__) {
// eslint-disable-next-line no-restricted-syntax
effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo));
}
// * 触发副作用函数的自定义触发逻辑
effect.trigger();
// * 检查副作用函数是否不在运行中,或者允许递归调用,并且脏数据级别不是 MaybeDirty_ComputedSideEffect
if (
(!effect._runnings || effect.allowRecurse) &&
effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect
) {
// * 标记为不需要调度
effect._shouldSchedule = false;
// * 如果副作用函数有调度器,将调度器添加到队列中
if (effect.scheduler) {
// * 将effect.scheduler添加到队列中
queueEffectSchedulers.push(effect.scheduler);
}
}
}
}
// * 恢复调度,执行队列中的调度器
resetScheduling();
}key 才是 effect:

简单可以概括为一下流程:

set 行为的完整执行流程如下图所示:

2.2 ref
测试案例:
const { ref, effect } = Vue;
const counter = ref(0);
effect(() => {
document.querySelector("#app").innerHTML = counter.value;
});
setTimeout(() => {
counter.value = 10;
}, 200);
当使用 ref 创建响应式对象时,底层首先调用的是 ref(),而 ref() 函数内部调用的是 createRef() 函数。


ref 函数源码为:
export function ref(value?: unknown) {
// * 本质上是去调用createRef,并且第二个参数shallow(浅的)设置为false
return createRef(value, false);
}createRef 函数源码为:
function createRef(rawValue: unknown, shallow: boolean) {
// * 判断传入是否已经是ref,如果是就直接返回
if (isRef(rawValue)) {
return rawValue;
}
// * 如果不是ref,就会创建一个RefImpl的对象
return new RefImpl(rawValue, shallow);
}在 createRef 函数内部,首先判断是否已经为 ref 了,如果已经是 ref 就直接返回了,没必要重复做 ref 操作(反而会浪费性能);如果不是 ref,那么就会创建一个 RefImpl 的实例。

RefImpl 类的源码为:
class RefImpl<T> {
private _value: T;
private _rawValue: T;
// * 用于存储依赖的副作用函数,是一个Map类型
public dep?: Dep = undefined;
public readonly __v_isRef = true;
constructor(value: T, public readonly __v_isShallow: boolean) {
// * 保留原始value值在_rawValue
this._rawValue = __v_isShallow ? value : toRaw(value);
// * 如果value是一个对象,那么会调用toReactive,所以本质上ref(对象)实际上调用的还是reactive(对象)
this._value = __v_isShallow ? value : toReactive(value);
}
// * 访问value值
get value() {
// * 使用这个ref的value时,就收集依赖
trackRefValue(this);
return this._value;
}
// * 设置value值
set value(newVal) {
// * 判断是否需要对新值进一步处理
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
newVal = useDirectValue ? newVal : toRaw(newVal);
// * 查看新值和旧值没有发生改变,有改变才会触发依赖(性能优化)
if (hasChanged(newVal, this._rawValue)) {
// * 原来的值保存到oldVal
const oldVal = this._rawValue;
// * _rawValue属性保存新值
this._rawValue = newVal;
// * 判断是否需要转化为响应式对象
this._value = useDirectValue ? newVal : toReactive(newVal);
// * 触发依赖更新
triggerRefValue(this, DirtyLevels.Dirty, newVal, oldVal);
}
}
}创建 ref 响应式的流程如下图所示:

这个过程和 reactive 章节中的 effect 的过程是一样的,理解了前面的 effect 执行过程,这里就不需要解释了。
具体内容见 2.2.2 执行 effect 函数过程。
接下来就是定时器结束进行修改值了,就进入到了 set 函数了,首先获取到新值,然后判断新值与原来的值是否发生改变,如果没有发生改变就不再走下面的逻辑了(算是一种性能优化吧),如果发生了改变那么就进入到了 triggerRefValue 函数了。

set 函数源码:
set value(newVal) {
// * 判断是否需要对新值进一步处理
const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
newVal = useDirectValue ? newVal : toRaw(newVal);
// * 查看新值和旧值没有发生改变,有改变才会触发依赖(性能优化)
if (hasChanged(newVal, this._rawValue)) {
// * 原来的值保存到oldVal
const oldVal = this._rawValue;
// * _rawValue属性保存新值
this._rawValue = newVal;
// * 判断是否需要转化为响应式对象
this._value = useDirectValue ? newVal : toReactive(newVal);
// * 触发依赖更新
triggerRefValue(this, DirtyLevels.Dirty, newVal, oldVal);
}
}紧接着就进入到了 triggerRefValue 函数内部了,获取到 ref 对象关联的以来集合 (Map 类型),然后如果依赖集合存在,那么就触发 triggerEffects 函数,后续过程就和 reactive 的过程一样了,这里就不再叙述了。

triggerRefValue 函数源码:
export function triggerRefValue(
ref: RefBase<any>,
dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
newVal?: any,
oldVal?: any
) {
// * 将 ref 对象转换为其原始对象(确保后续使用的是原始对象,不是某个包装过的响应式对象),避免代理对象的干扰
ref = toRaw(ref);
// * 获取 ref 对象关联的依赖集合
const dep = ref.dep;
// * 检查依赖集合是否存在
if (dep) {
// * 触发依赖集合中的所有副作用函数
triggerEffects(
dep,
dirtyLevel,
// * 开发环境下提供额外的调试信息
__DEV__
? {
target: ref, // * 触发更新的目标对象
type: TriggerOpTypes.SET, // * 操作类型为设置值
key: "value", // * 操作的键为 value
newValue: newVal, // * 新值
oldValue: oldVal, // * 旧值
}
: void 0
);
}
}triggerEffects 过程见章节 2.2.3 set 执行过程。
整体流程如下图所示:

2.3 computed
测试案例:
const { ref, effect, computed } = Vue;
const firstName = ref("coder");
const lastName = ref("hui");
debugger;
const fullName = computed(() => {
return firstName.value + lastName.value;
});
effect(() => {
document.querySelector("#app").innerHTML = `<h1>${fullName.value}</h1>`;
});
setTimeout(() => {
lastName.value = "shine";
}, 1000);先说结论,computed 就可以看作是 effect 函数,都是触发 getter,由于计算属性只会执行一次(缓存),是因为 dirtyLevel 变量,只有 dirtyLevel = 4(标记为脏数据) 时,才会执行 getter,执行完之后就会变为 0(即 dirtyLevel =0,非脏数据),这个变量也称为脏变量。
理解了 effect 的执行过程就差不多理解了 computed 的执行过程。
// 原:
const fullName = computed(() => {
return firstName.value + lastName.value;
});
// 等同于
effect(() => {
return firstName.value + lastName.value; // 但这里可能不太对,都是大概就是这个意思
});
使用 ref 创建响应式数据的过程这里就不再赘述了,具体过程见 2.2.1 章节。
首先会进入到 computed 函数,这个函数还是挺简单的,判断传入的 getterOrOptions 是否是一个函数,如果时就是一个只读的计算属性(不能修改,修改值控制台会发出警告,想要修改值,要传入一个对象并且包含 setter),此时判断的值为 true,进入 if 语句,否则进入 else 语句,主要是给 getter 和 setter 进行赋值,接下来会创建一个 ComputedRefImpl 实例,并且待会儿会返回出去。

computed 函数源码:
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false
) {
let getter: ComputedGetter<T>;
let setter: ComputedSetter<T>;
// * 判断传入的getterOrOptions是否是一个函数,如果是就是一个只读的计算属性,接下来会进入if分支
// * 例如 getterOrOptions 的值为 () => { return firstName.value + lastName.value }
const onlyGetter = isFunction(getterOrOptions);
if (onlyGetter) {
// * 只有 getter 并且是一个函数
// * 直接getterOrOptions赋值给getter即可
getter = getterOrOptions;
// * 这里的setter行为可以理解为空函数,调用时会发出警告
setter = __DEV__
? () => {
warn("Write operation failed: computed value is readonly");
}
: NOOP;
} else {
// * 有 getter 也有 setter
// * 直接将 getterOrOptions.get 赋值给 getter
// * 直接将 getterOrOptions.set 赋值给 setter
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
// * 创建一个ComputedRefImpl对象,并且待会儿会返回出去
const cRef = new ComputedRefImpl(
getter,
setter,
onlyGetter || !setter,
isSSR
);
// * dev环境
if (__DEV__ && debugOptions && !isSSR) {
cRef.effect.onTrack = debugOptions.onTrack;
cRef.effect.onTrigger = debugOptions.onTrigger;
}
// * 返回computed属性
return cRef as any;
}接下来进入到ComputedRefImpl 类的构造函数内部,在当前实例上添加 effect 属性,通过 new ReactiveEffect,会传入两个匿名函数,然后this.effect记录computed属性,this.effect 记录 active,在非 SSR 环境下是 true,是有缓存的,并且记录该属性是不是为只读;这部分就不放源码了。

创建计算属性这部分流程已经结束了,这部分还是挺简单的,流程如下图所示:

直接放流程图吧,确实挺复杂的,不仅要收集 ref 的依赖,也要收集 computed 的依赖。

这里的 set 执行过程就是 ref 的 set 的执行过程,但可能就是有两层依赖,ref 对应的依赖,effect 对应的依赖。
具体的过程见章节 2.2.3 set 执行过程。
收集阶段:
- 渲染
effect依赖了computed。 computed的副作用函数依赖了ref。
触发阶段:
- 修改
ref→ 触发computed的effect标记 dirty。 - 渲染
effect被调度执行 → 再访问computed→ 触发getter→ 间接重新依赖ref。
简单过程如下图所示:

2.4 watch
测试案例:
const { watch, ref } = Vue;
const name = ref("hui")
// source:
// 类型1 : 一个函数,返回一个值
// 类型2 : ref
// 类型3 : reactive 对象
// 类型4 : 以上类型组成的的
watch(name, () => {
console.log(`我的名字是:${name.value}`);
}
setTimeout(() => {
name.value = "幼儿园国王";
}, 2000);
这个过程就不再说明了,用的就是前面的过程。
2.4.2 watch 执行过程
首先会调用 watch 函数,但是 watch 内部又调用了 doWatch 函数,主要逻辑还是在 doWatch 内部,这个 doWatch 的源码太长了这里也不放了,可以去packages\runtime-core\src\apiWatch.ts 文件查看。


watch 执行过程:

图中省略了 effect.run() 的过程,这个过程可以看 2.1.2 执行 effect 函数过程。紫色箭头是 watchEffect 执行过程。
它的工作流程还是在 job 函数内部,通过 effect.run()获取到新值,这个也不再说了,后面有时间了再来详细说明。
2.5 watchEffect
他只传入一个 source,而不是 cb(watch(source,cb,options)),他这个 source 是一个函数类型的,他和 watch 底层都是调用了 doWatch,过程和上面一样,只不过走的 if 语句不同,见上面流程图。
INFO
注:watchEffect 是立即执行的,watch 默认情况下不会默认执行,触发设置 immediate 属性。
执行 effect.run()就只在执行传入的 fn 函数,比如 computed(()=>{...})、watchEffect(()=>{...})、effect(()=>{...})。
3 模板编译过程
Vue 的模板编译过程大致分为以下几个步骤:解析(Parse)、转换(Transform)、生成(Generate)。 
3.1 解析(Parse)
目的: 将模板字符串解析为抽象语法树(AST)。
过程:
使用有限状态机算法解析模板
识别各种模板结构:标签、属性、文本、表达式、指令等
生成具有层级结构的 AST 节点
默认会忽略注释节点(除非明确配置保留)
示例:
<div class="container">
<h1>{{ title }}</h1>
<button @click="handleClick">Click</button>
</div>解析后的 AST 结构(简化):
{
type: 1, // 元素节点
tag: "div",
props: [{ type: 6, name: "class", value: "container" }],
children: [
{
type: 1,
tag: "h1",
children: [{ type: 5, content: "title" }] // 表达式节点
},
{
type: 1,
tag: "button",
props: [{ type: 7, name: "click", exp: "handleClick" }], // 指令
children: [{ type: 3, content: "Click" }] // 文本节点
}
]
}3.2 转换(Transform)
目的: 对 AST 进行转换和优化
主要工作:
静态提升:标记和提升静态节点,避免重复渲染
补丁标志:为节点添加 patch flags,标识需要更新的内容类型
树形转换:处理条件、循环等特殊结构
优化处理:移除空白节点、合并静态节点等
示例转换:
静态的
class="container"会被标记为静态属性\{\{ title \}\}会被标记为动态文本(patch flag: 1) (这里使用\显示,实际是不需要的)@click指令会被转换为对应的事件处理器
3.3 生成(Generate)
目的: 将转换后的 AST 生成渲染函数代码
过程:
遍历 AST 节点,生成对应的 JavaScript 代码
使用
_createElementVNode和_createCommentVNode等 helper 函数生成优化的渲染函数
生成的代码示例:
import { createElementVNode as _createElementVNode, ... } from "vue"
export function render(_ctx, _cache) {
return _createElementVNode("div", { class: "container" }, [
_createElementVNode("h1", null, _toDisplayString(_ctx.title), 1 /* TEXT */),
_createElementVNode("button", {
onClick: _cache[0] || (_cache[0] = (...args) => _ctx.handleClick(...args))
}, "Click")
])
}