關注
若川視野
, 回復"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
等
function?_createVNode(type:?VNodeTypes?|?ClassComponent?|?typeof?NULL_DYNAMIC_COMPONENT,?//?type,標識?VNode?的種類props:?(Data?&?VNodeProps)?|?null?=?null,children:?unknown?=?null,patchFlag:?number?=?0,?//?標記節點動態變化的地方dynamicProps:?string[]?|?null?=?null,?//?動態?propsisBlockNode?=?false
):?VNode?{?...?}
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?歡迎收藏
覺得文章不錯,可以點個
在看
呀^_^另外歡迎留言
交流~
小提醒:若川視野公眾號面試、源碼等文章合集在菜單欄中間
【源碼精選】
按鈕,歡迎點擊閱讀,也可以星標我的公眾號,便于查找