Vue 3.0 源碼解析
源碼優(yōu)化
- 目的是讓代碼更易于開發(fā)和維護(hù)。源碼的優(yōu)化主要體現(xiàn)在使用monorepo 和 TypeScript 管理和開發(fā)源碼,這樣做的目標(biāo)是提升自身代碼可維護(hù)性
- Vue.js 2.x 的源碼托管在 src 目錄,然后依據(jù)功能拆分出了 compiler(模板編譯的相關(guān)代碼)峡扩、core(與平臺(tái)無關(guān)的通用運(yùn)行時(shí)代碼)、platforms(平臺(tái)專有代碼)、server(服務(wù)端渲染的相關(guān)代碼)撮抓、sfc(.vue 單文件解析相關(guān)代碼)、shared(共享工具代碼) 等目錄摇锋。
- Vue.js 3.0 丹拯,整個(gè)源碼是通過 monorepo 的方式維護(hù)的站超,根據(jù)功能將不同的模塊拆分到 packages 目錄下面不同的子目錄中,monorepo 把這些模塊拆分到不同的 package 中乖酬,每個(gè) package 有各自的 API死相、類型定義和測(cè)試。這樣使得模塊拆分更細(xì)化咬像,職責(zé)劃分更明確算撮,模塊之間的依賴關(guān)系也更加明確,開發(fā)人員也更容易閱讀施掏、理解和更改所有模塊源碼钮惠,提高代碼的可維護(hù)性。
性能優(yōu)化
- 源碼體積優(yōu)化七芭,JavaScript 包體積越小素挽,意味著網(wǎng)絡(luò)傳輸時(shí)間越短,JavaScript 引擎解析包的速度也越快
- Vue.js 3.0 在源碼體積的減少方面:移除一些冷門的 feature(比如 filter狸驳、inline-template 等)预明,引入 tree-shaking 的技術(shù),減少打包體積
-
數(shù)據(jù)劫持優(yōu)化耙箍,Vue.js 1.x 和 Vue.js 2.x 內(nèi)部都是通過 Object.defineProperty 這個(gè) API 去劫持?jǐn)?shù)據(jù)的 getter 和 setter撰糠,它必須預(yù)先知道要攔截的 key,它并不能檢測(cè)對(duì)象屬性的添加和刪除(提供了
delete 實(shí)例方法)辩昆。
- 嵌套層級(jí)比較深的對(duì)象阅酪,要劫持它內(nèi)部深層次的對(duì)象變化,就需要遞歸遍歷這個(gè)對(duì)象汁针,執(zhí)行 Object.defineProperty 把每一層對(duì)象數(shù)據(jù)都變成響應(yīng)式的术辐。響應(yīng)式數(shù)據(jù)過于復(fù)雜,就會(huì)有相當(dāng)大的性能負(fù)擔(dān)施无。
- Vue.js 3.0 使用了 Proxy API 做數(shù)據(jù)劫持辉词,由于它劫持的是整個(gè)對(duì)象,那么自然對(duì)于對(duì)象的屬性的增加和刪除都能檢測(cè)到猾骡。
- Proxy API 并不能監(jiān)聽到內(nèi)部深層次的對(duì)象變化瑞躺,因此 Vue.js 3.0 的處理方式是在 getter 中去遞歸響應(yīng)式,這樣的好處是真正訪問到的內(nèi)部對(duì)象才會(huì)變成響應(yīng)式兴想,減少非必要的遞歸幢哨,提升性能。
- 編譯優(yōu)化襟企,Vue2.x有許多非必要的diff 和遍歷嘱么,導(dǎo)致 vnode 的性能跟模版大小正相關(guān),跟動(dòng)態(tài)節(jié)點(diǎn)的數(shù)量無關(guān),當(dāng)一些組件的整個(gè)模版內(nèi)只有少量動(dòng)態(tài)節(jié)點(diǎn)時(shí)曼振,這些遍歷都是性能的浪費(fèi)几迄。
- Vue.js 3.0通過編譯階段對(duì)靜態(tài)模板的分析,編譯生成了 Block tree冰评。Vue.js 3.0 將 vnode 更新性能由與模版整體大小相關(guān)提升為與動(dòng)態(tài)內(nèi)容的數(shù)量相關(guān)映胁。
- Vue.js 3.0 在編譯階段還包含了對(duì) Slot 的編譯優(yōu)化、事件偵聽函數(shù)的緩存優(yōu)化甲雅,并且在運(yùn)行時(shí)重寫了 diff 算法解孙。
語法 API 優(yōu)化
01 | 組件渲染:vnode 到真實(shí) DOM 是如何轉(zhuǎn)變的?
- 組件的時(shí)候抛人,它的內(nèi)部是如何工作的嗎弛姜?
- 從編寫組件開始,到最終真實(shí)的 DOM 又是怎樣的一個(gè)轉(zhuǎn)變過程呢妖枚?
應(yīng)用程序初始化
在 Vue.js 內(nèi)部廷臼,一個(gè)組件想要真正的渲染生成 DOM,還需要經(jīng)歷“創(chuàng)建 vnode - 渲染 vnode - 生成 DOM” 這幾個(gè)步驟绝页。
一個(gè)組件可以通過“模板加對(duì)象描述”的方式創(chuàng)建荠商,組件創(chuàng)建好以后是如何被調(diào)用并初始化的呢?
在 Vue.js 3.0 中還導(dǎo)入了一個(gè) createApp续誉,其實(shí)這是個(gè)入口函數(shù)莱没,它是 Vue.js 對(duì)外暴露的一個(gè)函數(shù)。
createApp 主要做了兩件事情:創(chuàng)建 app 對(duì)象和重寫 app.mount 方法酷鸦。
- 創(chuàng)建 app 對(duì)象
使用 ensureRenderer().createApp() 來創(chuàng)建 app 對(duì)象
const app = ensureRenderer().createApp(...args)
ensureRenderer() 用來創(chuàng)建一個(gè)渲染器對(duì)象,用 ensureRenderer() 來延時(shí)創(chuàng)建渲染器饰躲,這樣做的好處是當(dāng)用戶只依賴響應(yīng)式包的時(shí)候,就不會(huì)創(chuàng)建渲染器臼隔,因此可以通過 tree-shaking 的方式移除核心渲染邏輯相關(guān)的代碼属铁。
- 重寫 app.mount 方法
為什么要重寫這個(gè)方法,而不把相關(guān)邏輯放在 app 對(duì)象的 mount 方法內(nè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)的組件渲染流程:標(biāo)準(zhǔn)的跨平臺(tái)渲染流程是先創(chuàng)建 vnode盒发,再渲染 vnode。
mount(rootContainer) {
// 創(chuàng)建根組件的 vnode
const vnode = createVNode(rootComponent, rootProps)
// 利用渲染器渲染 vnode
render(vnode, rootContainer)
app._container = rootContainer
return vnode.component.proxy
}
核心渲染流程:創(chuàng)建 vnode 和渲染 vnode
- 創(chuàng)建 vnode
vnode 有什么優(yōu)勢(shì)呢狡逢?為什么一定要設(shè)計(jì) vnode 這樣的數(shù)據(jù)結(jié)構(gòu)呢宁舰?
- 首先是抽象,引入 vnode奢浑,可以把渲染過程抽象化蛮艰,從而使得組件的抽象能力也得到提升。
- 其次是跨平臺(tái)雀彼,因?yàn)?patch vnode 的過程不同平臺(tái)可以有自己的實(shí)現(xiàn)壤蚜,基于 vnode 再做服務(wù)端渲染即寡、Weex 平臺(tái)、小程序平臺(tái)的渲染都變得容易了很多袜刷。
Vue.js 內(nèi)部是如何創(chuàng)建這些 vnode 的呢聪富?
通過 createVNode 函數(shù)創(chuàng)建了根組件的 vnode :對(duì) props 做標(biāo)準(zhǔn)化處理、對(duì) vnode 的類型信息編碼著蟹、創(chuàng)建 vnode 對(duì)象墩蔓,標(biāo)準(zhǔn)化子節(jié)點(diǎn) children 。
const vnode = createVNode(rootComponent, rootProps)
function createVNode(type, props = null
,children = null) {
if (props) {
// 處理 props 相關(guān)邏輯萧豆,標(biāo)準(zhǔn)化 class 和 style
}
// 對(duì) vnode 類型信息編碼
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ù)類型的 children 轉(zhuǎn)成數(shù)組或者文本類型
normalizeChildren(vnode, children)
return vnode
}
- 渲染 vnode
02 | 組件更新:完整的 DOM diff 流程是怎樣的?(上)
03 | 組件更新:完整的 DOM diff 流程是怎樣的涮雷?(下)
04 | Setup:組件渲染前的初始化過程是怎樣的阵面?
<template>
<button @click="increment">
Count is: {{ state.count }}, double is: {{ state.double }}
</button>
</template>
<script>
import { reactive, computed } from 'vue'
export default {
setup() {
const state = reactive({
count: 0,
double: computed(() => state.count * 2)
})
function increment() {
state.count++
}
return {
state,
increment
}
}
}
</script>
模板中引用到的變量 state 和 increment 包含在 setup 函數(shù)的返回對(duì)象中,那么它們是如何建立聯(lián)系的呢份殿?
在Vue.js 2.x 編寫組件的時(shí)候膜钓,會(huì)在 props、data卿嘲、methods颂斜、computed 等 options 中定義一些變量。在組件初始化階段拾枣,Vue.js 內(nèi)部會(huì)處理這些 options沃疮,即把定義的變量添加到了組件實(shí)例上。等模板編譯成 render 函數(shù)的時(shí)候梅肤,內(nèi)部通過 with(this){} 的語法去訪問在組件實(shí)例中的變量司蔬。
模板中引用到的變量 state 和 increment 包含在 setup 函數(shù)的返回對(duì)象中,那么它們是如何建立聯(lián)系的呢姨蝴?
到了 Vue.js 3.0俊啼,既支持組件定義 setup 函數(shù),而且在模板 render 的時(shí)候左医,又可以訪問到 setup 函數(shù)返回的值授帕,這是如何實(shí)現(xiàn)的?
Vue.js 2.x 使用 new Vue 來初始化一個(gè)組件的實(shí)例浮梢,到了 Vue.js 3.0跛十,我們直接通過創(chuàng)建對(duì)象去創(chuàng)建組件的實(shí)例。這兩種方式并無本質(zhì)的區(qū)別秕硝,都是引用一個(gè)對(duì)象芥映,在整個(gè)組件的生命周期中去維護(hù)組件的狀態(tài)數(shù)據(jù)和上下文環(huán)境。
創(chuàng)建和設(shè)置組件實(shí)例
組件的渲染流程:創(chuàng)建 vnode 、渲染 vnode 和生成 DOM奈偏。
渲染 vnode 的過程主要就是在掛載組件坞嘀,掛載組件的代碼主要做了三件事情:創(chuàng)建組件實(shí)例、設(shè)置組件實(shí)例和設(shè)置并運(yùn)行帶副作用的渲染函數(shù)霎苗。
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)
}
使用createComponentInstance 方法姆吭,創(chuàng)建組件實(shí)例
function createComponentInstance (vnode, parent, suspense) {
// 繼承父組件實(shí)例上的 appContext,如果是根組件唁盏,則直接從根 vnode 中取内狸。
const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
const instance = {
// 組件唯一 id
uid: uid++,
// 組件 vnode
vnode,
…
}
// 初始化渲染上下文
instance.ctx = { _: instance }
// 初始化根組件指針
instance.root = parent ? parent.root : instance
// 初始化派發(fā)事件方法
instance.emit = emit.bind(null, instance)
return instance
}
組件實(shí)例 instance 上定義了很多屬性.
Vue.js 2.x 使用 new Vue 來初始化一個(gè)組件的實(shí)例,到了 Vue.js 3.0厘擂,我們直接通過創(chuàng)建對(duì)象去創(chuàng)建組件的實(shí)例昆淡。這兩種方式并無本質(zhì)的區(qū)別,都是引用一個(gè)對(duì)象刽严,在整個(gè)組件的生命周期中去維護(hù)組件的狀態(tài)數(shù)據(jù)和上下文環(huán)境昂灵。
組件實(shí)例的設(shè)置流程就是對(duì)setup 函數(shù)的處理。
function setupComponent (instance, isSSR = false) {
const { props, children, shapeFlag } = instance.vnode
// 判斷是否是一個(gè)有狀態(tài)的組件
const isStateful = shapeFlag & 4
// 初始化 props
initProps(instance, props, isStateful, isSSR)
// 初始化 插槽
initSlots(instance, children)
// 設(shè)置有狀態(tài)的組件實(shí)例
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
return setupResult
}
setupStatefulComponent 函數(shù),它主要做了三件事:創(chuàng)建渲染上下文代理舞萄、判斷處理 setup 函數(shù)和完成組件實(shí)例設(shè)置眨补。