深入 Vue3 源碼,學習初始化流程

搭建調試環境

爲了弄清楚 Vue3 的初始化,建議先克隆 Vue3 到本地。

git clone https://github.com/vuejs/vue-next.git

安裝依賴

npm install

修改 package.json,將 dev 命令加上 --sourcemap 方便調試,並運行 npm run dev

// package.json
...
"scripts": {
  "dev": "node scripts/dev.js --sourcemap",
  ...
}
...

在 packages/vue 目錄下增加 index.html,內容如下

<!-- index.html -->
<div id="app">
  {{ count }}
</div>
<script src="./dist/vue.global.js"></script>
<script>
  Vue.createApp({
    setup() {
      const count = Vue.ref(0);
      return { count };
    }
  }).mount('#app');
</script>

在瀏覽器打開 index.html,程序正常運行則可以開始進行下一步調試。

進行調試

假如在下面的流程中迷失了方向,建議先看一下結尾的總結,再回過頭來看這一段。

在 createApp 的位置打上斷點,然後刷新頁面進斷點,開始調試。

具體大家可以自行調試,我在這裏就大概描述一下初始化流程。

createApp

進入 createApp 內部,會跳轉到 packages/runtime-dom/src/index.ts 的 createApp,執行完成返回 app 實例。

// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)
  ...
  return app
}) as CreateAppFunction<Element>

接下來繼續看 ensureRenderer 的實現。

// packages/runtime-dom/src/index.ts
function ensureRenderer() {
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}

這裏的 renderer 是個單例,初始時會調用 createRenderer 創建,再繼續深入。

來到 packages/runtime-core/src/renderer.ts,可以看到 createRenderer 又會調用 baseCreateRenderer。

// packages/runtime-core/src/renderer.ts
export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

baseCreateRenderer 的實現有 2000 行,我們只需要關注幾個關鍵點就可以了。

其返回值也就是 ensureRenderer() 的返回值

// packages/runtime-core/src/renderer.ts
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
    // 此處省略 2000 行
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

接下來回到開始的位置,在執行完 ensureRenderer() 後會接着執行 createApp,這個 createApp 就是上一步返回的 createApp,再接着看看做了哪些工作。

// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)
  ...
  return app
}) as CreateAppFunction<Element>

ensureRenderer 返回的 createApp 由 createAppAPI 實現,接下來再看看 createAppAPI 是如何實現的吧。

// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      _instance: null,

      version,

      get config() {},

      set config(v) {},

      use(plugin: Plugin, ...options: any[]) {},

      mixin(mixin: ComponentOptions) {},

      component(name: string, component?: Component): any {},

      directive(name: string, directive?: Directive) {},

      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {},

      unmount() {},

      provide(key, value) {}
    })
    return app
  }
}

可以看到 createAppAPI 會返回一個 createApp 函數,也就是我們調用 createApp,當 createApp 執行完之後會返回 app 實例。app 實例還會有 use、mixin、component、directive 等方法,可以爲全局 app 添加一些擴展。

案例如下,傳入的第一個參數爲上一步的 rootComponent 也就是根組件。

// index.html
const app = Vue.createApp({})
    .use(xxx)
    .component(xxx)
    .mount(xxx)

mount

createApp 創建 app 實例後,要渲染到頁面上還需要調用 mount。接下來看看 mount 又做了什麼工作。還是在 createAppAPI 內部

// packages/runtime-core/src/apiCreateApp.ts
mount(
  rootContainer: HostElement,
  isHydrate?: boolean,
  isSVG?: boolean
): any {
  if (!isMounted) {
    const vnode = createVNode(
      rootComponent as ConcreteComponent,
      rootProps
    )
        ...
    if (isHydrate && hydrate) {
      hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    } else {
      render(vnode, rootContainer, isSVG)
    }
    isMounted = true
    app._container = rootContainer
    ...
    return vnode.component!.proxy
  } else if (__DEV__) {
    // 開發環境警告提醒,app 不可以重複掛載
  }
}

最終會執行 render(vnode, rootContainer, isSVG) 這一行代碼,接下來看看調用 createAppAPI 時傳入的 renderer。

回到 baseCreateRenderer 中,可以看到在 return 時調用 createAppAPI 傳入的 renderer。

// packages/runtime-core/src/renderer.ts
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
    // 此處省略 2000 行
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

其中 renderer 在 baseCreateRenderer 中定義了

const render: RootRenderFunction = (vnode, container, isSVG) => {
  if (vnode == null) {
    if (container._vnode) {
      unmount(container._vnode, null, null, true)
    }
  } else {
    patch(container._vnode || null, vnode, container, null, null, null, isSVG)
  }
  flushPostFlushCbs()
  container._vnode = vnode
}

在 index.html 的例子中,第一次執行 render 時 vnode 是由 rootComponent 創建出來,rootComponent 則是 createApp 時傳入的對象。

container 爲 app 容器,也就是 id 爲 app 的 div , container._vnodeundefined

所以最終會進入 patch 。

patch 的邏輯同樣位於 baseCreateRenderer 中。代碼太長了,這裏就講一下思路。在 patch 中會判斷 vnode 的 type 或 shapeFlag 執行對應的操作。

因爲第一次 patch 時,vnode 是一個組件,會進入 ShapeFlags.COMPONENT 的判斷內,執行 processComponent進行組件的處理。

然後會觸發 mountComponent 掛載組件,從而觸發 setupComponent(instance) 初始化組件的 props、slots、setup 等將需要 proxy 代理的數據做好準備,以及將 template 進行編譯爲 render。

// packages/runtime-core/src/renderer.ts
const mountComponent: MountComponentFn = (
  initialVNode,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  // 2.x compat may pre-creaate the component instance before actually
  // mounting
  const compatMountInstance =
        __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
  const instance: ComponentInternalInstance =
        compatMountInstance ||
        (initialVNode.component = createComponentInstance(
          initialVNode,
          parentComponent,
          parentSuspense
        ))
    ...
  // resolve props and slots for setup context
  if (!(__COMPAT__ && compatMountInstance)) {
    ...
    setupComponent(instance)
    ...
  }
  ...
  setupRenderEffect(
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  )
  ...
}

緊接着會執行 setupRenderEffect,該方法會將渲染函數封裝成一個副作用,當依賴的響應式數據發生變化時,會自動重新執行。

重點關注 componentUpdateFn,代碼太長了,這裏也簡單講下吧,第一次會執行 instance.isMounted 爲 undefined,則會進入創建流程,其中會執行 const subTree = (instance.subTree = renderComponentRoot(instance)) 創建子樹,然後通過 patch 遞歸創建子節點,結束後 instance.isMounted = true

一旦依賴發生變化,componentUpdateFn會被重新執行,instance.isMounted 爲 true,則會盡心更新的處理,具體就不再展開了。

至於爲什麼依賴發生變化 componentUpdateFn會被重新執行,這個我們留在下一篇文章中介紹,記得關注我。

總結

  1. 在 index.html 調用 createApp 時會先經過ensureRendererbaseCreateRenderer 生成下面的對象
// baseCreateRenderer 返回值
return {
  render,
  hydrate,
  createApp: createAppAPI(render, hydrate)
}
  1. 繼續調用 baseCreateRenderer 返回的 createApp,這裏的 createApp 實際上調用的是 createAppAPI 返回的函數。

createAppAPI執行完成返回 app 實例。

  1. index.html 在創建好 app 後接着調用 mount 進行掛載,mount 的實現在 createAppAPI 內部。

mount 執行時會調用 render函數,該 renderbaseCreateRenderer 傳入。

  1. render 則開始 patch 進行渲染,patch 內部會進行遞歸渲染子節點。

以上就是 Vue3 的 createApp 和 mount 的大致流程。至於第一步爲什麼需要經過 ensureRendererbaseCreateRenderer

baseCreateRenderer 主要是平臺無關的邏輯處理,存放在 runtime-core 中。

patch 的時候需要操作 dom,則會調用外部傳入的方法進行操作,這樣就可以更方便實現跨端。

ensureRenderer 存放在 runtime-dom 中,主要爲 baseCreateRenderer 提供一系列 dom 操作的函數。

假如我們要自定義渲染器,那麼只需要實現ensureRenderer 即可。而不是像 Vue2 需要 fork 一份,大大提高了 Vue3 的應用範圍。


好了,這篇文章就水到這裏吧。如有錯誤的地方,希望還能在評論區指出,感謝!

下篇文章將解析 Vue3 的響應式原理,如果有興趣的話別忘了關注我呀,我們一起學習、進步。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章