【源码】vue3 初始化流程

发布于:2025-07-06 ⋅ 阅读:(14) ⋅ 点赞:(0)

前言

准备一个demo

 <div id="app">
     {{ a }}
 </div>
 ...
 createApp({
     setup() {
         const data = reactive({
             a: 1111
         })
         return {
             ...toRefs(data)
         }
     }
 }).mount('#app')

这段代码中的 createAppmount 两个函数就可以完成 vue 的初始化流程,从这里也能看到分成了两个部分: 一个是创建 app 实例,一个是对这个实例进行挂载。那就下来一点一点的看一下。

createApp 创建实例

函数主要做了两件事

  1. 创建 app 实例
  2. 重写 mount 函数

创建 vue 实例仅用了一行代码

 const app = ensureRenderer().createApp(...args)

从字面意思可以知道在一个渲染器中有一个 createApp 函数。

那么 ensureRenderer 函数都做了什么呢?

 function ensureRenderer() {
   return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
 }

可以看到在这里用来获取 renderer,如果不存在则通过 createRenderer 创建一个。那么 createRenderer 做了什么呢?

跟踪代码最终可以看到 createRenderer 最终调用的是 baseCreateRenderer 函数。这个函数是一个很庞大的函数,因为我们的主线任务是得到一个渲染器,然后从渲染其中拿到 createApp 函数。那么我们就可以先去看一下他的返回值:

 return {
   render,
   hydrate,
   createApp: createAppAPI(render, hydrate)
 }

这里就可以清晰的看到我们调用的 createApp 函数其实调用了 createAppAPI 函数,在调用这两个函数的时候传入了 renderhydrate 两个函数。这两个函数都是在 baseCreateRenderer 函数中定义的,render 函数是用来卸载和做更新节点的;hydrate 函数表示注水,是在 SSR 的时候会用到,这里暂时不关心。

接下来就可以看一下 createAppAPI 函数了:

 export function createAppAPI<HostElement>(
 render: RootRenderFunction,
  hydrate?: RootHydrateFunction
 ): CreateAppFunction<HostElement> {
   return function createApp(rootComponent, rootProps = null) {
     const context = createAppContext()
     const installedPlugins = new Set()
     let isMounted = false
     // vue 实例
     const app: App = (context.app = {
       //...
     })
     // 将 app 实例 return
     return app
   }
 }

createAppAPI 函数接收两个参数,但是函数内部直接 returncreateApp 函数。在 createApp 函数中定义了上下文也创建了 app 实例并 return。在 app 中定义了一些我们常见的函数其中就有 mount 函数。

至此,创建 app 实例这一步我们就搞清楚了:

  1. 得到一个 renderer
  2. 从 renderer 中拿到 createApp 并调用得到 app 实例

重写 mount 函数主要是将选择器转为 DOM 节点作为容器然后再去调用原来的都 mount 函数。

接下来就来看一下 app 中定义为 mount 函数是如何进行挂载的。# 前言

准备一个demo

 <div id="app">
     {{ a }}
 </div>
 ...
 createApp({
     setup() {
         const data = reactive({
             a: 1111
         })
         return {
             ...toRefs(data)
         }
     }
 }).mount('#app')

这段代码中的 createAppmount 两个函数就可以完成 vue 的初始化流程,从这里也能看到分成了两个部分: 一个是创建 app 实例,一个是对这个实例进行挂载。那就下来一点一点的看一下。

createApp 创建实例

函数主要做了两件事

  1. 创建 app 实例
  2. 重写 mount 函数

创建 vue 实例仅用了一行代码

 const app = ensureRenderer().createApp(...args)

从字面意思可以知道在一个渲染器中有一个 createApp 函数。

那么 ensureRenderer 函数都做了什么呢?

 function ensureRenderer() {
   return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
 }

可以看到在这里用来获取 renderer,如果不存在则通过 createRenderer 创建一个。那么 createRenderer 做了什么呢?

跟踪代码最终可以看到 createRenderer 最终调用的是 baseCreateRenderer 函数。这个函数是一个很庞大的函数,因为我们的主线任务是得到一个渲染器,然后从渲染其中拿到 createApp 函数。那么我们就可以先去看一下他的返回值:

 return {
   render,
   hydrate,
   createApp: createAppAPI(render, hydrate)
 }

这里就可以清晰的看到我们调用的 createApp 函数其实调用了 createAppAPI 函数,在调用这两个函数的时候传入了 renderhydrate 两个函数。这两个函数都是在 baseCreateRenderer 函数中定义的,render 函数是用来卸载和做更新节点的;hydrate 函数表示注水,是在 SSR 的时候会用到,这里暂时不关心。

接下来就可以看一下 createAppAPI 函数了:

 export function createAppAPI<HostElement>(
 render: RootRenderFunction,
  hydrate?: RootHydrateFunction
 ): CreateAppFunction<HostElement> {
   return function createApp(rootComponent, rootProps = null) {
     const context = createAppContext()
     const installedPlugins = new Set()
     let isMounted = false
     // vue 实例
     const app: App = (context.app = {
       //...
     })
     // 将 app 实例 return
     return app
   }
 }

createAppAPI 函数接收两个参数,但是函数内部直接 returncreateApp 函数。在 createApp 函数中定义了上下文也创建了 app 实例并 return。在 app 中定义了一些我们常见的函数其中就有 mount 函数。

至此,创建 app 实例这一步我们就搞清楚了:

  1. 得到一个 renderer
  2. 从 renderer 中拿到 createApp 并调用得到 app 实例

重写 mount 函数主要是将选择器转为 DOM 节点作为容器然后再去调用原来的都 mount 函数。

接下来就来看一下 app 中定义为 mount 函数是如何进行挂载的。

mount 进行挂载

精简后的 mount 函数如下

 mount(
   rootContainer: HostElement,
   isHydrate?: boolean,
   isSVG?: boolean
 ): any {
   // 初始化流程
   if (!isMounted) {
     const vnode = createVNode(
       rootComponent as ConcreteComponent,
       rootProps
     )
 ​
     vnode.appContext = context
 ​
     render(vnode, rootContainer, isSVG)
   }
   isMounted = true
   // 根容器 #app 的所属
   app._container = rootContainer
 ​
   // 返回的是组件的一个代理,因为这个组件要变成一个可追踪的响应式对象
   return vnode.component!.proxy
 }

主要是将根据 rootComponent 创建了一个 vnode,然后调用 render 函数来进行渲染,最后会 return vnode.component.proxy

接下来看主要的 render 函数

render 函数

这里的 render 函数是在调用 createAppAPI 函数的时候传入的,也就是在 baseCreateRenderer 中定义的 render,精简后的 render:

 const render: RootRenderFunction = (vnode, container, isSVG) => {
   patch(container._vnode || null, vnode, container, null, null, null, isSVG)
   container._vnode = vnode
 }

主要是判断了走卸载逻辑还是更新逻辑,如果 vnode 不存在则是 ummount 卸载逻辑,反之是 patch 更新逻辑。

patch 函数

patch 函数的前三个参数是 n1、n2、container 分别表示oldVNodenewVNodecontainer

patch 的主要作用就是根据新的 vnode 的类型来判断是一个什么节点,然后针对不同类型的节点来调用不同的函数来比对新旧节点并渲染。

根据 demo 中的调用 createApp 函数时传入的是一个对象,所以被认为是一个组件则会调用 processComponent 来处理。

createApp({/* 这里就是 component */})

 const patch: PatchFn = (
   n1, // 旧 vnode
   n2, // 新 vnode
   container,
   anchor = null,
   parentComponent = null,
   parentSuspense = null,
   isSVG = false,
   slotScopeIds = null,
   optimized = false
 ) => {
   const { type, ref, shapeFlag } = n2
   if (shapeFlag & ShapeFlags.COMPONENT) {
     processComponent(
       n1,
       n2,
       container,
       anchor,
       parentComponent,
       parentSuspense,
       isSVG,
       slotScopeIds,
       optimized
     )
   } 
 }

processComponent 函数

在这个函数的主线就是根据是否有旧 vnode 来判断是更新还是初始化挂载,根据我们的主题,只关心 mountComponent 函数就好。

 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
   ) => {
     // 挂载流程
     mountComponent(
       n2,
       container,
       anchor,
       parentComponent,
       parentSuspense,
       isSVG,
       optimized
     )
   }

mountComponent 函数

函数中根据当前 vnode 创建了组件实例 instance,然后分别将 instance 传入 setupComponent 函数和 setupRenderEffect 函数。

setupComponent 函数主要是用来初始化当前实例上的 propsslots 并调用 setup 函数

setupRenderEffect 函数是将当前实例的渲染函数创建为响应式的

 const mountComponent: MountComponentFn = (
   initialVNode, // 初始化的 vnode / 新的 vnode, 对应的 n2
   container,
   anchor,
   parentComponent,
   parentSuspense,
   isSVG,
   optimized
 ) => {
 ​
   const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
     initialVNode, 
     parentComponent,
     parentSuspense
   ))
 ​
   setupComponent(instance)
 ​
   setupRenderEffect(
     instance,
     initialVNode,
     container,
     anchor,
     parentSuspense,
     isSVG,
     optimized
   )
 }

setupComponent 函数

函数中初始化了 propsslots,后边还调用了 setupStatefulComponent 函数,用来调用 setup 函数

 export function setupComponent(
   instance: ComponentInternalInstance,
   isSSR = false
 ) {
   const { props, children } = instance.vnode
 ​
   initProps(instance, props, isStateful, isSSR)
   initSlots(instance, children)
  
     setupStatefulComponent(instance, isSSR)
  
   return setupResult
 }

接下来看看 setupStatefulComponent 函数

setupStatefulComponent 函数

函数中的主要操作:

  • 对实例上的 ctx 进行代理并绑定到 instanceproxy 属性上

     instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
    

    对实例上的 ctx 进行代理,未来对 ctx 操作将是响应式的

    我们在使用的时候需要使用 instance.proxy 对象

    因为 instance.ctx 在 prod 和 dev 坏境下是不同的

  • 创建 setupContext 上下文

     const setupContext = (instance.setupContext = createSetupContext(instance)
    
  • 调用 setup,并在调用的时候传入 propssetupContext,这也就意味着在使用 setup 函数的时候会有这两个参数。

     const setupResult = callWithErrorHandling(
           setup,
           instance,
           ErrorCodes.SETUP_FUNCTION,
           [instance.props, setupContext]
         )
    
  • 接下来又调用了 handleSetupResult 函数对 setupResult 进行处理

handleSetupResult 函数

函数中如果 setupResultfunction 类型,则会把 setupResult 赋值到 instance.render 渲染函数来处理

如果是 object 类型,则将通过 proxyRefs 代理 setupResult 的结果赋值给 instance.setupState

 instance.setupState = proxyRefs(setupResult)

proxyRefs 的作用就是把 setupResult 对象做一层代理

方便用户直接访问 ref 类型的值

比如 setupResult 里面有个 count 是个 ref 类型的对象,用户使用的时候就可以直接使用 count 了,而不需要在 count.value

这里也就是官网里面说到的自动解构 Ref 类型

demo 中的 setupResult 是一个对象,所以会被 proxyRefs 代理。

最终还调用了 finishComponentSetup 函数。

finishComponentSetup 函数

在函数中做了两件事

  • 如果 instance 中没有渲染函数则通过编译器根据 component.template 生成一个并赋值给 instance.render
  • 兼容 2.x 中的 options API

到这一步 setupComponent 函数里的主流程就看完了,接下来可以返回去看 setupRenderEffect 函数了

setupRenderEffect 函数

这个函数中主要就是定义了 instance.update,其值就是 effect 函数的返回值,也就是一个响应式的函数 activeEffect

 const setupRenderEffect = () => {
   // create reactive effect for rendering
   instance.update = effect(
     function componentEffect() {
             // ...
     }, prodEffectOptions)
 }

在调用 effect 的时候传入了 componentEffect 函数,componentEffect 就是要被收集的依赖。

还传入了一个 prodEffectOptions,在 prodEffectOptions 中有调度器 scheduler,它的作用就是在触发依赖的时候优先使用 scheduler 来触发我们的依赖。详情可以看 Vue3 Reactive 源码学习

componentEffect 函数中进行了初始化挂载和更新时 patch。

初始化的过程是:

  1. 执行 beforeMount 钩子

     invokeArrayFns(bm)
    
  2. 获取 subTree 并递归调用 patch 来处理 subTree

     patch(
         null,
         subTree,
         container,
         anchor,
         instance,
         parentSuspense,
         isSVG
       )
    
  3. 执行 mounted 钩子

     queuePostRenderEffect(m, parentSuspense)
    

当执行完这些的时候 APP 实例就已经被成功的挂载到 DOM 上了

总结

  1. 先创建 vue 实例,然后将实例进行挂载。
  2. 在挂载过程中根据传入的组件对象创建对应的虚拟 DOM
  3. 根据虚拟 DOM 创建对应的组件实例。
  4. 然后初始化 propsslots 并调用 setup 函数。
  5. 生成渲染函数
  6. 兼容 2.x 版本的 options API 的处理
  7. 执行 beforeMount 钩子函数
  8. 获取 subTree 并调用 patch 处理
  9. 调用 mounted 钩子函数

在整个挂载流程中遵循 深度优先

参考链接

mini-vue


网站公告

今日签到

点亮在社区的每一天
去签到