双向绑定精简版(版本:3.2)——track trigger effect
Vue3 中双向绑定的核心函数为:
- track
- trigger
- effect
还有 targetMap,这是一个 WeakMap 类型的依赖缓存。
const targetMap = {
target: {
key: new Set([effect]),
},
};
这里用对象来表示大概的数据结构。
targetMap 为 key 为目标对象,value 为 depsMap 的 WeakMap
depsMap 为 key 为目标对象的具体需要获取的 key,value 为 dep 的 Map
dep 为 一个 Set 数据结构,值为 副作用函数(effect)。
我们可以用这三个函数实现一个响应式
setup() {
const state = {
msg: 'Hello World',
showMsg: true
}
effect(() => {
console.log(state.msg) // Hello Vue3
track(state, 'get', 'msg')
})
setTimeout(() => {
debugger
state.msg = 'Hello Vue3'
trigger(state, 'set', 'msg', 'Hello Vue')
}, 1000)
return {}
}
track 函数接收三个参数,目标对象,track 操作类型,对象 key。
- target: 要追踪的对象
- type: 操作类型 (get | has | iterate)
- key: 要跟踪对象对应的 key
trigger 函数接收六个参数,目标对象,trigger 操作类型,对象 key,新值,老值,用于调试
- target: 要追踪的对象
- type: 操作类型 (set | add | delete | clear)
- key: 要跟踪对象对应的 key
- newValue: 对应 set 操作的新值
- oldValue: 未操作前的旧值
- oldTarget: 用于调试
effect 函数用于定义副作用函数。配和 targetMap 定义了一个数据结构。
// 伪代码
targetMap: {
target: depsMap
}
depsMap: {
key: dep
}
dep: [effect1, effect2, ...]
// targetMap: 类型为 WeakMap
// depsMap: 类型为 Map
// dep: 类型为 Set
所以响应式的流程其实是 effect 函数定义副作用函数,track 函数跟踪依赖,trigger 函数触发副作用函数。
所以,在 Vue3 的响应式中,最先执行的其实是 effect 函数。
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn;
}
const _effect = new ReactiveEffect(fn);
if (options) {
extend(_effect, options);
if (options.scope) recordEffectScope(_effect, options.scope);
}
if (!options || !options.lazy) {
_effect.run();
}
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner;
runner.effect = _effect;
return runner;
}
effect 函数接收两个参数,副作用函数和配置对象。
出去合并配置选项,主要流程为
- 新建 ReactiveEffect 实例
- 执行实例 run 函数
- 定义 runner ,将实例赋值给 runner.effect 属性
- 返回 runner
那什么时候收集了依赖呢?
我们来看看 ReactiveEffect。
export class ReactiveEffect<T = any> {
active = true;
deps: Dep[] = [];
// can be attached after creation
computed?: boolean;
allowRecurse?: boolean;
onStop?: () => void;
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope | null
) {
recordEffectScope(this, scope); // 3.2 版本新增 scope API 相关
}
run() {
if (!this.active) {
return this.fn();
}
if (!effectStack.includes(this)) {
try {
effectStack.push((activeEffect = this));
enableTracking();
trackOpBit = 1 << ++effectTrackDepth;
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this);
} else {
cleanupEffect(this);
}
return this.fn();
} finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this);
}
trackOpBit = 1 << --effectTrackDepth;
resetTracking();
effectStack.pop();
const n = effectStack.length;
activeEffect = n > 0 ? effectStack[n - 1] : undefined;
}
}
}
stop() {
if (this.active) {
cleanupEffect(this);
if (this.onStop) {
this.onStop();
}
this.active = false;
}
}
}
可以看出,在 constructor 构造函数中,只是把 fn 和 scheduler 绑定到 this 上。
再看 run 函数。
this.active 判断是否有效,是否执行 stop 函数。然后判断副作用函数栈中是否已存在当前实例。这里的栈是为了处理嵌套调用 effect 函数的情况。
在压栈的同时,会把 当前 this 赋值给全局变量 activeEffect。
function e1() {
console.log('e1');
}
function e2() {
effect(e1);
console.log('e2');
}
effect(e2);
因为函数的执行本身就是一个压栈出栈的操作,所以这里用一个副作用函数栈来保证 activeEffect 实例的正确。
然后会有 trackOpBit 和 effectTrackDepth。这两个变量进行了左移的位运算,目的的表示嵌套调用 effect 函数的层级。
先看 track 函数的实现
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!isTracking()) {
return;
}
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = createDep()));
}
const eventInfo = __DEV__ ? { effect: activeEffect, target, type, key } : undefined;
trackEffects(dep, eventInfo);
}
我们省略其中的判断语句,可以发现, track 函数的主要逻辑是从 targetMap 中获取 dep,如果不存在,就新建一个。最后会得到一个 targetMap 的结构。其中新建一个 dep 是通过 createDep 函数创建的。
export const createDep = (effects?: ReactiveEffect[]): Dep => {
const dep = new Set<ReactiveEffect>(effects) as Dep;
dep.w = 0; // 旧依赖,已被追踪
dep.n = 0; // 新依赖
return dep;
};
这个函数的主要作用就是定义两个属性,w、n。表示依赖是否被追踪。
然后会执行 trackEffects 函数。
export function trackEffects(dep: Dep, debuggerEventExtraInfo?: DebuggerEventExtraInfo) {
let shouldTrack = false;
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
dep.n |= trackOpBit; // set newly tracked
shouldTrack = !wasTracked(dep);
}
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!);
}
if (shouldTrack) {
dep.add(activeEffect!);
activeEffect!.deps.push(dep);
}
}
如果 effect 递归层级比最大层级小的话,会判断是否为新依赖。是否为新依赖的判断逻辑是由 dep.n & trackOpBit > 0。由于第一次收集依赖会新建一个 dep,新建的 dep 的 n 属性为 0。这是的 trackOpBit 为 2 。所以 dep.n & trackOpBit 等于 0;返回 false。于是设置 dep.n 为 dep.n | trackOpBit。接下来判断 dep 是否被收集过,如果收集过就不再收集。
还有一种情况是跟踪层级超过最大层级,这时就走清除所有依赖的逻辑。
最后收集依赖。track 结束。进入 finally 逻辑。
finally 逻辑主要是做一个状态的回溯。
包括 dep,trackOpBit,effectTrackDepth,effectStack 等等状态的回溯。
到这里依赖收集就完成了。接下来就是触发依赖。
触发依赖使用 trigger 函数。
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target);
let deps: (Dep | undefined)[] = [];
deps.push(depsMap.get(key));
if (deps.length === 1) {
if (deps[0]) {
triggerEffects(deps[0]);
}
} else {
const effects: ReactiveEffect[] = [];
for (const dep of deps) {
if (dep) {
effects.push(...dep);
}
}
triggerEffects(createDep(effects));
}
}
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) {
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
}
}
trigger 的代码流程其实是比较清晰的,就是去 targetMap 中获取对应的 dep,然后执行遍历所有 effect,如果存在 scheduler 就执行 scheduler,否则执行 run。
到这里就是一个完整的响应式流程。
effect -> _effect.run() -> try {} -> track -> finally {} -> trigger
结束
这篇文章对应的 Vue 版本为 3.2,仅为个人理解,错漏之处欢迎指出。