Vue3 源码瞎看 2——reactivity 浅析

前言

感觉 reactivity 代码比较少,看看能不能一篇写个大概。
一般这种响应式的套路,都是 reactiveobservableobserveobserver 之类的名字,vue2x 中相应逻辑的关键字也差不多:defineReactive + observe + new Observer

先说结论

如果用尽可能少的文字来描述 reactive 的工作流程,就是:

  1. 定义 Proxy
  2. mount 过程中触发 get,进而触发 track
  3. 通过 effect 方法设置 activeEffect
  4. 绑定当前 dep(依赖) 和 activeEffect
  5. 数据更新的时候触发 set,找到 dep,并触发与其绑定的 effect

对比 2x 版本的详细流程

  1. init 过程中定义 dep
  2. init 过程中调用 defineProperty
  3. beforeMount 到 mounted 之间创建 watcher
  4. 第一次执行 watcher 的 getter 渲染视图
  5. 触发 getter,进行 dep 与 Watcher 的绑定
  6. 数据更新触发 set,调用收到影响 dep 上的 watcher(例如组件的 render、watch 函数之类的操作)

之所以把结论搬到了前面。。。是因为简书的 md 可读性略差,接下来去看源码。。。

Proxy

扫了一眼vue-next-master/packages/reactivity/src/reactive.ts,感觉出现频率最高的就是函数 createReactiveObject,但是函数本身并没有被 export ,对外暴露的是(主要就看了 reactive):

  1. reactive
    a. vue 内部:在 collectionHandlers 的 toReactive 和 baseHandlers 的 createGetter 中被调用
    b. 兼容的 data 函数:在 data 函数里面的内容是由该方法进行 Proxy 加工的
    c. 兼容的类+装饰器写法:使用 vue-class-component 直接挂在类里面的变量会通过 proxyRefs (mountComponent => setupComponent => setupStatefulComponent => handleSetupResult => proxyRefs) 进行 Proxy 的加工
    d. 新版 setup 中:可以直接创建 Proxy,然后会走上述 c 的流程,区别就是最后一步不需要再用 Proxy 进行加工,直接把当前的 Proxy return 了出去直接挂在 instance 的 setupState 上
  2. shallowReactive
    a. vue 内部:调用时机在 setupComponent 的 initProps 时,会把 vNode 上的 rawProps 变成 Proxy 挂在 instance.props 上
  3. readonly
    a. vue 内部:在 collectionHandlers 的 toReadonly 被调用
    b. 对外作为只读加工使用
  4. shallowReadonly
    a. vue 内部:调用时机在 setupComponent 的 initProps 之后的 setupStatefulComponent,用于进一步处理 instance.props 和 组件内的 slots

这四个函数,对应封装好的 readonly 状态、handlers 和 collectionHandlers,外部只需要传个 target 进来就可以调用 createReactiveObject 函数,举个例子:

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
    // if trying to observe a readonly proxy, return the readonly version.
    if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
        return target
    }
    return createReactiveObject(
        target,
        false,
        mutableHandlers,
        mutableCollectionHandlers
    )
}

然后看下 createReactiveObject 的源码:

function createReactiveObject(
    target: Target,
    isReadonly: boolean,
    baseHandlers: ProxyHandler<any>,
    collectionHandlers: ProxyHandler<any>
) {
    if (!isObject(target)) {
        if (__DEV__) {
            console.warn(`value cannot be made reactive: ${String(target)}`)
        }
        return target
    }
    // target is already a Proxy, return it.
    // exception: calling readonly() on a reactive object
    if (
        target[ReactiveFlags.RAW] &&
        !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
    ) {
        return target
    }
    // target already has corresponding Proxy
    const proxyMap = isReadonly ? readonlyMap : reactiveMap
    const existingProxy = proxyMap.get(target)
    if (existingProxy) {
        return existingProxy
    }
    // only a whitelist of value types can be observed.
    const targetType = getTargetType(target)
    if (targetType === TargetType.INVALID) {
        return target
    }
    const proxy = new Proxy(
        target,
        targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
    )
    proxyMap.set(target, proxy)
    return proxy
}

官方的注释已经很清晰了,再强行描述一下流程就是:

  1. 判断是不是对象,非对象无法处理成响应式
  2. 判断是不是已经是一个 Proxy
  3. 判断 readOnly 状态,去相应的 Map 找是否已存在对应的 Proxy
  4. 判断是不是非法类型
  5. 在 Map 中存储 Proxy,并 return 出去

看完了取代 defineProperty 的 Proxy,还需要找到 2x 版本的依赖收集和派发更新对应的逻辑

依赖收集

依赖收集就是 vue 去收集在改变的时候需要触发视图更新的变量

在 2 版本,是在 defineProperty 之前创建了对应的 Dep 实例,然后在变量 get 的时候把 Dep 和 Watcher 建立联系

先只看看最常用的 reactive 的实现逻辑吧:
createReactiveObject 最后两个入参 baseHandlerscollectionHandlers,直接读起来 collectionHandlers 就是我们想找的逻辑,而 reactive 函数传入的则是 mutableCollectionHandlers

但是在 createReactiveObject 里两个 handlers 的使用逻辑,是基于 target 的 targetType 来判断的,代码如下:

function getTargetType(value: Target) {
    return value[ReactiveFlags.SKIP] || !Object.isExtensible(value) // ReactiveFlags.SKIP 就是 "__v_skip"
        ? TargetType.INVALID
        : targetTypeMap(toRawType(value))
}

function targetTypeMap(rawType: string) {
    switch (rawType) {
        case 'Object':
        case 'Array':
            return TargetType.COMMON // 1
        case 'Map':
        case 'Set':
        case 'WeakMap':
        case 'WeakSet':
            return TargetType.COLLECTION // 2
        default:
            return TargetType.INVALID // 0
    }
}
// ...
const targetType = getTargetType(target)
// ...
const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)

然后你就发现我们应该需要去看的是 baseHandlers 对应的 mutableHandlers

export const mutableHandlers: ProxyHandler<object> = {
    get,
    set,
    deleteProperty,
    has,
    ownKeys
}

const get = /*#__PURE__*/ createGetter()

function createGetter(isReadonly = false, shallow = false) {
    return function get(target: Target, key: string | symbol, receiver: object) {
        if (key === ReactiveFlags.IS_REACTIVE) {
            return !isReadonly
        } else if (key === ReactiveFlags.IS_READONLY) {
            return isReadonly
        } else if (
            key === ReactiveFlags.RAW &&
            receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
        ) {
            return target
        }

        const targetIsArray = isArray(target)
        if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
            return Reflect.get(arrayInstrumentations, key, receiver)
        }

        const res = Reflect.get(target, key, receiver)

        const keyIsSymbol = isSymbol(key)
        if (
            keyIsSymbol
                ? builtInSymbols.has(key as symbol)
                : key === `__proto__` || key === `__v_isRef`
        ) {
            return res
        }

        if (!isReadonly) {
            track(target, TrackOpTypes.GET, key) // <= 重点在这里
        }

        if (shallow) {
            return res
        }

        if (isRef(res)) {
            // ref unwrapping - does not apply for Array + integer key.
            const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
            return shouldUnwrap ? res.value : res
        }

        if (isObject(res)) { // 递归 reactive/readonly
            // 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 就是依赖收集的操作,看下 track 的代码(/packages/reactivity/src/effect.ts)

export function track(target: object, type: TrackOpTypes, key: unknown) {
    if (!shouldTrack || activeEffect === undefined) {
        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 = new Set()))
    }
    if (!dep.has(activeEffect)) {
        dep.add(activeEffect)
        activeEffect.deps.push(dep)
        if (__DEV__ && activeEffect.options.onTrack) {
            activeEffect.options.onTrack({
                effect: activeEffect,
                target,
                type,
                key
            })
        }
    }
}

感觉和 2 版本有些相同的地方,就是依然有 dep 关联 watcher,watcher 的 deps 下面挂着收集起来的 dep。

简单的描述一下全局变量 activeEffect,它是靠 createReactiveEffecteffect 方法(代码在 packages/reactivity/src/effect.ts里)来进行赋值操作的,比如在 mountComponent 的 setupRenderEffect,就是在 instance 的 update 方法上,挂了一个 effect(function componentEffect() {...}),这里面就会有组件的渲染逻辑。

而我们在触发 getter 的 track 过程中,等于把变量和具有重新渲染视图能力的 activeEffect 绑定在了一起,从而完成了依赖的收集,具体代码如下

export function effect<T = any>(
    fn: () => T,
    options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
    if (isEffect(fn)) {
        fn = fn.raw
    }
    const effect = createReactiveEffect(fn, options)
    if (!options.lazy) {
        effect()
    }
    return effect
}

function createReactiveEffect<T = any>(
    fn: () => T,
    options: ReactiveEffectOptions
): ReactiveEffect<T> {
    const effect = function reactiveEffect(): unknown {
        if (!effect.active) {
            return options.scheduler ? undefined : fn()
        }
        if (!effectStack.includes(effect)) {
            cleanup(effect)
            try {
                enableTracking()
                effectStack.push(effect)
                activeEffect = effect
                return fn()
            } finally {
                effectStack.pop()
                resetTracking()
                activeEffect = effectStack[effectStack.length - 1]
            }
        }
    } as ReactiveEffect
    effect.id = uid++
    effect._isEffect = true
    effect.active = true
    effect.raw = fn
    effect.deps = []
    effect.options = options
    return effect
}

派发更新

派发更新就是 vue 收集到的那些变量在变动时,如何促使视图跟着发生变化

在 2 版本,是靠在变量 set 的时候,dep 的 notify 触发 Watcher 的一系列响应

而上文已经看完了最简单的 get 相关逻辑( get 流程内的分支代码以及 watch 等的实现方式都还没去看),也就是在 reactive 执行时,会把对应的 activeEffect(内置 instance.update) 藏在 get 中等待收集。剩下就是大致看一下 set 触发后,如何调用 到 instance.update 从而更新视图的。

const set = /*#__PURE__*/ createSetter();

function createSetter(shallow = false) {
    return function set(
        target: object,
        key: string | symbol,
        value: unknown,
        receiver: object
    ): boolean {
        const oldValue = (target as any)[key]
        if (!shallow) {
            value = toRaw(value)
            if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
                oldValue.value = value
                return true
            }
        } else {
            // in shallow mode, objects are set as-is regardless of reactive or not
        }

        const hadKey =
            isArray(target) && isIntegerKey(key)
                ? Number(key) < target.length
                : hasOwn(target, key)
        const result = Reflect.set(target, key, value, receiver)
        // don't trigger if target is something up in the prototype chain of original
        if (target === toRaw(receiver)) {
            if (!hadKey) {
                trigger(target, TriggerOpTypes.ADD, key, value)
            } else if (hasChanged(value, oldValue)) {
                trigger(target, TriggerOpTypes.SET, key, value, oldValue)
            }
        }
        return result
    }
}

一眼就看到了 trigger

根据依赖收集的结果,现在需要去寻找的核心思路是,怎么在 targetMap 里找到当前 target 对应的 depsMap ,然后找到当前 key 对应的 dep,然后再在 dep(Set 结构)中读取需要执行的函数

export function trigger(
    target: object,
    type: TriggerOpTypes,
    key?: unknown,
    newValue?: unknown,
    oldValue?: unknown,
    oldTarget?: Map<unknown, unknown> | Set<unknown>
) {

    const depsMap = targetMap.get(target)
    if (!depsMap) {
        // never been tracked
        return
    }

    const effects = new Set<ReactiveEffect>()
    const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
        if (effectsToAdd) {
            effectsToAdd.forEach(effect => effects.add(effect))
        }
    }

    if (type === TriggerOpTypes.CLEAR) {
        // collection being cleared
        // trigger all effects for target
        depsMap.forEach(add)
    } else if (key === 'length' && isArray(target)) {
        depsMap.forEach((dep, key) => {
            if (key === 'length' || key >= (newValue as number)) {
                add(dep)
            }
        })
    } else {  // <= 在这里
        // schedule runs for SET | ADD | DELETE
        if (key !== void 0) {
            add(depsMap.get(key))
        }
        // also run for iteration key on ADD | DELETE | Map.SET
        const shouldTriggerIteration =
            (type === TriggerOpTypes.ADD &&
                (!isArray(target) || isIntegerKey(key))) ||
            (type === TriggerOpTypes.DELETE && !isArray(target))
        if (
            shouldTriggerIteration ||
            (type === TriggerOpTypes.SET && target instanceof Map)
        ) {
            add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
        }
        if (shouldTriggerIteration && target instanceof Map) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
        }
    }

    const run = (effect: ReactiveEffect) => {
        if (__DEV__ && effect.options.onTrigger) {
            effect.options.onTrigger({
                effect,
                target,
                key,
                type,
                newValue,
                oldValue,
                oldTarget
            })
        }
        if (effect.options.scheduler) {
            effect.options.scheduler(effect)
        } else {
            effect()
        }
    }

    effects.forEach(run)
}

这里主要就是识别一下 trigger 的 type,然后靠 add 收集 effect,最后靠 run 来执行收集起来的 effect,如果中间那一坨看着不太直观,下面是打包出来的 esm 代码

// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
    case "add" /* ADD */:
        if (!isArray(target)) {
            add(depsMap.get(ITERATE_KEY));
            if (isMap(target)) {
                add(depsMap.get(MAP_KEY_ITERATE_KEY));
            }
        }
        else if (isIntegerKey(key)) {
            // new index added to array -> length changes
            add(depsMap.get('length'));
        }
        break;
    case "delete" /* DELETE */:
        if (!isArray(target)) {
            add(depsMap.get(ITERATE_KEY));
            if (isMap(target)) {
                add(depsMap.get(MAP_KEY_ITERATE_KEY));
            }
        }
        break;
    case "set" /* SET */:
        if (isMap(target)) {
            add(depsMap.get(ITERATE_KEY));
        }
        break;
}

扩展阅读(没啥意义)

下面是扩展阅读,没啥太大意义,只是检查除此以为没有其他的依赖收集入口

前文还提到了,使用 reactive 之后,在组件初始化的时候,会走一遍(mountComponent => setupComponent => setupStatefulComponent => handleSetupResult => proxyRefs)这个流程。更直白的讲,我们这里只是在 setup,组件初始化的时候会执行 handleSetupResult 来处理我们 setup 的结果

setupStatefulComponent 中创建了一个新的 Proxy,看注释是说一个公用的实例/渲染的 proxy

但是你追进去就会发现,这个只针对 instance.ctx 的 proxy 只有在 dev 模式下,才会把 setup 内 reactive 加工的变量加到 ctx 上 (逻辑在 handleSetupResult 函数内部,exposePropsOnRenderContext 的调用)

function setupStatefulComponent(
    instance: ComponentInternalInstance,
    isSSR: boolean
) {
    const Component = instance.type as ComponentOptions

    //...

    // 0. create render proxy property access cache
    instance.accessCache = {}
    // 1. create public instance / render proxy  <= 这里看上去很重要
    // also mark it raw so it's never observed
    instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
    if (__DEV__) {
        exposePropsOnRenderContext(instance)
    }
    // 2. call setup()  <= 处理我们的 setup 函数
    const { setup } = Component
    if (setup) {
        const setupContext = (instance.setupContext =
            setup.length > 1 ? createSetupContext(instance) : null)

        currentInstance = instance
        pauseTracking()
        const setupResult = callWithErrorHandling(
            setup,
            instance,
            ErrorCodes.SETUP_FUNCTION,
            [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
        )
        resetTracking()
        currentInstance = null

        if (isPromise(setupResult)) {
            // ... 省略了,你可以全当这里也是在 handleSetupResult
        } else {
            handleSetupResult(instance, setupResult, isSSR)
        }
    } else {
        finishComponentSetup(instance, isSSR)
    }
}

handleSetupResult的代码:

export function handleSetupResult(
    instance: ComponentInternalInstance,
    setupResult: unknown,
    isSSR: boolean
) {
    if (isFunction(setupResult)) {
        // setup returned an inline render function
        instance.render = setupResult as InternalRenderFunction
    } else if (isObject(setupResult)) {
        if (__DEV__ && isVNode(setupResult)) {
            warn(
                `setup() should not return VNodes directly - ` +
                `return a render function instead.`
            )
        }
        // setup returned bindings.
        // assuming a render function compiled from template is present.
        if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            instance.devtoolsRawSetupState = setupResult
        }
        instance.setupState = proxyRefs(setupResult)
        if (__DEV__) {
            exposeSetupStateOnRenderContext(instance)
        }
    } else if (__DEV__ && setupResult !== undefined) {
        warn(
            `setup() should return an object. Received: ${setupResult === null ? 'null' : typeof setupResult
            }`
        )
    }
    finishComponentSetup(instance, isSSR)
}

// dev only
export function exposeSetupStateOnRenderContext(
    instance: ComponentInternalInstance
) {
    const { ctx, setupState } = instance
    Object.keys(toRaw(setupState)).forEach(key => {
        if (key[0] === '$' || key[0] === '_') {
            warn(
                `setup() return property ${JSON.stringify(
                    key
                )} should not start with "$" or "_" ` +
                `which are reserved prefixes for Vue internals.`
            )
            return
        }
        Object.defineProperty(ctx, key, {
            enumerable: true,
            configurable: true,
            get: () => setupState[key],
            set: NOOP
        })
    })
}

看来并没有遗漏什么重要的逻辑,所以我们就不继续追这个 proxy 了,有兴趣的同学可以继续追一下 PublicInstanceProxyHandlers 的代码,看看有没有什么神奇的地方

扩展阅读结束

下次准备看看什么是 Composition API 的,了解下热点。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,306评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,657评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,928评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,688评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,105评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,024评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,159评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,937评论 0 212
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,689评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,851评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,325评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,651评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,364评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,192评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,985评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,154评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,955评论 2 279

推荐阅读更多精彩内容