深入 Vue3 源碼,學(xué)習(xí)初始化流程

搭建調(diào)試環(huán)境

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

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

安裝依賴

npm install

修改 package.json,將 dev 命令加上 --sourcemap 方便調(diào)試,并運(yùn)行 npm run dev

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

在 packages/vue 目錄下增加 index.html,內(nèi)容如下

<!-- 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,程序正常運(yùn)行則可以開始進(jìn)行下一步調(diào)試。

進(jìn)行調(diào)試

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

調(diào)試

在 createApp 的位置打上斷點,然后刷新頁面進(jìn)斷點,開始調(diào)試。

具體大家可以自行調(diào)試,我在這里就大概描述一下初始化流程。

createApp

進(jìn)入 createApp 內(nèi)部,會跳轉(zhuǎn)到 packages/runtime-dom/src/index.ts 的 createApp,執(zhí)行完成返回 app 實例。

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

接下來繼續(xù)看 ensureRenderer 的實現(xiàn)。

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

這里的 renderer 是個單例,初始時會調(diào)用 createRenderer 創(chuàng)建,再繼續(xù)深入。

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

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

baseCreateRenderer 的實現(xiàn)有 2000 行,我們只需要關(guān)注幾個關(guān)鍵點就可以了。

image

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

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

接下來回到開始的位置,在執(zhí)行完 ensureRenderer() 后會接著執(zhí)行 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 實現(xiàn),接下來再看看 createAppAPI 是如何實現(xiàn)的吧。

// 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 函數(shù),也就是我們調(diào)用 createApp,當(dāng) createApp 執(zhí)行完之后會返回 app 實例。app 實例還會有 use、mixin、component、directive 等方法,可以為全局 app 添加一些擴(kuò)展。

案例如下,傳入的第一個參數(shù)為上一步的 rootComponent 也就是根組件。

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

mount

createApp 創(chuàng)建 app 實例后,要渲染到頁面上還需要調(diào)用 mount。接下來看看 mount 又做了什么工作。還是在 createAppAPI 內(nèi)部

// 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__) {
    // 開發(fā)環(huán)境警告提醒,app 不可以重復(fù)掛載
  }
}

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

回到 baseCreateRenderer 中,可以看到在 return 時調(diào)用 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 的例子中,第一次執(zhí)行 render 時 vnode 是由 rootComponent 創(chuàng)建出來,rootComponent 則是 createApp 時傳入的對象。

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

所以最終會進(jìn)入 patch 。

patch 的邏輯同樣位于 baseCreateRenderer 中。代碼太長了,這里就講一下思路。在 patch 中會判斷 vnode 的 type 或 shapeFlag 執(zhí)行對應(yīng)的操作。

image

因為第一次 patch 時,vnode 是一個組件,會進(jìn)入 ShapeFlags.COMPONENT 的判斷內(nèi),執(zhí)行 processComponent進(jìn)行組件的處理。

然后會觸發(fā) mountComponent 掛載組件,從而觸發(fā) setupComponent(instance) 初始化組件的 props、slots、setup 等將需要 proxy 代理的數(shù)據(jù)做好準(zhǔn)備,以及將 template 進(jìn)行編譯為 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
  )
  ...
}

緊接著會執(zhí)行 setupRenderEffect,該方法會將渲染函數(shù)封裝成一個副作用,當(dāng)依賴的響應(yīng)式數(shù)據(jù)發(fā)生變化時,會自動重新執(zhí)行。

重點關(guān)注 componentUpdateFn,代碼太長了,這里也簡單講下吧,第一次會執(zhí)行 instance.isMounted 為 undefined,則會進(jìn)入創(chuàng)建流程,其中會執(zhí)行 const subTree = (instance.subTree = renderComponentRoot(instance)) 創(chuàng)建子樹,然后通過 patch 遞歸創(chuàng)建子節(jié)點,結(jié)束后 instance.isMounted = true。

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

至于為什么依賴發(fā)生變化 componentUpdateFn會被重新執(zhí)行,這個我們留在下一篇文章中介紹,記得關(guān)注我。

總結(jié)

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

createAppAPI執(zhí)行完成返回 app 實例。

  1. index.html 在創(chuàng)建好 app 后接著調(diào)用 mount 進(jìn)行掛載,mount 的實現(xiàn)在 createAppAPI 內(nèi)部。

mount 執(zhí)行時會調(diào)用 render函數(shù),該 renderbaseCreateRenderer 傳入。

  1. render 則開始 patch 進(jìn)行渲染,patch 內(nèi)部會進(jìn)行遞歸渲染子節(jié)點。

以上就是 Vue3 的 createApp 和 mount 的大致流程。至于第一步為什么需要經(jīng)過 ensureRendererbaseCreateRenderer?

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

當(dāng) patch 的時候需要操作 dom,則會調(diào)用外部傳入的方法進(jìn)行操作,這樣就可以更方便實現(xiàn)跨端。

ensureRenderer 存放在 runtime-dom 中,主要為 baseCreateRenderer 提供一系列 dom 操作的函數(shù)。

假如我們要自定義渲染器,那么只需要實現(xiàn)ensureRenderer 即可。而不是像 Vue2 需要 fork 一份,大大提高了 Vue3 的應(yīng)用范圍。


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

下篇文章將解析 Vue3 的響應(yīng)式原理,如果有興趣的話別忘了關(guān)注我呀,我們一起學(xué)習(xí)、進(jìn)步。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 這是一篇一點都不講究的文章記錄下時間點吧 9 月 15 號把 Vue3 的 master 分支拉下來了,然后...
    zpkzpk閱讀 1,233評論 0 0
  • 話不多說,我們直奔主題,從0開始手寫實現(xiàn)Vue3初始化流程! Vue3初始化流程 在手寫實現(xiàn)之前,我們首先來看看V...
    深度剖析JavaScript閱讀 2,498評論 0 13
  • 16宿命:用概率思維提高你的勝算 以前的我是風(fēng)險厭惡者,不喜歡去冒險,但是人生放棄了冒險,也就放棄了無數(shù)的可能。 ...
    yichen大刀閱讀 7,575評論 0 4
  • 公元:2019年11月28日19時42分農(nóng)歷:二零一九年 十一月 初三日 戌時干支:己亥乙亥己巳甲戌當(dāng)月節(jié)氣:立冬...
    石放閱讀 7,386評論 0 2

友情鏈接更多精彩內(nèi)容