面试官:vue3 初始化都干了些啥呢?

3,864 阅读9分钟

我们从下面一个最简单的例子说起

<div id="app">
    <h1>{{title}}</h1>
</div>
<script>
    const {createApp} = Vue
createApp({
    data() {
        return {
            title: 'coboy'
        }
    }
}).mount('#app')
</script>

这其中 vue3 到底都干了些什么呢?

runtime-dom中的ensureRenderer

我们查看源码可以知道 createApp 来自 runtime-dom/src/index.ts

// 用户实际调用的
export const createApp = ((...args) => {
  // 先获取渲染器
  const app = ensureRenderer().createApp(...args)

  // ... 省略

  return app
}) as CreateAppFunction<Element>

在这段源码中我们可以看到在用户实际调用的 createApp 函数中,先获取了一个渲染器,执行了渲染器函数 ensureRenderer 返回的结果当中也包含一个 createApp 函数

我们继续追踪 ensureRenderer 函数里面干了什么

function ensureRenderer() {
  // 已经存在了渲染器,就返回,否则就创建一个渲染器,那么初始化的时候,肯定走的后面部份代码
  return (
    renderer || ((renderer = createRenderer < Node), Element > rendererOptions)
  );
}

继续查看 createRenderer 函数 我们查看源码可以知道 createRenderer 来自 runtime-core/src/renderer.ts

export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer < HostNode, HostElement > options;
}

我们追查到了 baseCreateRenderer 函数,然后看看 baseCreateRenderer 函数

runtime-core中的baseCreateRenderer

我们查看源码可以知道 baseCreateRenderer 来自 runtime-core/src/renderer.ts

function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  // 代码太多,将近2000行,省略
}

然后我们可以看到这个创建渲染器的基础函数是非常多代码的,有将近 2000 行代码,是 vue3 源码库中目前最大的函数,据说当初也有人建议尤大大把这个函数拆分一下,但被尤大大拒绝了,尤大大认为没必要过度设计,这函数的职责就是做渲染器的,职责非常明确,不用再分

所以从这个故事中我们也可以得知,这个函数就是做渲染器一系列工作的,具体内容可以先不去关注,我们先只需要关注一下这个函数的返回值

function baseCreateRenderer(
    options: RendererOptions,
    createHydrationFns?: typeof createHydrationFunctions
): any {
	// 代码太多,将近2000行,省略具体细节

    // 重点函数,只要vnode转换成dom都要使用patch函数
    const patch: PatchFn = () => {}

    // ...

    // 重点函数
    const processComponent = () => {}
    // 重点函数
    const mountComponent: MountComponentFn = () => {}

    const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {}
    // 重点函数
    const setupRenderEffect: SetupRenderEffectFn = () => {}

    // ...

    const render: RootRenderFunction = (vnode, container, isSVG) => {}

    let hydrate: ReturnType<typeof createHydrationFunctions>[0] | undefined


    // 返回createApp是由createAppAPI函数的执行结果
    return {
        render, // 相当于react里面的ReactDom.render, 不同组件内部的render,此render的作用是传入vnode,转换成dom,追加到宿主
        hydrate, // 用于SSR
        createApp: createAppAPI(render, hydrate)
    }
}

然后初始化渲染器后返回一个渲染器,执行 createApp 函数

// 先获取渲染器
const app = ensureRenderer().createApp(...args);

我们从上面的创建渲染器的 baseCreateRenderer 函数返回的结果中可以知道 createApp 是由 createAppAPI 函数执行的返回结果

渲染器实例中的createAppAPI

所以我们去 createAppAPI 里面看看都干了什么 我们查看源码可以知道 baseCreateRenderer 来自 runtime-core/src/apiCreateApp.ts createAppAPI初始化的时候接收了一个render方法,返回一个createApp方法

// 此方法是createApp的工厂方法
export function createAppAPI<HostElement>(
    render: RootRenderFunction,
    hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
    // 返回App实例
    return function createApp(rootComponent, rootProps = null) {
        if (rootProps != null && !isObject(rootProps)) {
            rootProps = null
        }

        const context = createAppContext()
        const installedPlugins = new Set()

        let isMounted = false
		// 外面得到的APP实例
        const app: App = (context.app = {
            get config() {
                return context.config
            },

            set config(v) {

            },
			// 插件应用方法
            use(plugin: Plugin, ...options: any[]) {
                return app
            },
			//兼容vue2的mixin方法
            mixin(mixin: ComponentOptions) {
                return app
            },
		    // 组件方法
            component(name: string, component?: Component): any {
                return app
            },
			// 指令方法
            directive(name: string, directive?: Directive) {
                return app
            },
            // 初始化,这里最重要的就是这个mount方法
            mount(
                rootContainer: HostElement,
                isHydrate?: boolean,
                isSVG?: boolean
            ): any {
                // 初始化的时候,还买挂载,走这里
                if (!isMounted) {
                    // 构建根组件虚拟dom
                    const vnode = createVNode(
                        rootComponent as ConcreteComponent,
                        rootProps
                    )
                    vnode.appContext = context

                    if (isHydrate && hydrate) {
					// 服务端渲染走这里
                      hydrate(vnode as VNode<Node, Element>, rootContainer as any)
                    } else {
                        // spa程序走这里
                        render(vnode, rootContainer, isSVG)
                    }
                    isMounted = true
                    app._container = rootContainer
                    ;(rootContainer as any).__vue_app__ = app

                    return vnode.component!.proxy
                } else if (__DEV__) {

                }
            },

            unmount() {
                if (isMounted) {
                    render(null, app._container)
                    delete app._container.__vue_app__
                } else if (__DEV__) {
                }
            },

            provide(key, value) {
                context.provides[key as string] = value
                return app
            }
        })

        if (__COMPAT__) {
            installAppCompatProperties(app, context, render)
        }

        return app
    }
}

createApp 进行初始化传进来了一个 render(利用闭包特性,等于设置一个未来可以更新的机制),将来造一个 vnode 传给 render 并把在 mount 的时候接收到的 rootComponent 宿主元素也传进去,

重写mount方法

我们走了一遍然后又回到

export const createApp = ((...args) => {
    const app = ensureRenderer().createApp(...args)

    const {mount} = app
    // 重写mount方法,也是最外面用户调用mount方法的时候,就是调用这个函数
    app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
        // 获取模板内容
        const container = normalizeContainer(containerOrSelector)
        if (!container) return

        const component = app._component
        if (!isFunction(component) && !component.render && !component.template) {
            // 重点步骤,把模板内容赋值给component.template
            component.template = container.innerHTML
        }

       // 重点步骤,挂载前清空模板内容
        container.innerHTML = ''
        // 这里的mount方法就是createAppAPI里createApp中的mount方法
        const proxy = mount(container, false, container instanceof SVGElement)
        if (container instanceof Element) {
            container.removeAttribute('v-cloak')
            container.setAttribute('data-v-app', '')
        }
        return proxy
    }

    return app
}) as CreateAppFunction<Element>
createAppAPI 里的mount方法

利用闭包的特性我们最终执行的 mount 方法是刚刚上面的 createAppAPI 里面的 mount 方法

关键代码

// 初始化,这里最重要的就是这个mount方法
mount(
    rootContainer: HostElement,
    isHydrate?: boolean,
    isSVG?: boolean
): any {
    // 初始化的时候,还买挂载,走这里
    if (!isMounted) {
        // 创建根组件虚拟dom
        const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
        )
        vnode.appContext = context

        // 服务器端渲染
        if (isHydrate && hydrate) {
            hydrate(vnode as VNode<Node, Element>, rootContainer as any)
        } else {
            // 初始化走这里
            render(vnode, rootContainer, isSVG)
        }
        isMounted = true
        app._container = rootContainer
        ;(rootContainer as any).__vue_app__ = app


        return vnode.component!.proxy
    } else if (__DEV__) {

    }
},

mount 方法里初始化最终走的是 render 函数,这个 render 在那个 2000 行代码的渲染器里

关键代码

// 接收vnode,转为dom,并追加到宿主container
const render: RootRenderFunction = (vnode, container, isSVG) => {
  if (vnode == null) {
    if (container._vnode) {
      unmount(container._vnode, null, null, true);
    }
  } else {
    // 初始化走这里
    // 只要vnode转换成dom都要使用patch函数
    // 初始化时container._vnode为null,走初始化的逻辑,不会走diff
    patch(container._vnode || null, vnode, container, null, null, null, isSVG);
  }
  flushPostFlushCbs();
  container._vnode = vnode;
};

这个 patch 也在那个 2000 行代码的渲染器里

const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = false
) => {
    if (n1 && !isSameVNodeType(n1, n2)) {
    }

    if (n2.patchFlag === PatchFlags.BAIL) {
    }

    const {type, ref, shapeFlag} = n2
    switch (type) {
        case Text:
            break
            case Comment:
            break
            case Static:
            break
            case Fragment:
            break
            default:
            if (shapeFlag & ShapeFlags.ELEMENT) {

            } else if (shapeFlag & ShapeFlags.COMPONENT) {
                // 初始化走的是这里
                processComponent(
                    n1,
                    n2,
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    slotScopeIds,
                    optimized
                )
            } else if (shapeFlag & ShapeFlags.TELEPORT) {

            } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {

            } else if (__DEV__) {

            }
    }

为什么初始化走 else if (shapeFlag & ShapeFlags.COMPONENT) 这个分支呢

因为在 createAppAPI 里面的 mount 方法创建了根组件的虚拟 DOM,所以 ShapeFlags.COMPONENT 为真

// 初始化的时候,还未挂载,走这里
if (!isMounted) {
    // 创建根组件虚拟dom
    const vnode = createVNode(
        rootComponent as ConcreteComponent,
        rootProps
    )
    vnode.appContext = context
}

这个 processComponent 函数也在那 2000 行的渲染器函数里

    const processComponent = (
        n1: VNode | null,
        n2: VNode,
        container: RendererElement,
        anchor: RendererNode | null,
        parentComponent: ComponentInternalInstance | null,
        parentSuspense: SuspenseBoundary | null,
        isSVG: boolean,
        slotScopeIds: string[] | null,
        optimized: boolean
    ) => {
        n2.slotScopeIds = slotScopeIds
        if (n1 == null) {
            if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
                ;(parentComponent!.ctx as KeepAliveContext).activate(
                    n2,
                    container,
                    anchor,
                    isSVG,
                    optimized
                )
            } else {
                // 初始化走的是这里
                mountComponent(
                    n2,
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    optimized
                )
            }
        } else {
            updateComponent(n1, n2, optimized)
        }
    }
mountComponent,挂载组件函数

终于看到了熟悉的函数 mountComponent,挂载组件函数

const mountComponent: MountComponentFn = (
  initialVNode,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  // 1.创建组件实例
  const instance: ComponentInternalInstance = (initialVNode.component =
    createComponentInstance(initialVNode, parentComponent, parentSuspense));
  if (isKeepAlive(initialVNode)) {
  }

  // 2.组件实例安装,相当于组件初始化,相当于vue2的this._init 实例属性,方法初始化,数据响应式,两个生命周期的钩子
  // 属性声明,响应式等等
  setupComponent(instance);

  if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
  }
  // 更新机制.安装渲染函数副作用,相当于updateComponent+watcher
  setupRenderEffect(
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  );
};
setupComponent函数
export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  isInSSRComponentSetup = isSSR;

  const { props, children, shapeFlag } = instance.vnode;
  const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT;
  // 初始化Props
  initProps(instance, props, isStateful, isSSR);
  // 初始化slots
  initSlots(instance, children);
  // setup数据状态的处理
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined;
  isInSSRComponentSetup = false;
  return setupResult;
}
setup数据状态的处理 setupStatefulComponent函数
function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions
  // 此处ctx就是vue2中大家熟悉的this,组件实例
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
  // 2. call setup()
  const { setup } = Component
  // 如果设置了setup函数
  if (setup) {
    // setupContext上下文
    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
    // setup有可能返回的是一个Promise
    if (isPromise(setupResult)) {
      if (isSSR) {
      } else if (__FEATURE_SUSPENSE__) {
        instance.asyncDep = setupResult
      } else if (__DEV__) {
      }
    } else {
      handleSetupResult(instance, setupResult, isSSR)
    }
  } else {
    finishComponentSetup(instance, isSSR)
  }
}
finishComponentSetup函数

注意不管有没有设置setup函数最终都会执行finishComponentSetup函数

function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions
  if (__NODE_JS__ && isSSR) {
  } else if (!instance.render) {
    // 如果没有render函数就编译一下
    if (compile && Component.template && !Component.render) {
      Component.render = compile(Component.template, {
        isCustomElement: instance.appContext.config.isCustomElement,
        delimiters: Component.delimiters
      })
    }

    instance.render = (Component.render || NOOP) as InternalRenderFunction
    if (instance.render._rc) {
      instance.withProxy = new Proxy(
        instance.ctx,
        RuntimeCompiledPublicInstanceProxyHandlers
      )
    }
  }

  // 兼容vue2.0
  if (__FEATURE_OPTIONS_API__) {
    currentInstance = instance
    applyOptions(instance, Component)
    currentInstance = null
  }
}
applyOptions 函数,处理vue2的api
export function applyOptions(
  instance: ComponentInternalInstance,
  options: ComponentOptions,
  deferredData: DataFn[] = [],
  deferredWatch: ComponentWatchOptions[] = [],
  asMixin: boolean = false
) {
  const {
    // composition
    mixins,
    extends: extendsOptions,
    // state
    data: dataOptions,
    computed: computedOptions,
    methods,
    watch: watchOptions,
    provide: provideOptions,
    inject: injectOptions,
    // assets
    components,
    directives,
    // lifecycle 可以看到vue3的生命周期函数是没有beforeCreate、created函数的,用setup取代
    beforeMount,
    mounted,
    beforeUpdate,
    updated,
    activated,
    deactivated,
    beforeDestroy,
    beforeUnmount,
    destroyed,
    unmounted,
    render,
    renderTracked,
    renderTriggered,
    errorCaptured
  } = options

  const publicThis = instance.proxy!
  
  if (beforeMount) {
    // 我们删除了很多其他代码逻辑,单独看看这个函数,很明显我们可以beforeMount的上下文绑定是的instance.proxy
    onBeforeMount(beforeMount.bind(publicThis))
  }

}
setupRenderEffect函数

该方法会将渲染函数封装成一个副作用函数,并立即执行,执行完毕界面渲染出现,当依赖的响应式数据发生变化时,会重新执行

  const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // 类似于useEffect(fn,[])
    instance.update = effect(function componentEffect() {
      // 首次isMounted是false,走初始化流程
      if (!instance.isMounted) {
        // 首先获取组件vnode,其实就是调用组件render
        // 这次调用触发了依赖收集
        // subTree可以理解为当前组件的子树
        const subTree = (instance.subTree = renderComponentRoot(instance))

        if (el && hydrateNode) {

        } else {
          // 向下递归更新
          // 首次是完整递归创建
          patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
          )
          initialVNode.el = subTree.el
        }

        instance.isMounted = true
      } else {// 更新走这里

      }
    }
  }
总结:

vue3里没有Watcher,Dep,变得非常纯粹,就是响应式数据和它相关副作用函数的之间的关系

画一张流程图来说明vue3的初始化

Vue3-init.png

结合流程图来说就是,我们从一个全局变量 Vue 中导出了一个 createApp 的函数,然后用这个 createApp 创建了一个实例,createApp主要是先创建了一个渲染器,而渲染器是由runtime-dom里的ensureRenderer自定义web平台渲染器调用runtime-core里的baseCreateRenderer基础的渲染器创建返回一个渲染器实例,再由这个渲染器实例里的createApp方法创建一个app实例,这个app实例里有一个 mount 方法,最后使用这个 mount 方法解析转换模板内容,最后再挂载到了宿主节点上