Vue3.0 - 02 - 03

124 阅读4分钟

回顾

上面介绍完 mountComponent 函数的,创建组实例和初始化组件两个关键步骤,接下来分析组件渲染 setupRenderEffect 函数。

setupRenderEffect


const setupRenderEffect: SetupRenderEffectFn = (

    instance,

    initialVNode,

    container,

    anchor,

    parentSuspense,

    isSVG,

    optimized

) => {

    const componentUpdateFn = () => {

        /* 组件渲染相关逻辑 */

    }

    // 创建渲染 reactive effect

    const effect = (instance.effect = new ReactiveEffect(
        componentUpdateFn, // 渲染逻辑

        () => queueJob(instance.update),

        instance.scope // track it in component's effect scope

    ))

    // 初始化渲染函数 update

    const update = (instance.update = effect.run.bind(effect) as SchedulerJob)

    // 确认渲染函数 id = 组件实例 uid

    update.id = instance.uid

    // ...

    // 执行渲染函数

    update()

}

上述代码涉及到了 vue3 的响应模式 effect,这个后续分析。

先看下渲染相关的函数 componentUpdateFn

componentUpdateFn


const componentUpdateFn = () => {

    if (!instance.isMounted) {

        // 未被渲染执行下面代码

        // ...

        if (el && hydrateNode) {

            // ... ssr

        } else {

            // render start

            // ...

            const subTree = (instance.subTree = renderComponentRoot(instance))

            // ...

            patch(

                null,

                subTree,

                container,

                anchor,

                instance,

                parentSuspense,

                isSVG

            )

            // ...

            initialVNode.el = subTree.el

        }

        // ...

    } else {

        // 组件更新执行下面代码

        // ...

    }

}

此函数组要是执行了两个函数 renderComponentRootpatch,首先,看看 renderComponentRoot 函数,入参是组件实例(instance)

renderComponentRoot


export function renderComponentRoot(

    instance: ComponentInternalInstance

): VNode {

    const {

        type: Component,

        vnode,

        proxy,

        withProxy,

        props,

        propsOptions: [propsOptions],

        slots,

        attrs,

        emit,

        render,

        renderCache,

        data,

        setupState,

        ctx,

        inheritAttrs

    } = instance

    let result

    let fallthroughAttrs

    // ...

    try {

        if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {

            const proxyToUse = withProxy || proxy

            result = normalizeVNode(

                render!.call(

                    proxyToUse,

                    proxyToUse!,

                    renderCache,

                    props,

                    setupState,

                    data,

                    ctx

                )

            )

            fallthroughAttrs = attrs

        } else {

            // ...

        }

    } catch (err) {

    // ...

    }

    // ...

    return result

}

首先,执行了 render!.call(proxyToUse, ...) 函数,简单回顾下 render 函数。


function render(_ctx/* withProxy */, _cache/* [] */) {

    with (_ctx) {

        const { toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

        return (_openBlock(), _createElementBlock("div", null, _toDisplayString(msg), 1 /* TEXT */))

    }

}

此函数,执行了两个函数 openBlockcreateElementBlock,其中 openBlock 函数,用来生成存放子 vnode 对象的数组,代码如下:


export const blockStack: (VNode[] | null)[] = []

let currentBlock: VNode[] | null = null

// openBlock 函数定义

export function openBlock(disableTracking = false) {

    blockStack.push((currentBlock = disableTracking ? null : []))

}

剩下的 createElementBlock 函数,用来生成 vnode,其中,需要注意的是 _toDisplayString(msg) 中的 msg,会触发 render 函数中,_ctx(withProxy)的 has 拦截。


get(target: ComponentRenderContext, key: string) {

    // 判断属性名是否等于 Symbol.unscopables

    // 此时 key = msg

    if ((key as any) === Symbol.unscopables) {

        return

    }

    // 因此执行 PublicInstanceProxyHandlers.get 函数

    return PublicInstanceProxyHandlers.get!(target, key, target)

},

has(_: ComponentRenderContext, key: string) {

    // 判断属性名称 key 值不是以 '_' 开头的,并且不是特定的一些字符串,类似 Object、Boolean 等

    const has = key[0] !== '_' && !isGloballyWhitelisted(key)

    // ...

    return has

}

因为,return has = true,所以,执行 \_ctx.msg,触发 get 拦截,执行 PublicInstanceProxyHandlers.get 函数


// PublicInstanceProxyHandlers内部get钩子函数的具体实现

get({ _: instance }: ComponentRenderContext, key: string) {

    // ...

    let normalizedProps

    // 判断属性名 key(msg) 不是以'$'开头

    if (key[0] !== '$') {

        // 不存在于 instance 的 accessCache 缓存对象中

        const n = accessCache![key]

        // ...

    } else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {

            accessCache![key] = AccessTypes.SETUP // 0

            // setupState 对象是 setup 函数返回值的 Proxy 对象,所以执行 setupState[message] 时会触发 get 钩子函数

            return setupState[key]

        }

    // ...

    }

}


export function unref<T>(ref: T): T extends Ref<infer V> ? V : T {

    return isRef(ref) ? (ref.value as any) : ref

}

const shallowUnwrapHandlers: ProxyHandler<any> = {

    get: (target, key, receiver) => unref(Reflect.get(target, key, receiver))

}

这里涉及双向数据绑定逻辑,不细说,以后会详细分析。总之,返回具体值('Hello World')

执行 _createElementBlock("div", null, _toDisplayString(msg), 1 /* TEXT */) 等价于 createElementBlock("div", null, 'Hello World', 1 /* TEXT */)


export function createElementBlock(

    type: string | typeof Fragment,

    props?: Record<string, any> | null,

    children?: any,

    patchFlag?: number,

    dynamicProps?: string[],

    shapeFlag?: number

) {

    return setupBlock(

        // createBaseVNode 会将 vnode push 进 currentBlock 中,如上文中 openBlock 函数所说

        // createBaseVNode return vnode 结构如下

        /*

        __v_isVNode: true,

        __v_skip: true,

        type,

        props,

        key: props && normalizeKey(props),

        ref: props && normalizeRef(props),

        scopeId: currentScopeId,

        slotScopeIds: null,

        children,

        component: null,

        suspense: null,

        ssContent: null,

        ssFallback: null,

        dirs: null,

        transition: null,

        el: null,

        anchor: null,

        target: null,

        targetAnchor: null,

        staticCount: 0,

        shapeFlag,

        patchFlag,

        dynamicProps,

        dynamicChildren: null,

        appContext: null

        */

        createBaseVNode(

            type, // div

            props, // null

            children, // Hello World

            patchFlag, // 1

            dynamicProps,

            shapeFlag,

            true /* isBlock */

        )

    )

}

回到最开始 const subTree = (instance.subTree = renderComponentRoot(instance)) 中,renderComponentRoot() 函数,做了一下几件事:

  1. 执行 instance.render,返回组件 vnode;

  2. 标准化 vnode,normalizeVNode(vnode),返回 vnode。

patch


patch(

    null,

    subTree, // 组件 vnode

    container, // querySelector('#app')

    anchor, // null

    instance, // 组件实例

    parentSuspense,

    isSVG

)

接着执行 patch 函数


const patch: PatchFn = (

    n1,

    n2,

    container,

    anchor = null,

    parentComponent = null,

    parentSuspense = null,

    isSVG = false,

    slotScopeIds = null,

    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren

) => {

    // ...

    const { type, ref, shapeFlag } = n2

    switch (type) {

        // ...

        default:

            if (shapeFlag & ShapeFlags.ELEMENT) {

                processElement(

                    n1,

                    n2,

                    container,

                    anchor,

                    parentComponent,

                    parentSuspense,

                    isSVG,

                    slotScopeIds,

                    optimized

                )

            }

        // ...

    }

    // ..

}

这次执行 patch 函数会执行 processElement 函数

processElement


const processElement = (

    n1: VNode | null,

    n2: VNode,

    container: RendererElement,

    anchor: RendererNode | null,

    parentComponent: ComponentInternalInstance | null,

    parentSuspense: SuspenseBoundary | null,

    isSVG: boolean,

    slotScopeIds: string[] | null,

    optimized: boolean

) => {

    isSVG = isSVG || (n2.type as string) === 'svg'

    if (n1 == null) {

        // 挂载 element

        mountElement(

            n2,

            container,

            anchor,

            parentComponent,

            parentSuspense,

            isSVG,

            slotScopeIds,

            optimized

        )

    } else {

        // 和更新有关,这个函数后,可能会执行 diff 算法

        patchElement(

            n1,

            n2,

            parentComponent,

            parentSuspense,

            isSVG,

            slotScopeIds,

            optimized

        )

    }

}

上面的代码对于第一次渲染来说,会执行 mountElement 函数

mountElement


const mountElement = (

    vnode: VNode,

    container: RendererElement,

    anchor: RendererNode | null,

    parentComponent: ComponentInternalInstance | null,

    parentSuspense: SuspenseBoundary | null,

    isSVG: boolean,

    slotScopeIds: string[] | null,

    optimized: boolean

) => {

    let el: RendererElement

    let vnodeHook: VNodeHook | undefined | null

    const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode

    if (

        !__DEV__ &&

        vnode.el &&

        hostCloneNode !== undefined &&

        patchFlag === PatchFlags.HOISTED

    ) {

        // ...

    } else {

        // document.createElement / document.createElementNS

        el = vnode.el = hostCreateElement(

            vnode.type as string,

            isSVG,

            props && props.is,

            props

        )

        if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {

            // el.textContent = text

            hostSetElementText(el, vnode.children as string)

        } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {

            // ...

        }

            // ...

        }

        // ...

        // parent.insertBefore(child, anchor || null)

        // parent = querySelector('#app')

        hostInsert(el, container, anchor)

        // ...

}

到这里首次渲染就完成了