Vue3.2 双向绑定源码分析

665 阅读4分钟

双向绑定精简版(版本: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 函数接收两个参数,副作用函数和配置对象。

出去合并配置选项,主要流程为

  1. 新建 ReactiveEffect 实例
  2. 执行实例 run 函数
  3. 定义 runner ,将实例赋值给 runner.effect 属性
  4. 返回 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,仅为个人理解,错漏之处欢迎指出。