千层套路 - Vue 3.0 初始化源码探秘
關(guān)注若川視野, 回復(fù)"pdf" 領(lǐng)取資料,回復(fù)"1",可加群長期交流學(xué)習(xí)
劉崇楨,微醫(yī)云服務(wù)團隊前端工程師,左手抱娃、右手持家的非典型碼農(nóng)。
9 月初 Vue.js 3.0 正式發(fā)布,代號 "One Piece"。大秘寶都擺到眼巴前了,再不扒拉扒拉就說不過去了。那我們就從初始化開始。
目標(biāo):
弄清楚 createApp(App).mount("#app") 到底做了什么
弄清楚 Vue3.0 的初始化渲染是怎么樣的過程
能收獲到什么:
了解 Vue3.0 的初始化過程
介紹一個閱讀 Vue3.0 源碼的入口和方向
先跑起來
將 vue-next 代碼克隆到本地,打開 package.json 將 scripts dev 末尾加上 --sourcemap。
然后 ?yarn dev,vue 目錄下的 ?dist ?打包出了一份 ?vue.global.js 和相應(yīng)的 sourcemap 文件。這樣方便我們一步一步調(diào)試代碼,查看程序在 call Stack 中的每一步調(diào)用。
查看 vue 官方給出的 demo,發(fā)現(xiàn) vue 的使用分為 classic 和 composition,我們先用 classic 方式,實現(xiàn)一個最簡單的 demo。
const app = {data () {return {counter: 1}} } Vue.createApp(app).mount("#app")ok,頁面跑起來了。我們就在這段代碼打個斷點,然后一步一步的調(diào)試,觀察createApp(App).mount("#app")到底做了什么,了解Vue3.0的初始化過程。
在這之前,簡單了解一下整體的背景,我們這次主要涉及到 runtime 運行時的代碼。
runtime-dom
我們先跟著代碼進入:createApp(App).mount("#app");
這個 createApp() 來自 runtime-dom,我們通過這個圖可以看到他大致做的事情:return 了一個注冊了 mount 方法 app。這樣我們的 demo 至少能跑起來不報錯。
createApp 調(diào)用了 ensureRenderer 方法,他確保你能得到一個 renderer 渲染器。renderer 是通過調(diào)用創(chuàng)建渲染器的 createRenderer 來生成的,這個 createRenderer 來自于 runtime-core,后面我們會看到。
而這個 rendererOptions 是什么呢?
const?rendererOptions?=?extend({?patchProp,?forcePatchProp?},?nodeOps);export?const?nodeOps:?Omit<RendererOptions<Node,?Element>,?"patchProp">?=?{insert:?(child,?parent,?anchor)?=>?{parent.insertBefore(child,?anchor?||?null);},remove,createElement,createText,//?... };是不是就是一些 DOM API 的高階封裝,這個在 vue 的生態(tài)中,叫平臺特性。vue 源碼中的平臺特性就是針對 web 平臺的。如果開發(fā)者想要在別的平臺上運行 vue,比如 mpvue、weex,就不需要 fork 源碼庫改源碼了,直接把 nodeOps 中的方法按著平臺的特性逐一實現(xiàn)就可以了。這也是 createRenderer 等跨平臺的代碼放到 runtime-core 中的原因。
當(dāng)然 runtime-dom 遠遠不只圖中這些東西,我們先大致過一下初始化過程,以對 vue3.0 有一個大致的了解。
runtime-core
緊接著,進入 runtime-core,創(chuàng)建渲染器
我們注意 baseCreateRenderer 這個 fn,2000 多行的代碼量,里面的東西都是渲染的核心代碼,從平臺特性 options 取出相關(guān) API,實現(xiàn)了 patch、處理節(jié)點、處理組件、更新組件、安裝組件實例等等方法,最終返回了一個對象。這里我們看到了【2】中渲染器調(diào)用的 createApp 方法,他是通過 createAppAPI 創(chuàng)建的。代碼進入 createAppAPI。
這里我們又看見了熟悉的 Vue2.x 中的 API,掛載在 app 上面。
至此,Vue.createApp(app).mount("#app"),創(chuàng)建 app 實例的流程,終于在【7】中 return app 告一段落,我們拿到了【2】中的 app 實例。
大致瞄一眼 app ,我們可以在 apiCreateApp.ts 中找到其實現(xiàn)
初次渲染 .mount("#app")
上面的介紹中,其實有兩處 .mount 的實現(xiàn),一處是在 runtime-dom【2】中的 mount,我們叫他 dom-mount。一處是【7】中的 mount,我們叫他 core-mount。
dom-mount的實現(xiàn):
const?{?mount?}?=?app;?//?先暫存'core-mount' app.mount?=?(containerOrSelector:?Element?|?string):?any?=>?{const?container?=?normalizeContainer(containerOrSelector);?//?#app?dom?節(jié)點if?(!container)?return;const?component?=?app._component;if?(!isFunction(component)?&&?!component.render?&&?!component.template)?{component.template?=?container.innerHTML;?//?平臺特性的邏輯}//?clear?content?before?mountingcontainer.innerHTML?=?"";const?proxy?=?mount(container);?//?執(zhí)行'core-mount'container.removeAttribute("v-cloak");return?proxy; };dom-mount 并不是重寫 core-mount,而是提取了平臺特性的邏輯。比如上面如果 component 不是 function,又沒有 render、template,就讀取 dom 節(jié)點內(nèi)部的 html 作為渲染模板。
然后再執(zhí)行 core-mount,mount(container)。
代碼很簡單,就兩步:
創(chuàng)建根組件的 vnode
渲染這個 vnode
創(chuàng)建根組件的vnode
創(chuàng)建 vnode,是一個初始化 vnode 的過程,這個階段中,下面的這些屬性被初始化為具體的值(還有很多屬性沒有羅列,都是初始值)。
當(dāng) vnode 描述不同的事物時,他的屬性值也各不相同,這些在 vnode 初始化階段確定的屬性在渲染組件時,能帶來非常重要的效率提升。
type,標(biāo)識 VNode 的種類
html 標(biāo)簽的描述,type 屬性就是一個字符串,即標(biāo)簽的名字
組件的描述,type 屬性就是引用組件類(或函數(shù))本身
文本節(jié)點的描述,type 屬性就是 null
patchFlag,標(biāo)識組件變化的地方
shapeFlag,VNode 的標(biāo)識,標(biāo)明 VNode 屬于哪一類,demo 中的shapeFlag 是 4:STATEFUL_COMPONENT,有狀態(tài)的組件。
在packages/shared/src/shapeFlags.ts中,定義了這些通過將十進制數(shù)字 1 左移不同的位數(shù)得來的枚舉值。
export?const?enum?ShapeFlags?{ELEMENT?=?1,?//?1?-?html/svg?標(biāo)簽FUNCTIONAL_COMPONENT?=?1?<<?1,?//?2?-?函數(shù)式組件STATEFUL_COMPONENT?=?1?<<?2,?//?4?-?有狀態(tài)組件TEXT_CHILDREN?=?1?<<?3,?//?8ARRAY_CHILDREN?=?1?<<?4,?//?16SLOTS_CHILDREN?=?1?<<?5,?//?32TELEPORT?=?1?<<?6,?//?64SUSPENSE?=?1?<<?7,?//?128COMPONENT_SHOULD_KEEP_ALIVE?=?1?<<?8,?//?256?-?需要被?keepAlive?的有狀態(tài)組件COMPONENT_KEPT_ALIVE?=?1?<<?9,?//?512?-?已經(jīng)被?keepAlive?的有狀態(tài)組件COMPONENT?=?ShapeFlags.STATEFUL_COMPONENT?|?ShapeFlags.FUNCTIONAL_COMPONENT?//?組件 }為什么為 VNode 標(biāo)識這些枚舉值呢?在 Vue2.x 的 patch 過程中,代碼通過 createElm 區(qū)分 VNode 是 html 還是組件或者 text 文本。
所以 Vue2.x 的 patch 是一個試錯過程,在這個階段是有很大的性能損耗的。Vue3.0 把對 VNode 的判斷放到了創(chuàng)建的時候,這樣在 patch 的時候就能避免消耗性能的判斷。
最終,我們看一下 vnode 的結(jié)構(gòu)
export?interface?VNode<HostNode?=?RendererNode,HostElement?=?RendererElement,ExtraProps?=?{?[key:?string]:?any?} >?{/***?@internal*/__v_isVNode:?true?//?一個始終為?true?的值,有了它,我們就可以判斷一個對象是否是?VNode?對象/***?@internal?內(nèi)部屬性*/[ReactiveFlags.SKIP]:?truetype:?VNodeTypesprops:?(VNodeProps?&?ExtraProps)?|?nullkey:?string?|?number?|?nullref:?VNodeNormalizedRef?|?nullscopeId:?string?|?null?//?SFC?onlychildren:?VNodeNormalizedChildrencomponent:?ComponentInternalInstance?|?nulldirs:?DirectiveBinding[]?|?nulltransition:?TransitionHooks<HostElement>?|?null//?DOM?相關(guān)el:?HostNode?|?nullanchor:?HostNode?|?null?//?fragment?anchortarget:?HostElement?|?null?//?teleport?targettargetAnchor:?HostNode?|?null?//?teleport?target?anchorstaticCount:?number?//?number?of?elements?contained?in?a?static?vnode//?suspense?支持?suspense?的屬性suspense:?SuspenseBoundary?|?nullssContent:?VNode?|?nullssFallback:?VNode?|?null//?optimization?only?優(yōu)化模式中使用的屬性shapeFlag:?numberpatchFlag:?numberdynamicProps:?string[]?|?nulldynamicChildren:?VNode[]?|?null//?application?root?node?onlyappContext:?AppContext?|?null }渲染這個vnode
ok,書接上回,我們拿到 根組件的 VNode,接下來執(zhí)行到 render 函數(shù)。
render 的核心邏輯就是 patch 函數(shù)。
patch 函數(shù)
patch 有兩種含義: 1)整個虛擬 dom 映射到真實 dom 的過程;2)patch 函數(shù)。我們這里講的是函數(shù)。
patch 就是 render 渲染組件的關(guān)鍵邏輯,【5】中 baseCreateRenderer 2000 行左右的代碼,主要是為了 patch 服務(wù)的。
//?patching?&?not?same?type,?unmount?old?tree if?(n1?&&?!isSameVNodeType(n1,?n2))?{anchor?=?getNextHostNode(n1)unmount(n1,?parentComponent,?parentSuspense,?true)n1?=?null } //?對于前后節(jié)點類型不同的,vue 是直接卸載之前的然后重新渲染新的,不會考慮可能的子節(jié)點復(fù)用。 ...const?{?type,?ref,?shapeFlag?}?=?n2 switch?(type)?{?//?根據(jù)節(jié)點類型?type?分發(fā)到不同的?processcase?Text:processText(n1,?n2,?container,?anchor)breakcase?Comment:processCommentNode(n1,?n2,?container,?anchor)breakcase?Static:...case?Fragment:?...default:?//?根據(jù)不同的節(jié)點標(biāo)識?shapeFlag?分發(fā)到不同的?processif?(shapeFlag?&?ShapeFlags.ELEMENT)?{?processElement(...)?}?else?if?(shapeFlag?&?ShapeFlags.COMPONENT)?{processComponent(...)...patch 根據(jù)節(jié)點 VNode(4.1 創(chuàng)建的根組件的 vnode) 的 type 和 shapeFlags 執(zhí)行不同的 process。
type:Text 文本
type:Comment 注釋
type:Static 靜態(tài)標(biāo)簽
type:Fragment 片段:VNode 的類型是 Fragment,就只需要把該 VNode 的子節(jié)點渲染到頁面。有了他,就沒有只能有一個根節(jié)點的限制,也可以做到組件平級遞歸
shapeFlags:ShapeFlags.ELEMENT 原生節(jié)點,html/svg 標(biāo)簽
shapeFlags:ShapeFlags.COMPONENT 組件節(jié)點
shapeFlags:ShapeFlags.TELEPORT 傳送節(jié)點,將組件渲染的內(nèi)容傳送到制定的 dom 節(jié)點中
shapeFlags:ShapeFlags.SUSPENSE 掛起節(jié)點(異步渲染)
Vue3 新增組件 - Fragment、Teleport、Suspense,可見此鏈接 (https://www.yuque.com/hugsun/vue3/component)
我們的 demo 中的根組件 VNode 的 shapeFlag 是 4(0100),ShapeFlags.COMPONENT(0110),按位與后結(jié)果為非零,代碼會進入 processCompoent。
processXXX
processXXX 是對掛載(mount)和更新(update)補丁的統(tǒng)一操作入口。
processXXX 會根據(jù)節(jié)點是否是初次渲染,進行不同的操作。
如果沒有老的 VNode,就掛載組件(mount)。首次掛載,遞歸創(chuàng)建真實節(jié)點。
如果有老的 VNode,就更新組件(update)。更新補丁的的渲染系統(tǒng)的介紹放到下下篇來介紹。
掛載
創(chuàng)建組件內(nèi)部實例
內(nèi)部實例也會暴露一些實例屬性給其他更高級的庫或工具使用。組件實例屬性很多很重要也能幫助理解,可以在 packages/runtime-core/src/component.ts 查看實例的接口聲明 ComponentInternalInstance。很壯觀啊,啪的一下 100 多行屬性的定義,主要包括基本屬性、響應(yīng)式 state 相關(guān)、suspense 相關(guān)、生命周期鉤子等等
安裝組件實例
初始化 props 和 slots
安裝有狀態(tài)的組件,這里會初始化組件的響應(yīng)式
【15】setupStatefulComponent,調(diào)用了 setup(props, setupContext)。
如果沒有 setup 時會調(diào)用 applyOptions,應(yīng)用 vue2.x 的 options API,最終對 data() 的響應(yīng)式處理也是使用 vue3.0 的 reactive。
上面講過,安裝組件實例觸發(fā)響應(yīng)式初始化就發(fā)生在這里,具體怎么觸發(fā)的,這塊又是一個千層套路,放到下一篇中。
【16】主要是根據(jù) template 拿到組件的 render 渲染函數(shù)和應(yīng)用 vue2.x 的 options API。
我們看一下 template 模板編譯后生成的 render 函數(shù)。
我們大致看下生成的 render 函數(shù),有幾點需要注意
這里的 render 函數(shù)執(zhí)行后的返回是組件的 VNode
_createVNode 函數(shù),用于創(chuàng)建 VNode
_createVNode函數(shù)的入?yún)?#xff0c;type、patchFlags、dynamicProps等
createVNode 在創(chuàng)建根節(jié)點的時候就出現(xiàn)過,用于創(chuàng)建虛擬 DOM。這個是內(nèi)部使用的 API,面向用戶的 API 還是h函數(shù)。
export?function?h(type:?any,?propsOrChildren?:?any,?children?:?any):?VNode?{?...?}h 的實現(xiàn)也是調(diào)用 createVNode,但是沒有 patchFlag、dynamicProps、isBlockNode 這三個參數(shù)。也就是 h 是沒有 optimization 的,應(yīng)該是因為這三個參數(shù),讓用戶自己算容易出錯。
看來這個 patchFlags 有點意思,標(biāo)識組件變化的地方,用于 patch 的 diff 算法優(yōu)化。
export?const?enum?PatchFlags?{TEXT?=?1,?//?動態(tài)文字內(nèi)容CLASS?=?1?<<?1,?//?[2]動態(tài)?class?綁定STYLE?=?1?<<?2,?//?[4]動態(tài)樣式PROPS?=?1?<<?3,?//?[8]動態(tài)?props,不是?class?和?style?的動態(tài)?propsFULL_PROPS?=?1?<<?4,?//?[16]有動態(tài)的 key,也就是說 props 對象的 key 不是確定的。key 變化時,進行一次 full diffHYDRATE_EVENTS?=?1?<<?5,?//?[32]STABLE_FRAGMENT?=?1?<<?6,?//?[64]children?順序確定的?fragmentKEYED_FRAGMENT?=?1?<<?7,?//?[128]children?中有帶有?key?的節(jié)點的?fragmentUNKEYED_FRAGMENT?=?1?<<?8,?//?[256]沒有?key?的?children?的?fragmentNEED_PATCH?=?1?<<?9,?//?[512]DYNAMIC_SLOTS?=?1?<<?10,?//?[1024]動態(tài)的插槽//?SPECIAL?FLAGS?-------------------------------------------------------------//?以下是特殊的?flag,負(fù)值HOISTED?=?-1,?//?表示他是靜態(tài)節(jié)點,他的內(nèi)容永遠不會改變BAIL?=?-2,?//?用來表示一個節(jié)點的?diff?應(yīng)該結(jié)束 }之所以使用位運算,是因為
用 | 來進行復(fù)合,TEXT | PROPS得到0000 1001,即十進制 9。標(biāo)識他既有動態(tài)文字內(nèi)容,也有動態(tài) props。
用 & 進行 check,patchFlag & TEXT,0000 1001 & 0000 0001,得到0000 0001,只要結(jié)果大于 0,就說明屬性命中。
方便擴展、計算更快...
patchFlag 被賦值到 VNode 的屬性中,他在后面更新節(jié)點時會被用到。為了配合代碼的正常流轉(zhuǎn),先放一放,代碼繼續(xù) F10。如果你去調(diào)試代碼,會發(fā)現(xiàn)這真的是千層套路啊,一直 shift + F11 跳出代碼到懷疑人生,才終于回到 mountComponent...
總結(jié)一下 setupComponent 安裝組件實例,主要做了什么事情:initProps、initSlots、響應(yīng)式初始化、得到模板的 render 函數(shù)等等。
回顧前文,跳出到【13】,setup 安裝組件實例后,下一步是 setupRenderEffect 激活渲染函數(shù)的副作用
激活渲染函數(shù)的副作用 setupRenderEffect
實現(xiàn)基于【21】,effect 副作用,意味著響應(yīng)式數(shù)據(jù)變化后引起的變更。effect 源自 reactive,傳入一個 fn 得到一個 reactiveEffect。
effect 的入?yún)?componentEffect 是一個命名函數(shù),會立即執(zhí)行。componentEffect 執(zhí)行過程中,觸發(fā)響應(yīng)式數(shù)據(jù)的 getter 攔截,會在全局?jǐn)?shù)據(jù)響應(yīng)關(guān)系倉庫記錄當(dāng)前componentEffect。在響應(yīng)式對象發(fā)生改變時,派發(fā)更新,執(zhí)行componentEffect。
回到componentEffect
function?componentEffect()?{if?(!instance.isMounted)?{let?vnodeHook:?VNodeHook?|?null?|?undefinedconst?{?el,?props?}?=?initialVNodeconst?{?bm,?m,?parent?}?=?instance//?beforeMount?hook?生命周期鉤子函數(shù)if?(bm)?{invokeArrayFns(bm)}...//?subTree?根節(jié)點的?subTree,通過?renderComponentRoot?根據(jù)?render?生成的?vnode//大家回憶一下 render 是什么?是不是根組件的 template 編譯后得到的好多_createVNode 的渲染器函數(shù)?const?subTree?=?(instance.subTree?=?renderComponentRoot(instance))...//?更新patch(null,?subTree,?container,?...)...if?(m)?{?//?parent?的?mounted?執(zhí)行之前,先執(zhí)行?subTree?的?patchqueuePostRenderEffect(m,?parentSuspense)}...instance.isMounted?=?true?//?標(biāo)志實例已掛載}?else?{?...?} }執(zhí)行前面編譯后得到的渲染函數(shù) render,生成subTree: vnode
最后執(zhí)行 patch,上文中渲染根節(jié)點的 vnode 時執(zhí)行過 patch,這里就進入了一個大循環(huán),根據(jù)組件的 children 的 type 和 shapeFlag,baseCreateRenderer 會繼續(xù)進行各種 processXXX 處理,直至基于 平臺特性 的 DOM 操作 掛載到各自的父節(jié)點中。
這個順序是深度遍歷的過程,子節(jié)點的 patch 完成之后再進行父節(jié)點的 mounted。
patch 循環(huán) && subTree 一覽
//?subTree?的?模板?template <div?id="app"><h1>composition-api</h1><p?@click="add"?:attr-key="counter">{{counter}}</p><p?:class="{'counter':?counter?%?2}">{{doubleCounter}}</p> </div>//?patchFlag:?64? // STABLE_FRAGMENT = 1 << 6, // 64 表示:children 順序確定的 fragment //?shapeFlag:?16 //?ARRAY_CHILDREN?=?1?<<?4,?//?16?觀察上面這個模板,Vue2.x 中的模板只能有一個根元素,Vue3.0 的這個 demo 中有三個根元素,這得益于新增的 fragment 組件。
vnode 標(biāo)識出來 patchFlag:64,表示 children 順序確定的 fragment;
vnode 標(biāo)識出來 shapeFlag:16,表示當(dāng)前節(jié)點是一個孩子數(shù)組。
vnode 標(biāo)識出來 dynamicChildren,標(biāo)識動態(tài)變化的孩子節(jié)點。顯然是兩個 p 標(biāo)簽,可以想象這個數(shù)組的元素也是當(dāng)前呈現(xiàn)的 vnode,只不過具體屬性值不同罷了
等等,還有 4 嗎,我不知道...
當(dāng)然還有,processxxx 中一般都會判斷是掛載還是更新,更新的時候就會用到 patchFlag,比如 patchElement... 下次一定
等等,還有 5 嗎,我不知道...
當(dāng)然還有,第五層我就已經(jīng)裂開了啊...
あ:あげない??????あ:不給你哦~????????????? い:いらない,????い:不要了啦~????????????? う:うごけない????う:動不了了~????????????? え:えらべない????え:不會選嘛~????????????? お:おせない??????お:按不到耶~?[裂開][裂開][裂開]剛看源碼不久,只能靠 F11 、參考其他文檔,憑自己的理解寫出這樣的文章,肯定有很多理解不對的地方,希望得到批判指正。
附錄
Vue3初始化.drawio (https://www.yuque.com/office/yuque/0/2020/drawio/441847/1605880555730-4e18923f-c087-4082-af06-ec51986ba658.drawio?from=https%3A%2F%2Fwww.yuque.com%2Fdocs%2Fshare%2F64bd5cdc-3086-4154-a447-04032d161830%3F%23)
推薦閱讀
我在阿里招前端,我該怎么幫你?(現(xiàn)在還可以加模擬面試群)
如何拿下阿里巴巴 P6 的前端 Offer
如何準(zhǔn)備阿里P6/P7前端面試--項目經(jīng)歷準(zhǔn)備篇
大廠面試官常問的亮點,該如何做出?
如何從初級到專家(P4-P7)打破成長瓶頸和有效突破
若川知乎問答:2年前端經(jīng)驗,做的項目沒什么技術(shù)含量,怎么辦?
若川知乎高贊:有哪些必看的 JS庫?
末尾
你好,我是若川,江湖人稱菜如若川,歷時一年只寫了一個學(xué)習(xí)源碼整體架構(gòu)系列~(點擊藍字了解我)
關(guān)注若川視野,回復(fù)"pdf" 領(lǐng)取優(yōu)質(zhì)前端書籍pdf,回復(fù)"1",可加群長期交流學(xué)習(xí)
我的博客地址:https://lxchuan12.gitee.io?歡迎收藏
覺得文章不錯,可以點個在看呀^_^另外歡迎留言交流~
小提醒:若川視野公眾號面試、源碼等文章合集在菜單欄中間【源碼精選】按鈕,歡迎點擊閱讀,也可以星標(biāo)我的公眾號,便于查找
總結(jié)
以上是生活随笔為你收集整理的千层套路 - Vue 3.0 初始化源码探秘的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: react(87)--控制禁用disab
- 下一篇: OSMDroid —— 开源的 Andr