首先肝断,組件是一個(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)了。
- 重寫(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
- 創(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è)面中去。
- 渲染 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)间学,最終掛載到最外層的容器上殷费。
搬運(yùn)自網(wǎng)絡(luò),若有不妥請(qǐng)聯(lián)系刪除低葫!