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