Vue3.0源碼解析之組件渲染,vnode 到真實(shí) DOM

首先肝断,組件是一個(gè)抽象的概念杈曲,它是對(duì)一棵 DOM 樹(shù)的抽象,我們?cè)陧?yè)面中寫(xiě)一個(gè)組件節(jié)點(diǎn):

<hello-world></hello-world>

這段代碼并不會(huì)在頁(yè)面上渲染一個(gè)<hello-world>標(biāo)簽胸懈,而它具體渲染成什么担扑,取決于你怎么編寫(xiě) HelloWorld 組件的模板。舉個(gè)例子趣钱,HelloWorld 組件內(nèi)部的模板定義是這樣的:

<template>
  <div>
    <p>Hello World</p>
  </div>
</template>

可以看到眉抬,模板內(nèi)部最終會(huì)在頁(yè)面上渲染一個(gè) div,內(nèi)部包含一個(gè) p 標(biāo)簽脊凰,用來(lái)顯示 Hello World 文本。

所以间唉,從表現(xiàn)上來(lái)看,組件的模板決定了組件生成的 DOM 標(biāo)簽利术,而在 Vue.js 內(nèi)部呈野,一個(gè)組件想要真正的渲染生成 DOM,還需要經(jīng)歷“創(chuàng)建 vnode - 渲染 vnode - 生成 DOM” 這幾個(gè)步驟:

你可能會(huì)問(wèn)印叁,什么是 vnode被冒,它和組件什么關(guān)系呢?先不要著急轮蜕,我們?cè)诤竺鏁?huì)詳細(xì)說(shuō)明昨悼。這里,你只需要記住它就是一個(gè)可以描述組件信息的 JavaScript 對(duì)象即可跃洛。

接下來(lái)率触,我們就從應(yīng)用程序的入口開(kāi)始,逐步來(lái)看 Vue.js 3.0 中的組件是如何渲染的汇竭。

應(yīng)用程序初始化
一個(gè)組件可以通過(guò)“模板加對(duì)象描述”的方式創(chuàng)建葱蝗,組件創(chuàng)建好以后是如何被調(diào)用并初始化的呢?因?yàn)檎麄€(gè)組件樹(shù)是由根組件開(kāi)始渲染的细燎,為了找到根組件的渲染入口两曼,我們需要從應(yīng)用程序的初始化過(guò)程開(kāi)始分析。

在這里玻驻,我分別給出了通過(guò) Vue.js 2.x 和 Vue.js 3.0 來(lái)初始化應(yīng)用的代碼:

// 在 Vue.js 2.x 中悼凑,初始化一個(gè)應(yīng)用的方式如下
import Vue from 'vue'
import App from './App'
const app = new Vue({
  render: h => h(App)
})
app.$mount('#app')
// 在 Vue.js 3.0 中,初始化一個(gè)應(yīng)用的方式如下
import { createApp } from 'vue'
import App from './app'
const app = createApp(App)
app.mount('#app')

可以看到璧瞬,Vue.js 3.0 初始化應(yīng)用的方式和 Vue.js 2.x 差別并不大户辫,本質(zhì)上都是把 App 組件掛載到 id 為 app 的 DOM 節(jié)點(diǎn)上。

但是嗤锉,在 Vue.js 3.0 中還導(dǎo)入了一個(gè) createApp寸莫,其實(shí)這是個(gè)入口函數(shù),它是 Vue.js 對(duì)外暴露的一個(gè)函數(shù)档冬,我們來(lái)看一下它的內(nèi)部實(shí)現(xiàn):

const createApp = ((...args) => {
  // 創(chuàng)建 app 對(duì)象
  const app = ensureRenderer().createApp(...args)
  const { mount } = app
  // 重寫(xiě) mount 方法
  app.mount = (containerOrSelector) => {
    // ...
  }
  return app
})

從代碼中可以看出 createApp 主要做了兩件事情:創(chuàng)建 app 對(duì)象和重寫(xiě) app.mount 方法膘茎。接下來(lái),我們就具體來(lái)分析一下它們酷誓。

1. 創(chuàng)建 app 對(duì)象
首先披坏,我們使用 ensureRenderer().createApp() 來(lái)創(chuàng)建 app 對(duì)象 :

 const app = ensureRenderer().createApp(...args)
其中 ensureRenderer() 用來(lái)創(chuàng)建一個(gè)渲染器對(duì)象,它的內(nèi)部代碼是這樣的:

// 渲染相關(guān)的一些配置盐数,比如更新屬性的方法棒拂,操作 DOM 的方法
const rendererOptions = {
  patchProp,
  ...nodeOps
}
let renderer
// 延時(shí)創(chuàng)建渲染器,當(dāng)用戶(hù)只依賴(lài)響應(yīng)式包的時(shí)候,可以通過(guò) tree-shaking 移除核心渲染邏輯相關(guān)的代碼
function ensureRenderer() {
  return renderer || (renderer = createRenderer(rendererOptions))
}
function createRenderer(options) {
  return baseCreateRenderer(options)
}
function baseCreateRenderer(options) {
  function render(vnode, container) {
    // 組件渲染的核心邏輯
  }
  return {
    render,
    createApp: createAppAPI(render)
  }
}
function createAppAPI(render) {
  // createApp createApp 方法接受的兩個(gè)參數(shù):根組件的對(duì)象和 prop
  return function createApp(rootComponent, rootProps = null) {
    const app = {
      _component: rootComponent,
      _props: rootProps,
      mount(rootContainer) {
        // 創(chuàng)建根組件的 vnode
        const vnode = createVNode(rootComponent, rootProps)
        // 利用渲染器渲染 vnode
        render(vnode, rootContainer)
        app._container = rootContainer
        return vnode.component.proxy
      }
    }
    return app
  }
}

可以看到帚屉,這里先用 ensureRenderer() 來(lái)延時(shí)創(chuàng)建渲染器谜诫,這樣做的好處是當(dāng)用戶(hù)只依賴(lài)響應(yīng)式包的時(shí)候,就不會(huì)創(chuàng)建渲染器攻旦,因此可以通過(guò) tree-shaking 的方式移除核心渲染邏輯相關(guān)的代碼喻旷。

這里涉及了渲染器的概念,它是為跨平臺(tái)渲染做準(zhǔn)備的牢屋,之后我會(huì)在自定義渲染器的相關(guān)內(nèi)容中詳細(xì)說(shuō)明且预。在這里,你可以簡(jiǎn)單地把渲染器理解為包含平臺(tái)渲染核心邏輯的 JavaScript 對(duì)象烙无。

我們結(jié)合上面的代碼繼續(xù)深入锋谐,在 Vue.js 3.0 內(nèi)部通過(guò) createRenderer 創(chuàng)建一個(gè)渲染器,這個(gè)渲染器內(nèi)部會(huì)有一個(gè) createApp 方法截酷,它是執(zhí)行 createAppAPI 方法返回的函數(shù)涮拗,接受了 rootComponent 和 rootProps 兩個(gè)參數(shù),我們?cè)趹?yīng)用層面執(zhí)行 createApp(App) 方法時(shí)迂苛,會(huì)把 App 組件對(duì)象作為根組件傳遞給 rootComponent三热。這樣,createApp 內(nèi)部就創(chuàng)建了一個(gè) app 對(duì)象灾部,它會(huì)提供 mount 方法康铭,這個(gè)方法是用來(lái)掛載組件的惯退。

在整個(gè) app 對(duì)象創(chuàng)建過(guò)程中赌髓,Vue.js 利用閉包和函數(shù)顆粒化的技巧催跪,很好地實(shí)現(xiàn)了參數(shù)保留锁蠕。比如,在執(zhí)行 app.mount 的時(shí)候懊蒸,并不需要傳入渲染器 render荣倾,這是因?yàn)樵趫?zhí)行 createAppAPI 的時(shí)候渲染器 render 參數(shù)已經(jīng)被保留下來(lái)了。

  1. 重寫(xiě) app.mount 方法
    接下來(lái)骑丸,是重寫(xiě) app.mount 方法舌仍。

根據(jù)前面的分析,我們知道 createApp 返回的 app 對(duì)象已經(jīng)擁有了 mount 方法了通危,但在入口函數(shù)中铸豁,接下來(lái)的邏輯卻是對(duì) app.mount 方法的重寫(xiě)。先思考一下菊碟,為什么要重寫(xiě)這個(gè)方法节芥,而不把相關(guān)邏輯放在 app 對(duì)象的 mount 方法內(nèi)部來(lái)實(shí)現(xiàn)呢?

這是因?yàn)?Vue.js 不僅僅是為 Web 平臺(tái)服務(wù),它的目標(biāo)是支持跨平臺(tái)渲染头镊,而 createApp 函數(shù)內(nèi)部的 app.mount 方法是一個(gè)標(biāo)準(zhǔn)的可跨平臺(tái)的組件渲染流程:

mount(rootContainer) {
  // 創(chuàng)建根組件的 vnode
  const vnode = createVNode(rootComponent, rootProps)
  // 利用渲染器渲染 vnode
  render(vnode, rootContainer)
  app._container = rootContainer
  return vnode.component.proxy
}

標(biāo)準(zhǔn)的跨平臺(tái)渲染流程是先創(chuàng)建 vnode蚣驼,再渲染 vnode。此外參數(shù) rootContainer 也可以是不同類(lèi)型的值相艇,比如颖杏,在 Web 平臺(tái)它是一個(gè) DOM 對(duì)象,而在其他平臺(tái)(比如 Weex 和小程序)中可以是其他類(lèi)型的值厂捞。所以這里面的代碼不應(yīng)該包含任何特定平臺(tái)相關(guān)的邏輯输玷,也就是說(shuō)這些代碼的執(zhí)行邏輯都是與平臺(tái)無(wú)關(guān)的。因此我們需要在外部重寫(xiě)這個(gè)方法靡馁,來(lái)完善 Web 平臺(tái)下的渲染邏輯欲鹏。

接下來(lái),我們?cè)賮?lái)看 app.mount 重寫(xiě)都做了哪些事情:

app.mount = (containerOrSelector) => {
  // 標(biāo)準(zhǔn)化容器
  const container = normalizeContainer(containerOrSelector)
  if (!container)
    return
  const component = app._component
   // 如組件對(duì)象沒(méi)有定義 render 函數(shù)和 template 模板臭墨,則取容器的 innerHTML 作為組件模板內(nèi)容
  if (!isFunction(component) && !component.render && !component.template) {
    component.template = container.innerHTML
  }
  // 掛載前清空容器內(nèi)容
  container.innerHTML = ''
  // 真正的掛載
  return mount(container)
}

首先是通過(guò) normalizeContainer 標(biāo)準(zhǔn)化容器(這里可以傳字符串選擇器或者 DOM 對(duì)象赔嚎,但如果是字符串選擇器,就需要把它轉(zhuǎn)成 DOM 對(duì)象胧弛,作為最終掛載的容器)尤误,然后做一個(gè) if 判斷,如果組件對(duì)象沒(méi)有定義 render 函數(shù)和 template 模板结缚,則取容器的 innerHTML 作為組件模板內(nèi)容损晤;接著在掛載前清空容器內(nèi)容,最終再調(diào)用 app.mount 的方法走標(biāo)準(zhǔn)的組件渲染流程红竭。

在這里尤勋,重寫(xiě)的邏輯都是和 Web 平臺(tái)相關(guān)的,所以要放在外部實(shí)現(xiàn)茵宪。此外最冰,這么做的目的是既能讓用戶(hù)在使用 API 時(shí)可以更加靈活,也兼容了 Vue.js 2.x 的寫(xiě)法稀火,比如 app.mount 的第一個(gè)參數(shù)就同時(shí)支持選擇器字符串和 DOM 對(duì)象兩種類(lèi)型暖哨。

從 app.mount 開(kāi)始,才算真正進(jìn)入組件渲染流程凰狞,那么接下來(lái)篇裁,我們就重點(diǎn)看一下核心渲染流程做的兩件事情:創(chuàng)建 vnode 和渲染 vnode。

核心渲染流程:創(chuàng)建 vnode 和渲染 vnode

  1. 創(chuàng)建 vnode
    首先赡若,是創(chuàng)建 vnode 的過(guò)程达布。

vnode 本質(zhì)上是用來(lái)描述 DOM 的 JavaScript 對(duì)象,它在 Vue.js 中可以描述不同類(lèi)型的節(jié)點(diǎn)斩熊,比如普通元素節(jié)點(diǎn)往枣、組件節(jié)點(diǎn)等。

什么是普通元素節(jié)點(diǎn)呢?舉個(gè)例子分冈,在 HTML 中我們使用 <button> 標(biāo)簽來(lái)寫(xiě)一個(gè)按鈕:

<button class="btn" style="width:100px;height:50px">click me</button>

我們可以用 vnode 這樣表示<button>標(biāo)簽:

const vnode = {
  type: 'button',
  props: { 
    'class': 'btn',
    style: {
      width: '100px',
      height: '50px'
    }
  },
  children: 'click me'
}

其中圾另,type 屬性表示 DOM 的標(biāo)簽類(lèi)型,props 屬性表示 DOM 的一些附加信息雕沉,比如 style 集乔、class 等,children 屬性表示 DOM 的子節(jié)點(diǎn)坡椒,它也可以是一個(gè) vnode 數(shù)組扰路,只不過(guò) vnode 可以用字符串表示簡(jiǎn)單的文本 。

什么是組件節(jié)點(diǎn)呢倔叼?其實(shí)汗唱, vnode 除了可以像上面那樣用于描述一個(gè)真實(shí)的 DOM,也可以用來(lái)描述組件丈攒。

我們先在模板中引入一個(gè)組件標(biāo)簽 <custom-component>:

<custom-component msg="test"></custom-component>
我們可以用 vnode 這樣表示 <custom-component> 組件標(biāo)簽:

const CustomComponent = {
  // 在這里定義組件對(duì)象
}
const vnode = {
  type: CustomComponent,
  props: { 
    msg: 'test'
  }
}

組件 vnode 其實(shí)是對(duì)抽象事物的描述哩罪,這是因?yàn)槲覀儾⒉粫?huì)在頁(yè)面上真正渲染一個(gè) <custom-component> 標(biāo)簽,而是渲染組件內(nèi)部定義的 HTML 標(biāo)簽巡验。

除了上兩種 vnode 類(lèi)型外际插,還有純文本 vnode、注釋 vnode 等等显设,但鑒于我們的主線(xiàn)只需要研究組件 vnode 和普通元素 vnode框弛,所以我在這里就不贅述了。

另外捕捂,Vue.js 3.0 內(nèi)部還針對(duì) vnode 的 type瑟枫,做了更詳盡的分類(lèi),包括 Suspense绞蹦、Teleport 等力奋,且把 vnode 的類(lèi)型信息做了編碼榜旦,以便在后面的 patch 階段幽七,可以根據(jù)不同的類(lèi)型執(zhí)行相應(yīng)的處理邏輯:

const shapeFlag = isString(type)
  ? 1 /* ELEMENT */
  : isSuspense(type)
    ? 128 /* SUSPENSE */
    : isTeleport(type)
      ? 64 /* TELEPORT */
      : isObject(type)
        ? 4 /* STATEFUL_COMPONENT */
        : isFunction(type)
          ? 2 /* FUNCTIONAL_COMPONENT */
          : 0

知道什么是 vnode 后,你可能會(huì)好奇溅呢,那么 vnode 有什么優(yōu)勢(shì)呢澡屡?為什么一定要設(shè)計(jì) vnode 這樣的數(shù)據(jù)結(jié)構(gòu)呢?

首先是抽象咐旧,引入 vnode驶鹉,可以把渲染過(guò)程抽象化,從而使得組件的抽象能力也得到提升铣墨。

其次是跨平臺(tái)室埋,因?yàn)?patch vnode 的過(guò)程不同平臺(tái)可以有自己的實(shí)現(xiàn),基于 vnode 再做服務(wù)端渲染、Weex 平臺(tái)姚淆、小程序平臺(tái)的渲染都變得容易了很多孕蝉。

不過(guò)這里要特別注意,使用 vnode 并不意味著不用操作 DOM 了腌逢,很多同學(xué)會(huì)誤以為 vnode 的性能一定比手動(dòng)操作原生 DOM 好降淮,這個(gè)其實(shí)是不一定的。

因?yàn)椴龋紫冗@種基于 vnode 實(shí)現(xiàn)的 MVVM 框架佳鳖,在每次 render to vnode 的過(guò)程中,渲染組件會(huì)有一定的 JavaScript 耗時(shí)媒惕,特別是大組件系吩,比如一個(gè) 1000 * 10 的 Table 組件,render to vnode 的過(guò)程會(huì)遍歷 1000 * 10 次去創(chuàng)建內(nèi)部 cell vnode妒蔚,整個(gè)耗時(shí)就會(huì)變得比較長(zhǎng)淑玫,加上 patch vnode 的過(guò)程也會(huì)有一定的耗時(shí),當(dāng)我們?nèi)ジ陆M件的時(shí)候面睛,用戶(hù)會(huì)感覺(jué)到明顯的卡頓絮蒿。雖然 diff 算法在減少 DOM 操作方面足夠優(yōu)秀,但最終還是免不了操作 DOM叁鉴,所以說(shuō)性能并不是 vnode 的優(yōu)勢(shì)土涝。

那么,Vue.js 內(nèi)部是如何創(chuàng)建這些 vnode 的呢幌墓?

回顧 app.mount 函數(shù)的實(shí)現(xiàn)但壮,內(nèi)部是通過(guò) createVNode 函數(shù)創(chuàng)建了根組件的 vnode :

 const vnode = createVNode(rootComponent, rootProps)

我們來(lái)看一下 createVNode 函數(shù)的大致實(shí)現(xiàn):

function createVNode(type, props = null
,children = null) {
  if (props) {
    // 處理 props 相關(guān)邏輯,標(biāo)準(zhǔn)化 class 和 style
  }
  // 對(duì) vnode 類(lèi)型信息編碼
  const shapeFlag = isString(type)
    ? 1 /* ELEMENT */
    : isSuspense(type)
      ? 128 /* SUSPENSE */
      : isTeleport(type)
        ? 64 /* TELEPORT */
        : isObject(type)
          ? 4 /* STATEFUL_COMPONENT */
          : isFunction(type)
            ? 2 /* FUNCTIONAL_COMPONENT */
            : 0
  const vnode = {
    type,
    props,
    shapeFlag,
    // 一些其他屬性
  }
  // 標(biāo)準(zhǔn)化子節(jié)點(diǎn)常侣,把不同數(shù)據(jù)類(lèi)型的 children 轉(zhuǎn)成數(shù)組或者文本類(lèi)型
  normalizeChildren(vnode, children)
  return vnode
}

通過(guò)上述代碼可以看到蜡饵,其實(shí) createVNode 做的事情很簡(jiǎn)單,就是:對(duì) props 做標(biāo)準(zhǔn)化處理胳施、對(duì) vnode 的類(lèi)型信息編碼溯祸、創(chuàng)建 vnode 對(duì)象,標(biāo)準(zhǔn)化子節(jié)點(diǎn) children 舞肆。

我們現(xiàn)在擁有了這個(gè) vnode 對(duì)象焦辅,接下來(lái)要做的事情就是把它渲染到頁(yè)面中去。

  1. 渲染 vnode
    接下來(lái)椿胯,是渲染 vnode 的過(guò)程筷登。

回顧 app.mount 函數(shù)的實(shí)現(xiàn),內(nèi)部通過(guò)執(zhí)行這段代碼去渲染創(chuàng)建好的 vnode:

render(vnode, rootContainer)
const render = (vnode, container) => {
  if (vnode == null) {
    // 銷(xiāo)毀組件
    if (container._vnode) {
      unmount(container._vnode, null, null, true)
    }
  } else {
    // 創(chuàng)建或者更新組件
    patch(container._vnode || null, vnode, container)
  }
  // 緩存 vnode 節(jié)點(diǎn)哩盲,表示已經(jīng)渲染
  container._vnode = vnode
}

這個(gè)渲染函數(shù) render 的實(shí)現(xiàn)很簡(jiǎn)單前方,如果它的第一個(gè)參數(shù) vnode 為空狈醉,則執(zhí)行銷(xiāo)毀組件的邏輯,否則執(zhí)行創(chuàng)建或者更新組件的邏輯惠险。

接下來(lái)我們接著看一下上面渲染 vnode 的代碼中涉及的 patch 函數(shù)的實(shí)現(xiàn):

const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => {
  // 如果存在新舊節(jié)點(diǎn), 且新舊節(jié)點(diǎn)類(lèi)型不同舔糖,則銷(xiāo)毀舊節(jié)點(diǎn)
  if (n1 && !isSameVNodeType(n1, n2)) {
    anchor = getNextHostNode(n1)
    unmount(n1, parentComponent, parentSuspense, true)
    n1 = null
  }
  const { type, shapeFlag } = n2
  switch (type) {
    case Text:
      // 處理文本節(jié)點(diǎn)
      break
    case Comment:
      // 處理注釋節(jié)點(diǎn)
      break
    case Static:
      // 處理靜態(tài)節(jié)點(diǎn)
      break
    case Fragment:
      // 處理 Fragment 元素
      break
    default:
      if (shapeFlag & 1 /* ELEMENT */) {
        // 處理普通 DOM 元素
        processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      }
      else if (shapeFlag & 6 /* COMPONENT */) {
        // 處理組件
        processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      }
      else if (shapeFlag & 64 /* TELEPORT */) {
        // 處理 TELEPORT
      }
      else if (shapeFlag & 128 /* SUSPENSE */) {
        // 處理 SUSPENSE
      }
  }
}

patch 本意是打補(bǔ)丁的意思,這個(gè)函數(shù)有兩個(gè)功能莺匠,一個(gè)是根據(jù) vnode 掛載 DOM金吗,一個(gè)是根據(jù)新舊 vnode 更新 DOM。對(duì)于初次渲染趣竣,我們這里只分析創(chuàng)建過(guò)程摇庙,更新過(guò)程在后面的章節(jié)分析。

在創(chuàng)建的過(guò)程中遥缕,patch 函數(shù)接受多個(gè)參數(shù)卫袒,這里我們目前只重點(diǎn)關(guān)注前三個(gè):

第一個(gè)參數(shù) n1 表示舊的 vnode,當(dāng) n1 為 null 的時(shí)候单匣,表示是一次掛載的過(guò)程夕凝;
第二個(gè)參數(shù) n2 表示新的 vnode 節(jié)點(diǎn),后續(xù)會(huì)根據(jù)這個(gè) vnode 類(lèi)型執(zhí)行不同的處理邏輯户秤;
第三個(gè)參數(shù) container 表示 DOM 容器码秉,也就是 vnode 渲染生成 DOM 后,會(huì)掛載到 container 下面鸡号。
對(duì)于渲染的節(jié)點(diǎn)转砖,我們這里重點(diǎn)關(guān)注兩種類(lèi)型節(jié)點(diǎn)的渲染邏輯:對(duì)組件的處理和對(duì)普通 DOM 元素的處理。
先來(lái)看對(duì)組件的處理鲸伴。由于初始化渲染的是 App 組件府蔗,它是一個(gè)組件 vnode,所以我們來(lái)看一下組件的處理邏輯是怎樣的汞窗。首先是用來(lái)處理組件的 processComponent 函數(shù)的實(shí)現(xiàn):

const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  if (n1 == null) {
   // 掛載組件
   mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  }
  else {
    // 更新組件
    updateComponent(n1, n2, parentComponent, optimized)
  }
}

該函數(shù)的邏輯很簡(jiǎn)單姓赤,如果 n1 為 null,則執(zhí)行掛載組件的邏輯仲吏,否則執(zhí)行更新組件的邏輯不铆。

我們接著來(lái)看掛載組件的 mountComponent 函數(shù)的實(shí)現(xiàn):

const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  // 創(chuàng)建組件實(shí)例
  const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
  // 設(shè)置組件實(shí)例
  setupComponent(instance)
  // 設(shè)置并運(yùn)行帶副作用的渲染函數(shù)
  setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
}

可以看到,掛載組件函數(shù) mountComponent 主要做三件事情:創(chuàng)建組件實(shí)例蜘矢、設(shè)置組件實(shí)例狂男、設(shè)置并運(yùn)行帶副作用的渲染函數(shù)综看。

首先是創(chuàng)建組件實(shí)例品腹,Vue.js 3.0 雖然不像 Vue.js 2.x 那樣通過(guò)類(lèi)的方式去實(shí)例化組件,但內(nèi)部也通過(guò)對(duì)象的方式去創(chuàng)建了當(dāng)前渲染的組件實(shí)例红碑。

其次設(shè)置組件實(shí)例舞吭,instance 保留了很多組件相關(guān)的數(shù)據(jù)泡垃,維護(hù)了組件的上下文,包括對(duì) props羡鸥、插槽蔑穴,以及其他實(shí)例的屬性的初始化處理。

創(chuàng)建和設(shè)置組件實(shí)例這兩個(gè)流程我們這里不展開(kāi)講惧浴,會(huì)在后面的章節(jié)詳細(xì)分析存和。

最后是運(yùn)行帶副作用的渲染函數(shù) setupRenderEffect,我們重點(diǎn)來(lái)看一下這個(gè)函數(shù)的實(shí)現(xiàn):

const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
  // 創(chuàng)建響應(yīng)式的副作用渲染函數(shù)
  instance.update = effect(function componentEffect() {
    if (!instance.isMounted) {
      // 渲染組件生成子樹(shù) vnode
      const subTree = (instance.subTree = renderComponentRoot(instance))
      // 把子樹(shù) vnode 掛載到 container 中
      patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
      // 保留渲染生成的子樹(shù)根 DOM 節(jié)點(diǎn)
      initialVNode.el = subTree.el
      instance.isMounted = true
    }
    else {
      // 更新組件
    }
  }, prodEffectOptions)
}

該函數(shù)利用響應(yīng)式庫(kù)的 effect 函數(shù)創(chuàng)建了一個(gè)副作用渲染函數(shù) componentEffect (effect 的實(shí)現(xiàn)我們后面講響應(yīng)式章節(jié)會(huì)具體說(shuō))衷旅。副作用捐腿,這里你可以簡(jiǎn)單地理解為,當(dāng)組件的數(shù)據(jù)發(fā)生變化時(shí)柿顶,effect 函數(shù)包裹的內(nèi)部渲染函數(shù) componentEffect 會(huì)重新執(zhí)行一遍茄袖,從而達(dá)到重新渲染組件的目的。

渲染函數(shù)內(nèi)部也會(huì)判斷這是一次初始渲染還是組件更新嘁锯。這里我們只分析初始渲染流程宪祥。

初始渲染主要做兩件事情:渲染組件生成 subTree、把 subTree 掛載到 container 中家乘。

首先蝗羊,是渲染組件生成 subTree,它也是一個(gè) vnode 對(duì)象仁锯。這里要注意別把 subTree 和 initialVNode 弄混了(其實(shí)在 Vue.js 3.0 中肘交,根據(jù)命名我們已經(jīng)能很好地區(qū)分它們了,而在 Vue.js 2.x 中它們分別命名為 _vnode 和 $vnode)扑馁。我來(lái)舉個(gè)例子說(shuō)明涯呻,在父組件 App 中里引入了 Hello 組件:

<template>
  <div class="app">
    <p>This is an app.</p>
    <hello></hello>
  </div>
</template>

在 Hello 組件中是 <div> 標(biāo)簽包裹著一個(gè)<p>標(biāo)簽:

<template>
  <div class="hello">
    <p>Hello, Vue 3.0!</p>
  </div>
</template>

在 App 組件中, <hello> 節(jié)點(diǎn)渲染生成的 vnode 腻要,對(duì)應(yīng)的就是 Hello 組件的 initialVNode 复罐,為了好記,你也可以把它稱(chēng)作“組件 vnode”雄家。而 Hello 組件內(nèi)部整個(gè) DOM 節(jié)點(diǎn)對(duì)應(yīng)的 vnode 就是執(zhí)行 renderComponentRoot 渲染生成對(duì)應(yīng)的 subTree效诅,我們可以把它稱(chēng)作“子樹(shù) vnode”。

我們知道每個(gè)組件都會(huì)有對(duì)應(yīng)的 render 函數(shù)趟济,即使你寫(xiě) template乱投,也會(huì)編譯成 render 函數(shù),而 renderComponentRoot 函數(shù)就是去執(zhí)行 render 函數(shù)創(chuàng)建整個(gè)組件樹(shù)內(nèi)部的 vnode顷编,把這個(gè) vnode 再經(jīng)過(guò)內(nèi)部一層標(biāo)準(zhǔn)化戚炫,就得到了該函數(shù)的返回結(jié)果:子樹(shù) vnode。

渲染生成子樹(shù) vnode 后媳纬,接下來(lái)就是繼續(xù)調(diào)用 patch 函數(shù)把子樹(shù) vnode 掛載到 container 中了双肤。

那么我們又再次回到了 patch 函數(shù)施掏,會(huì)繼續(xù)對(duì)這個(gè)子樹(shù) vnode 類(lèi)型進(jìn)行判斷,對(duì)于上述例子茅糜,App 組件的根節(jié)點(diǎn)是 <div> 標(biāo)簽七芭,那么對(duì)應(yīng)的子樹(shù) vnode 也是一個(gè)普通元素 vnode,那么我們接下來(lái)看對(duì)普通 DOM 元素的處理流程蔑赘。

首先我們來(lái)看一下處理普通 DOM元素的 processElement 函數(shù)的實(shí)現(xiàn):

const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  isSVG = isSVG || n2.type === 'svg'
  if (n1 == null) {
    //掛載元素節(jié)點(diǎn)
    mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  }
  else {
    //更新元素節(jié)點(diǎn)
    patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
  }
}

該函數(shù)的邏輯很簡(jiǎn)單狸驳,如果 n1 為 null,走掛載元素節(jié)點(diǎn)的邏輯缩赛,否則走更新元素節(jié)點(diǎn)邏輯锌历。

我們接著來(lái)看掛載元素的 mountElement 函數(shù)的實(shí)現(xiàn):

const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  let el
  const { type, props, shapeFlag } = vnode
  // 創(chuàng)建 DOM 元素節(jié)點(diǎn)
  el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is)
  if (props) {
    // 處理 props,比如 class峦筒、style究西、event 等屬性
    for (const key in props) {
      if (!isReservedProp(key)) {
        hostPatchProp(el, key, null, props[key], isSVG)
      }
    }
  }
  if (shapeFlag & 8 /* TEXT_CHILDREN */) {
    // 處理子節(jié)點(diǎn)是純文本的情況
    hostSetElementText(el, vnode.children)
  }
  else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
    // 處理子節(jié)點(diǎn)是數(shù)組的情況
    mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || !!vnode.dynamicChildren)
  }
  // 把創(chuàng)建的 DOM 元素節(jié)點(diǎn)掛載到 container 上
  hostInsert(el, container, anchor)
}

可以看到,掛載元素函數(shù)主要做四件事:創(chuàng)建 DOM 元素節(jié)點(diǎn)物喷、處理 props卤材、處理 children、掛載 DOM 元素到 container 上峦失。

首先是創(chuàng)建 DOM 元素節(jié)點(diǎn)扇丛,通過(guò) hostCreateElement 方法創(chuàng)建,這是一個(gè)平臺(tái)相關(guān)的方法尉辑,我們來(lái)看一下它在 Web 環(huán)境下的定義:

function createElement(tag, isSVG, is) {
  isSVG ? document.createElementNS(svgNS, tag)
    : document.createElement(tag, is ? { is } : undefined)
}

它調(diào)用了底層的 DOM API document.createElement 創(chuàng)建元素帆精,所以本質(zhì)上 Vue.js 強(qiáng)調(diào)不去操作 DOM ,只是希望用戶(hù)不直接碰觸 DOM隧魄,它并沒(méi)有什么神奇的魔法卓练,底層還是會(huì)操作 DOM。

另外购啄,如果是其他平臺(tái)比如 Weex襟企,hostCreateElement 方法就不再是操作 DOM ,而是平臺(tái)相關(guān)的 API 了狮含,這些平臺(tái)相關(guān)的方法是在創(chuàng)建渲染器階段作為參數(shù)傳入的顽悼。

創(chuàng)建完 DOM 節(jié)點(diǎn)后,接下來(lái)要做的是判斷如果有 props 的話(huà)几迄,給這個(gè) DOM 節(jié)點(diǎn)添加相關(guān)的 class蔚龙、style、event 等屬性映胁,并做相關(guān)的處理木羹,這些邏輯都是在 hostPatchProp 函數(shù)內(nèi)部做的,這里就不展開(kāi)講了屿愚。

接下來(lái)是對(duì)子節(jié)點(diǎn)的處理汇跨,我們知道 DOM 是一棵樹(shù)务荆,vnode 同樣也是一棵樹(shù)妆距,并且它和 DOM 結(jié)構(gòu)是一一映射的穷遂。

如果子節(jié)點(diǎn)是純文本,則執(zhí)行 hostSetElementText 方法娱据,它在 Web 環(huán)境下通過(guò)設(shè)置 DOM 元素的 textContent 屬性設(shè)置文本:

function setElementText(el, text) {
  el.textContent = text
}

如果子節(jié)點(diǎn)是數(shù)組蚪黑,則執(zhí)行 mountChildren 方法:

const mountChildren = (children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start = 0) => {
  for (let i = start; i < children.length; i++) {
    // 預(yù)處理 child
    const child = (children[i] = optimized
      ? cloneIfMounted(children[i])
      : normalizeVNode(children[i]))
    // 遞歸 patch 掛載 child
    patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  }
}

子節(jié)點(diǎn)的掛載邏輯同樣很簡(jiǎn)單,遍歷 children 獲取到每一個(gè) child中剩,然后遞歸執(zhí)行 patch 方法掛載每一個(gè) child 忌穿。注意,這里有對(duì) child 做預(yù)處理的情況(后面編譯優(yōu)化的章節(jié)會(huì)詳細(xì)分析)结啼。

可以看到掠剑,mountChildren 函數(shù)的第二個(gè)參數(shù)是 container,而我們調(diào)用 mountChildren 方法傳入的第二個(gè)參數(shù)是在 mountElement 時(shí)創(chuàng)建的 DOM 節(jié)點(diǎn)郊愧,這就很好地建立了父子關(guān)系朴译。

另外,通過(guò)遞歸 patch 這種深度優(yōu)先遍歷樹(shù)的方式属铁,我們就可以構(gòu)造完整的 DOM 樹(shù)眠寿,完成組件的渲染。

處理完所有子節(jié)點(diǎn)后焦蘑,最后通過(guò) hostInsert 方法把創(chuàng)建的 DOM 元素節(jié)點(diǎn)掛載到 container 上盯拱,它在 Web 環(huán)境下這樣定義:

function insert(child, parent, anchor) {
  if (anchor) {
    parent.insertBefore(child, anchor)
  }
  else {
    parent.appendChild(child)
  }
}

這里會(huì)做一個(gè) if 判斷,如果有參考元素 anchor例嘱,就執(zhí)行 parent.insertBefore 狡逢,否則執(zhí)行 parent.appendChild 來(lái)把 child 添加到 parent 下,完成節(jié)點(diǎn)的掛載拼卵。

因?yàn)?insert 的執(zhí)行是在處理子節(jié)點(diǎn)后甚侣,所以?huà)燧d的順序是先子節(jié)點(diǎn),后父節(jié)點(diǎn)间学,最終掛載到最外層的容器上殷费。

2a882b9dcb5a4fa6b5a8346669fdda5f_tplv-k3u1fbpfcp-zoom.png

搬運(yùn)自網(wǎng)絡(luò),若有不妥請(qǐng)聯(lián)系刪除低葫!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末详羡,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子嘿悬,更是在濱河造成了極大的恐慌实柠,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,376評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件善涨,死亡現(xiàn)場(chǎng)離奇詭異窒盐,居然都是意外死亡草则,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)蟹漓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)炕横,“玉大人,你說(shuō)我怎么就攤上這事葡粒》莸睿” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,966評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵嗽交,是天一觀的道長(zhǎng)卿嘲。 經(jīng)常有香客問(wèn)我,道長(zhǎng)夫壁,這世上最難降的妖魔是什么拾枣? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,432評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮盒让,結(jié)果婚禮上梅肤,老公的妹妹穿的比我還像新娘。我一直安慰自己糯彬,他們只是感情好凭语,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著撩扒,像睡著了一般似扔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上搓谆,一...
    開(kāi)封第一講書(shū)人閱讀 49,792評(píng)論 1 290
  • 那天炒辉,我揣著相機(jī)與錄音,去河邊找鬼泉手。 笑死黔寇,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的斩萌。 我是一名探鬼主播缝裤,決...
    沈念sama閱讀 38,933評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼颊郎!你這毒婦竟也來(lái)了憋飞?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,701評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤姆吭,失蹤者是張志新(化名)和其女友劉穎榛做,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,143評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡检眯,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評(píng)論 2 327
  • 正文 我和宋清朗相戀三年厘擂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锰瘸。...
    茶點(diǎn)故事閱讀 38,626評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡刽严,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出获茬,到底是詐尸還是另有隱情港庄,我是刑警寧澤倔既,帶...
    沈念sama閱讀 34,292評(píng)論 4 329
  • 正文 年R本政府宣布恕曲,位于F島的核電站,受9級(jí)特大地震影響渤涌,放射性物質(zhì)發(fā)生泄漏佩谣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評(píng)論 3 313
  • 文/蒙蒙 一实蓬、第九天 我趴在偏房一處隱蔽的房頂上張望茸俭。 院中可真熱鬧,春花似錦安皱、人聲如沸调鬓。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,742評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)腾窝。三九已至,卻和暖如春居砖,著一層夾襖步出監(jiān)牢的瞬間虹脯,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工奏候, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留循集,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓蔗草,卻偏偏與公主長(zhǎng)得像咒彤,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子咒精,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評(píng)論 2 348