第二節(jié):一文帶你全面理解 vue3源碼中createApp 初始應(yīng)用的邏輯

前言:

在源碼分析概述中, 我們對源碼源碼結(jié)構(gòu)進(jìn)行了分析, 整個vue三大核心系統(tǒng), 即響應(yīng)式系統(tǒng), 渲染系統(tǒng),編譯系統(tǒng). 而響應(yīng)式系統(tǒng)渲染系統(tǒng) 又共同構(gòu)建了vue官網(wǎng)上所謂的運行時構(gòu)建

vue3 源碼中的三大系統(tǒng)有明確的分界, 但在使用時, 通常并不會單獨區(qū)分使用某一個系統(tǒng), vue3的應(yīng)用是在三個系統(tǒng)相互作用的結(jié)果, 至少是運行時, 即包括渲染系統(tǒng)響應(yīng)式系統(tǒng).
所以我的對vue3源碼分析會根據(jù)需要交替分析三大系統(tǒng)中對應(yīng)的源碼


vue3中, 我們需要使用createApp API 來創(chuàng)建應(yīng)用實例, 然后通過創(chuàng)建的實例調(diào)用mount方法將應(yīng)用掛載到DOM 節(jié)點上, 因此createApp可以理解為整個vue3 應(yīng)用的入口, 包括官網(wǎng)API都將createApp排列在第一位

所以今天這篇文章, 主要針對createApp創(chuàng)建vue3應(yīng)用的API 進(jìn)行源碼分析.

在分析之前, 我先放一張createApp初始化應(yīng)用的流程圖:
createApp初始化流程圖如下:

不知道為什么簡書一直傳不上去圖片, 如果大家想看流程圖, 可以去我的CSDN博客中看,地址給到大家
CSDN博客


如果你此時看到該流程圖是一個呆萌的狀態(tài), 可以在看完該文章后在回頭來看一下該流程圖, 你就會非常清楚的知道createApp 函數(shù)都做了什么

好, 接下來我們開始正式分析createApp創(chuàng)建應(yīng)用API的具體實現(xiàn), 那接下請大家跟著我一起探尋一下createApp API 源碼內(nèi)部邏輯, 看看源碼中都做了些什么.


1. 初始化應(yīng)用

我們使用一個具體的實例作為切入點, 通過斷點調(diào)試的方式對createApp源碼進(jìn)行分析.理解vue3初始化的過程.

實例如下:

const { createApp } = Vue

const app = createApp({
  template: '<h1>hello</h1>'
})

app.mount('#app')

這是一個最簡單的實例, 在實例中, 我們首先通過解構(gòu)的方式獲取createApp方法.
通過createApp方法創(chuàng)建應(yīng)用, 并通過mount方法將應(yīng)用掛載到DOM

接下來我們將通過這個實例分析createApp創(chuàng)建vue3應(yīng)用的API , 以及mount掛載方法.


2. 創(chuàng)建vue3應(yīng)用的源碼分析

2.1 定位 createApp 方法

我們知道vue3 源碼采用pnpm多包管理, 目前我們只知道createAppAPI 是從vue中結(jié)構(gòu)出來的, 那么該API具體來自于哪個包中, 現(xiàn)在并不知道.

因此在分析createAppAPI 前, 我們需要先確認(rèn)該API 來自于哪個包.

我們可以通過斷點調(diào)試的分時來分析, 使用單步調(diào)試, 進(jìn)入到createApp方法內(nèi)

此時就可以看到createApp方法來自于packages/runtime-dom包中


runtime是運行時.其中包括runtime-dom,runtime-core兩個包

  • runtime-dom 處理與瀏覽器相關(guān)的dom 操作的API
  • runtime-core是運行時核心代碼,與渲染平臺無關(guān)

也就意味著runtime-dom的運行依賴于runtime-core包, 所以在runtime-dom/src/index.ts模塊中一定會引入runtime-core

import { /.../ } from '@vue/runtime-core'


// 創(chuàng)建 vue 應(yīng)用API
export const createApp = ((...args) => {
  // ...
}) as CreateAppFunction<Element>


2.2 createApp 函數(shù)分析

在確定createApp 函數(shù)后, 接下來就是分析該方法的實現(xiàn)
源碼:

export const createApp = ((...args) => {
  // 1. 首先創(chuàng)建應(yīng)用
  const app = ensureRenderer().createApp(...args)

  // 2. 重寫mount 方法
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // ...后面分析
  }

  //3.返回應(yīng)用
  return app
}) as CreateAppFunction<Element>

這里我對源碼進(jìn)行了精簡, 這樣非常方便的可以看出.createApp函數(shù)的核心邏輯就是創(chuàng)建Vue3應(yīng)用對象app并返回, 至于重寫mount方法, 稍后我會帶大家具體分析



我們先看下createApp的入?yún)? 用過createAppAPI的朋友, 相信大家都知道, createApp接收兩個入?yún)?/p>

  1. 根組件: 根組件可以是單文件組件, 也可以是組件的選項對象
  2. props: 向根組件傳入的props數(shù)據(jù), 該參數(shù)為可選參數(shù).

在源碼中可以看出, createApp函數(shù)通過...args剩余運算符收集所有入?yún)? 將參數(shù)組合成為數(shù)組. 如果傳入了第二個參數(shù)props, args數(shù)組收集到的數(shù)據(jù)將會有兩項, 否則將只有一項, 即根組件對象.

所以createApp函數(shù)會接收根組件作為參數(shù), 并且返回vue應(yīng)用, 即方法內(nèi)app就是createApp創(chuàng)建的應(yīng)用對象


我相信大家已經(jīng)看出來了, createApp函數(shù)中創(chuàng)建的應(yīng)用對象app其本質(zhì)是通過ensureRenderer().createApp(...args)創(chuàng)建的

通過這個結(jié)構(gòu)不難看出: ensureRenderer()方法調(diào)用完畢后返回一個包含createApp方法的對象, 并通過該方法的調(diào)用創(chuàng)建vue應(yīng)用


2.3 ensureRenderer 確認(rèn)渲染器

接下來我們需要先確認(rèn)ensureRenderer確認(rèn)渲染器函數(shù)
源碼:

// Renderer 為Web前端渲染器 , HydrationRenderer為SSR服務(wù)器端渲染使用,
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer

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

通過ensureRenderer 函數(shù)內(nèi)部邏輯不難看出,ensureRenderer函數(shù)的作用就是用來確定渲染器對象(renderer)存在.

如果渲染器renderer不存在, 就會調(diào)用createRenderer函數(shù)創(chuàng)建渲染器, 根據(jù)前面的分析, 渲染器將是一個包含createApp方法的對象

初始創(chuàng)建vue應(yīng)用renderer值為必然為空, 會調(diào)用createRenderer函數(shù), 并傳入?yún)?shù)rendererOptions
這里rendererOptions參數(shù)是渲染器配置對象, 主要作用就是用來操作DOM的一些方法

源碼:

// 渲染器配置對象
const rendererOptions = /*#__PURE__*/ extend({ patchProp }, nodeOps)


// extend 就是合并對象的方法(shared/src/general.ts)
export const extend = Object.assign

// dom 操作(runtime-core/src/nodeOps.ts   )
const doc = (typeof document !== 'undefined' ? document : null) as Document
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
  // 插入節(jié)點
  insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null)
  },
  // 創(chuàng)建文本節(jié)點
  createText: text => doc.createTextNode(text),
    // 創(chuàng)建注釋節(jié)點
  createComment: text => doc.createComment(text),
    //...
}

這里將幾個模塊中的中的方法放在了一起, 很方便的看出, rendererOptions渲染器配置對象就是封裝了DOM操作方法的對象, 由兩部分組成:

  1. patchProp:處理元素的 props砸西、Attribute影所、classstyleevent事件等伦连。
  2. nodeOps:處理DOM節(jié)點指蚁,這個對象里面包含了各種封裝原生DOM操作的方法。

rendererOptions對象最終會傳遞到渲染器里面颈娜,里面的各種方法最終會在頁面渲染的過程中被調(diào)用剑逃。

至此我們暫時了解如下幾件事:

  1. createApp方法的作用就是創(chuàng)建Vue3應(yīng)用.
  2. createApp方法內(nèi)真正創(chuàng)建Vue3應(yīng)用的是渲染器對象的createApp方法
  3. createRenderer方法的作用是用來創(chuàng)建渲染器對象, 接收rendererOptions渲染器配置對象
  4. rendererOptions配置對象中包含DOM操作的方法

因此要創(chuàng)建Vue3 應(yīng)用必須先創(chuàng)建渲染器對象, 因此接下來我們繼續(xù)分析createRenderer方法的實現(xiàn)


2.4 createRenderer 創(chuàng)建渲染器方法

createRenderer方法來源于runtime-core/src/render.ts模塊
具體源碼實現(xiàn)如下

// 創(chuàng)建渲染器方法
function createRenderer(options) {
  return baseCreateRenderer(options)
}

// 真正創(chuàng)建渲染器函數(shù)
function baseCreateRenderer(options,createHydrationFns) {
  // ... 省略渲染方法
  
    // 渲染函數(shù)
  const render = (vnode, container, isSVG) => {
     //...
  }
  let hydrated;
  let hydrateNode;
  if (createHydrationFns) {
    ;[hydrate, hydrateNode] = createHydrationFns(internals)
  }
  
  return {
    render,
    hydrate,  
    createApp: createAppAPI(render, hydrate)
  }
}

通過源碼可以看出createRenderer函數(shù)返回baseCreateRenderer方法執(zhí)行的結(jié)果, 即真正創(chuàng)建渲染器對象的函數(shù)是baseCreateRenderer
baseCreateRenderer創(chuàng)建的渲染器對象包括render, hydrate, createApp三個方法

獲取到渲染器對象后, 就會通過渲染器對象調(diào)用createApp方法創(chuàng)建應(yīng)用. 而createApp方法的值是通過createAppAPI方法創(chuàng)建的, 同時會將render, hydrate,兩個方法作為參數(shù)傳入createAppAPI方法中.

所以接下來我們需要看一下createAppAPI方法的實現(xiàn)


2.5 createApp API 方法

createAppAPI方法來自于runtime-core/src/apiCreateApp.ts

具體實現(xiàn)如下

let uid = 0

export function createAppAPI(render,hydrate){
  
  // 返回創(chuàng)建應(yīng)用的createApp 方法(利用閉包緩存render, hydrate 參數(shù))
  return function createApp(rootComponent, rootProps = null) {
    // rootComponent 就是傳入的根組件
    // rootProps 為向根組件傳入props 數(shù)據(jù)

    // 1. 如果根組件不是函數(shù), 則進(jìn)行一次淺拷貝
    if (!isFunction(rootComponent)) {
      rootComponent = extend({}, rootComponent)
    }

    // 2.rootProps 為傳入根組件的props , 參數(shù)必須是一個對象或null
    if (rootProps != null && !isObject(rootProps)) {
      __DEV__ && warn(`root props passed to app.mount() must be an object.`)
      rootProps = null
    }

    // 3. 創(chuàng)建vue應(yīng)用實例上下文對象, 就是一些默認(rèn)配置
    const context = createAppContext()

    // 4. 初始化插件集合, 記錄通過use 創(chuàng)建的插件
    const installedPlugins = new WeakSet()

    // 5. 始化應(yīng)用掛載狀態(tài),默認(rèn)為false
    let isMounted = false

    // 6. 創(chuàng)建 vue 應(yīng)用實例對象
        // 同時將應(yīng)用實例存儲到context上下文的app屬性
    const app: App = (context.app = {
      // 初始化應(yīng)用屬性
      _uid: uid++,  // 項目中可能存在多個vue實例官辽,需使用id標(biāo)識
      _component: rootComponent as ConcreteComponent,  // 根組件
      _props: rootProps,  // 傳遞給根組件的props
      _container: null,  // 掛載點: DOM容器
      _context: context,  // app上下文
      _instance: null,   // 應(yīng)用實例

      version,   // 版本

      // 定義了一個訪問器屬性app.config蛹磺,只能讀取,不能直接替換
      get config() {return context.config},

      set config(v) {},

      // 掛載插件, 返回app對象
      use(plugin, ...options) {/*... 省略代碼*/  return app },

      // 混入
      mixin(mixin) { /*... 省略代碼*/ return app },

      // 注冊全局組件
      component(name, component) { /*... 省略代碼*/ return app },

      // 定義全局指令
      directive(name, directive) { /*... 省略代碼*/ return app },

      // 應(yīng)用掛載/卸載方法
      mount( rootContainer,isHydrate,isSVG ) {/*... 省略代碼*/},
      unmount() { /*... 省略代碼*/ },

      // 全局注入方法
      provide(key, value) {/*... 省略代碼*/ return app },

      runWithContext(fn) {/*... 省略代碼*/ }
    })

    // __COMPAT__ 判斷是否兼容vue2
    // 若開啟兼容同仆,安裝vue2相關(guān)的API
    if (__COMPAT__) {
      installAppCompatProperties(app, context, render)
    }

    // 返回 app 對象
    return app
  }
}

源碼中createAppAPI函數(shù)返回了一個函數(shù), 返回的這個函數(shù)才是真正的createApp函數(shù).

因此該返回的函數(shù)就是最后用來創(chuàng)建應(yīng)用對象的createApp方法

分析至此,我們就可以知道了

  1. createAppAPI 創(chuàng)建應(yīng)用實例對象并返回
  2. createAppAPI 中創(chuàng)建應(yīng)用對象, 是通過調(diào)用渲染器{render,hydrate,createApp}中的createApp方法創(chuàng)建的
  3. 渲染器中的createApp方法則是createAppAPI函數(shù)的返回函數(shù)

createApp創(chuàng)建的應(yīng)用對象就是包含了一些屬性和常用的use,mixins,component,directive,mount,unmount,provide方法的對象. 因此可以通過app應(yīng)用對象調(diào)用mount掛載應(yīng)用


3. mount 掛載源碼分析

在上面我們已經(jīng)分析了createApp函數(shù), 在調(diào)用完畢后返回vue應(yīng)用實例對象, 在使用時會通過應(yīng)用實例對象調(diào)用mount方法掛載應(yīng)用.

3.1 確定mount 方法

通過前面的分析,我們已經(jīng)明白, 創(chuàng)建應(yīng)用實例的createApp函數(shù)是通過調(diào)用createApp API方法返回的createApp 創(chuàng)建的應(yīng)用實例. 在此方法中可以看到生成應(yīng)用對象app具有mount方法

源碼:

export function createAppAPI(render,hydrate){
  // 創(chuàng)建vue應(yīng)用實例的方法
  return function createApp(rootComponent, rootProps = null) {
    //...

    // 應(yīng)用實例對象
    const app: App = (context.app = {
      //..
      // 應(yīng)用實例對象掛載mount 方法
      mount(rootContainer,isHydrate,isSVG) {
         // ...
      },
      //...
    })

   //..

    // 返回應(yīng)用實例對象
    return app
  }
}

但我們調(diào)用app.mount()掛載應(yīng)用時并不是調(diào)用該mount 方法, 原因在于createApp方法中對當(dāng)前mount方法進(jìn)行了重寫

export const createApp = ((...args) => {
  // 1. 首先創(chuàng)建應(yīng)用
  const app = ensureRenderer().createApp(...args)

  // 2. 重寫mount 方法
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // ...后面分析
  }

  //3.返回應(yīng)用
  return app
}) as CreateAppFunction<Element>

源碼很清楚的表明,createApp函數(shù)中創(chuàng)建完應(yīng)用實例對象, 接著通過解構(gòu)的方式將應(yīng)用實例對象appmount方法記錄在變量mount

然后重寫app對象的mount方法進(jìn)行重寫, 賦值了一個新的方法

也就是在在我們通過app.mount()掛載應(yīng)用時, 進(jìn)入的是createApp函數(shù)中對app應(yīng)用對象重寫的mount方法

3.2 mount 掛載

在實例中,我們調(diào)用mount方法進(jìn)行掛載時,傳入了一個字符串#app , 以此獲取到掛載點DOM元素

app.mount('#app')

mount具體是如何掛載的呢?, 我們首先分析一下createApp函數(shù)中重寫的mount方法

重寫的mount方法

export const createApp = ((...args) => {
  // 創(chuàng)建應(yīng)用實例對象
  const app = ensureRenderer().createApp(...args)

  // 重寫mount 方法
  // 備份mount 方法
  const { mount } = app

  // 重寫mount 方法
  app.mount = (containerOrSelector: Element | ShadowRoot | string) => {
    // 1. 獲取掛載容器(dom元素)
    const container = normalizeContainer(containerOrSelector)
    // 沒有容器直接return 無法掛載應(yīng)用
    if (!container) return

    // 2. 處理根組件
    // 通過應(yīng)用對象獲取根組件, 即createApp() 方法第一個參數(shù)
    const component = app._component

    // 驗證根組件是一個對象,且不具有render, template屬性, 則使用掛載點內(nèi)容作為模板 
    if (!isFunction(component) && !component.render && !component.template) {
      // __UNSAFE__
      // Reason: potential execution of JS expressions in in-DOM template.
      // The user must make sure the in-DOM template is trusted. If it's
      // rendered by the server, the template should not contain any user data.
      
      // 需要確保掛載點模板是可信的, 因為模板中可能存在JS表達(dá)式
      component.template = container.innerHTML
      
    }

    // 清空掛載點
    // clear content before mounting
    container.innerHTML = ''

    // 3. 調(diào)用 mount 真正的掛載
    // 調(diào)用從應(yīng)用實例上備份的mount 進(jìn)行掛載
    const proxy = mount(container, false, container instanceof SVGElement)

    // 掛載完成后, 清理v-clock指令, 為容器添加data-v-app 標(biāo)識
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }

    // 返回根組件實例對象(注意不是app應(yīng)用實例對象)
    return proxy
  }

  // 返回應(yīng)用對象
  return app
}) as CreateAppFunction<Element>

可以看到重寫的mount方法通過調(diào)用normalizeContainer獲取掛載點容器, 其具體實現(xiàn)如下:

源碼:

// 獲取掛載點dom 容器
function normalizeContainer(
  container: Element | ShadowRoot | string
): Element | null {

  // 1. 參數(shù)為字符串, 則通過querySelector 獲取dom 元素
  if (isString(container)) {
    const res = document.querySelector(container)
    return res
  }

  // 2. 參數(shù)不是字符串, 則直接返回
  return container as any
}

在調(diào)用mount方法掛載時, 參數(shù)可以是字符串, 也可以是真實的DOM元素.因此在該方法中針對參數(shù)進(jìn)行判斷,

  • 如果參數(shù)是字符串, 則認(rèn)為是選擇器, 通過querySelector獲取DOM元素
  • 如果參數(shù)不是字符串, 則認(rèn)為參數(shù)是真實的DOM 元素, 直接返回

上面重寫的mount方法, 其實最主要的就是做了三件事

  1. 獲取掛載點容器, 通過normalizeContainer方法獲取
  2. 處理根組件對象, 根組件對象如果不具有渲染函數(shù)render方法和模板template, 則使用掛載點元素內(nèi)容作為template模板
  3. 調(diào)用應(yīng)用對象app原本的mount方法掛載根組件, 掛載完成后為容器添加特定的屬性

3.3 應(yīng)用對象本身的mount 方法

在重寫mount方法中最終會調(diào)用應(yīng)用對象app本身的mount方法進(jìn)行正式的掛載,

mount方法接收三個參數(shù)

  1. 掛載點容器: container
  2. 是否為SSR: 傳入固定值false
  3. 是否為SVG元素

mount方法具體實現(xiàn)如下

let isMounted = false

const app = {
  //...

  // app 掛載方法
  mount(rootContainer: HostElement,isHydrate?: boolean,isSVG?: boolean) {
    // 判斷是否已經(jīng)掛載 
    if (!isMounted) {
      // #5571

      // 如果掛載容器具有__vue_app__, 表示當(dāng)前容器已就作為其他應(yīng)用掛載容器
      if (__DEV__ && (rootContainer as any).__vue_app__) {
        warn(
          `There is already an app instance mounted on the host container.\n` +
            ` If you want to mount another app on the same host container,` +
            ` you need to unmount the previous app by calling \`app.unmount()\` first.`
        )
      }

      // 1. 調(diào)用createVNode 創(chuàng)建根組件的VNode
      // rootComponent, rootProps 是createApp 調(diào)用時傳入的參數(shù), 根組件對象與props
      const vnode = createVNode(rootComponent, rootProps)
      
      // store app context on the root VNode.
      // this will be set on the root instance on initial mount.
      // 在根VNode上綁定應(yīng)用上下文對象,在掛載完畢后綁定到根組件實例對象上
      vnode.appContext = context

      // HMR root reload
      // 熱更新
      if (__DEV__) {
        context.reload = () => {
          render(cloneVNode(vnode), rootContainer, isSVG)
        }
      }

      // 2. 渲染 VNode
      // 其他渲染方式
      if (isHydrate && hydrate) {
        hydrate(vnode as VNode<Node, Element>, rootContainer as any)
      } else {
        // 瀏覽器調(diào)用render 渲染根VNode 
        // render 函數(shù)在創(chuàng)建創(chuàng)建渲染函數(shù)中定義, 傳遞到createAppAPI, 通過閉包緩存
        render(vnode, rootContainer, isSVG)
      }

      // 掛載完畢后, isMounted 狀態(tài)設(shè)置為true
      isMounted = true

      // 應(yīng)用實例上綁定掛載容器
      app._container = rootContainer
        
      // for devtools and telemetry
      // 將app 應(yīng)用實例對象綁定到容器上, 因此可以通過容器訪問app 實例
      ;(rootContainer as any).__vue_app__ = app

      // 開發(fā)環(huán)境調(diào)試
      if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
        app._instance = vnode.component
        devtoolsInitApp(app, version)
      }

      
      return getExposeProxy(vnode.component!) || vnode.component!.proxy
    } else if (__DEV__) {
      warn(
        `App has already been mounted.\n` +
          `If you want to remount the same app, move your app creation logic ` +
          `into a factory function and create fresh app instances for each ` +
          `mount - e.g. \`const createMyApp = () => createApp(App)\``
      )
    }
  },
}

通過上面的分析, 在掛載應(yīng)用實例時, 去除邊緣判斷,核心邏輯就以下兩點

  1. 通過調(diào)用createVNode創(chuàng)建虛擬節(jié)點, 參數(shù)為根組件對象和向根組件傳入的props 對象
  2. 通過調(diào)用rendder方法渲染虛擬節(jié)點

接下來我們分別分析創(chuàng)建虛擬節(jié)點和渲染虛擬節(jié)點邏輯


3.4 createVNode 創(chuàng)建VNode 函數(shù)

虛擬節(jié)點的本質(zhì)就是一個JS對象, 通過屬性描述一個DOM 節(jié)點, 包括tag, props,children

具體看一下createVNode函數(shù)的實現(xiàn)

// 創(chuàng)建createVNode 函數(shù)
export const createVNode = ( __DEV__ ? createVNodeWithArgsTransform : _createVNode )

// 創(chuàng)建具有參數(shù)轉(zhuǎn)換的VNode 函數(shù)
const createVNodeWithArgsTransform = (...args) => {
  
  // 調(diào)用_createVNode 創(chuàng)建VNode
  return _createVNode(
    ...(vnodeArgsTransformer
      ? vnodeArgsTransformer(args, currentRenderingInstance)
      : args)
  )
}


通過代碼不難看出, 真正來創(chuàng)建虛擬節(jié)點的函數(shù)是_createVNode.
開發(fā)環(huán)境調(diào)用createVNodeWithArgsTransform也只不過根據(jù)vnodeArgsTransformer函數(shù)是否存在來轉(zhuǎn)換一下參數(shù), 最終函數(shù)返回_createVNode調(diào)用的結(jié)果

因此我們將通過_createVNode函數(shù)分析創(chuàng)建的虛擬節(jié)點(vnode)
_createVNode的具體實現(xiàn)

// _createVNode 第一個參數(shù)是必傳的, 后面參數(shù)都具有默認(rèn)值
function _createVNode(
  type,    // 創(chuàng)建VNode的類型
  props = null,
  children = null,
  patchFlag = 0,
  dynamicProps = null,
  isBlockNode = false
) {
  // type 不存在  或 type 值為 Symbol(v-ndc)
  // 則提示type 無效, 并將type 認(rèn)定為 Comment 注釋類型
  if (!type || type === NULL_DYNAMIC_COMPONENT) {
    if (__DEV__ && !type) {
      warn(`Invalid vnode type when creating vnode: ${type}.`)
    }
    type = Comment
  }

  // 判斷type 是否已經(jīng)是一個VNode 
  if (isVNode(type)) {
   //...
  }

  // 調(diào)用isClassComponent 判斷是否為有狀態(tài)的組件
  // class component normalization.
  if (isClassComponent(type)) {
    type = type.__vccOpts
  }

  // 2.x async/functional component compat
  if (__COMPAT__) {
    type = convertLegacyComponent(type, currentRenderingInstance)
  }

  // props 參數(shù)存在, 則規(guī)范處理class, style
  // class & style normalization.
  if (props) {
   //...
  }

  // 判斷VNode 標(biāo)識, 采用二進(jìn)制表示, 示例代碼標(biāo)識為 100 = 4
  // encode the vnode type information into a bitmap
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT
    : __FEATURE_SUSPENSE__ && isSuspense(type)
      ? ShapeFlags.SUSPENSE
      : isTeleport(type)
        ? ShapeFlags.TELEPORT
        : isObject(type)
          ? ShapeFlags.STATEFUL_COMPONENT
          : isFunction(type)
            ? ShapeFlags.FUNCTIONAL_COMPONENT
            : 0

  // 判斷節(jié)點狀態(tài)
  if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) {
    //...
  }

  // 返回createBaseVNode 創(chuàng)建的VNode
  return createBaseVNode(
    type,
    props,
    children,
    patchFlag,
    dynamicProps,
    shapeFlag,
    isBlockNode,
    true
  )
}

從源碼中可以看出,_createVNode函數(shù)只是對需要創(chuàng)建虛擬節(jié)點的參數(shù)進(jìn)行各種判斷檢查, 規(guī)范props數(shù)據(jù), 以及創(chuàng)建VNode 參數(shù)的節(jié)點標(biāo)識
最后返回createBaseVNode函數(shù)創(chuàng)建的節(jié)點

createBaseVNode函數(shù)的實現(xiàn)如下:

// 創(chuàng)建VNode
function createBaseVNode(
  type,
  props = null,
  children = null,
  patchFlag = 0,
  dynamicProps = null,
  shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
  isBlockNode = false,
  needFullChildrenNormalization = false
) {
  // 虛擬節(jié)點對象 VNode
  const 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,
    ctx: currentRenderingInstance
  } as VNode

  // ...

  return vnode
}

可以看出createBaseVNode函數(shù)最大的作用就是創(chuàng)建VNode, 并返回該虛擬節(jié)點, 即JavaScript 對象


3.5 render 渲染VNode

在創(chuàng)建完虛擬DOM 后, 就會調(diào)用render方法渲染虛擬節(jié)點
render函數(shù)是在講createRenderer的時候出現(xiàn)的萤捆,是在baseCreateRenderer中定義的

具體源碼如下

function baseCreateRenderer(options,createHydrationFns) {
    //...
  // render 渲染VNode 函數(shù)
  const render = (vnode, container, isSVG) => {
    // 如果VNode 不存在, 有可能之前已經(jīng)掛載, 那么將會執(zhí)行卸載操作
    if (vnode == null) {
      // container._vnode 表示上一次渲染的VNode, 存在則會執(zhí)行卸載操作
      if (container._vnode) {
        unmount(container._vnode, null, null, true  // 卸載VNode
      }
    } else {
      // 如果VNode 存在, 則調(diào)用patch 方法, 判斷處理除此渲染或更新渲染
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    

    // 刷新任務(wù)隊列, 暫不分析
    flushPreFlushCbs()
    flushPostFlushCbs()

    // 渲染完畢后, 容器記錄渲染的VNode
    container._vnode = vnode
  }

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


render 渲染函數(shù)的主要作用就是分發(fā)

  1. 渲染的vnode為空, 則判斷之前已渲染的情況下,則調(diào)用unmount進(jìn)行卸載操作
  2. 渲染的vnode不為空, 則調(diào)用patch函數(shù), 根據(jù)情況執(zhí)行初始渲染或更新渲染操作

4. 初始化應(yīng)用總結(jié)

分析到這里,以上的內(nèi)容就是createApp源碼的基本內(nèi)容俗批。主要作用就是確定渲染器俗或,創(chuàng)建一個Vue應(yīng)用實例,最后進(jìn)行掛載岁忘,掛載之后具體渲染邏輯,后面分析

接下來對本節(jié)初始化應(yīng)用進(jìn)行總結(jié),主要有兩部分內(nèi)容: 創(chuàng)建應(yīng)用實例對象掛載應(yīng)用

4.1 創(chuàng)建應(yīng)用實例對象

創(chuàng)建應(yīng)用實例對象步驟:

  1. 調(diào)用createApp函數(shù)創(chuàng)建應(yīng)用實例
  2. createApp函數(shù)中通過createRenderer函數(shù)創(chuàng)建渲染器
  3. createRenderer函數(shù)返回渲染器對象(renderer): {render, createApp }
  4. 調(diào)用渲染器中的createApp方法創(chuàng)建應(yīng)用實例對象
  5. createApp函數(shù)中重寫mount掛載方法,并返回應(yīng)用實例對象

4.2 掛載應(yīng)用

調(diào)用應(yīng)用實例對象的mount方法進(jìn)行掛載

  1. 首先在重寫的mount方法中處理獲取掛載容器的DOM元素, 以及渲染根組件的模板
  2. 其后調(diào)用應(yīng)用實例對象本身的mount方法
  3. mount掛載方法中根據(jù)根組件調(diào)用createVNode函數(shù)創(chuàng)建vnode
  4. 之后在調(diào)用渲染器中的render渲染函數(shù), 渲染根組件生成的vnode

簡單來說初始化應(yīng)用就是做了如下幾件事

  1. 創(chuàng)建Vue應(yīng)用實例對象辛慰。
  2. 確定應(yīng)用掛載容器節(jié)點。
  3. 創(chuàng)建根組件vnode對象
  4. 執(zhí)行render渲染根組件vnode干像。

至此, 整個createApp初始化應(yīng)用我就帶大家分析完了, 你可以回頭去前言在看一眼createApp應(yīng)用的流程圖.

最后: 如果覺得這篇文章對你有幫助, 點贊關(guān)注不迷路, 你的支持是我的動力!
[圖片上傳失敗...(image-45b9f3-1715600214328)]

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末帅腌,一起剝皮案震驚了整個濱河市驰弄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌速客,老刑警劉巖戚篙,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異溺职,居然都是意外死亡岔擂,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門浪耘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乱灵,“玉大人,你說我怎么就攤上這事点待±龋” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵癞埠,是天一觀的道長状原。 經(jīng)常有香客問我,道長苗踪,這世上最難降的妖魔是什么颠区? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮通铲,結(jié)果婚禮上毕莱,老公的妹妹穿的比我還像新娘。我一直安慰自己颅夺,他們只是感情好朋截,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著吧黄,像睡著了一般部服。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拗慨,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天廓八,我揣著相機(jī)與錄音,去河邊找鬼赵抢。 笑死剧蹂,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的烦却。 我是一名探鬼主播宠叼,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼短绸!你這毒婦竟也來了车吹?” 一聲冷哼從身側(cè)響起筹裕,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤醋闭,失蹤者是張志新(化名)和其女友劉穎窄驹,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體证逻,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡乐埠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了囚企。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丈咐。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖龙宏,靈堂內(nèi)的尸體忽然破棺而出棵逊,到底是詐尸還是另有隱情,我是刑警寧澤银酗,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布辆影,位于F島的核電站,受9級特大地震影響黍特,放射性物質(zhì)發(fā)生泄漏蛙讥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一膨疏、第九天 我趴在偏房一處隱蔽的房頂上張望胚鸯。 院中可真熱鬧倒淫,春花似錦、人聲如沸迫像。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽闻妓。三九已至,卻和暖如春傅蹂,著一層夾襖步出監(jiān)牢的瞬間纷闺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工份蝴, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留犁功,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓婚夫,卻偏偏與公主長得像浸卦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子案糙,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容