源碼淺析Vue3中的組件掛載
前言
前面我們一起探討了 Vue3 的 響應(yīng)式原理 和 編譯過程,render函數(shù)我們已經(jīng)拿到了,那具體到底要怎么用呢?這一節(jié),我們就開啟一個(gè)新篇章——組件的掛載。
組件掛載/更新函數(shù)——setupRenderEffect
組件掛載和更新的核心函數(shù)都是setupRenderEffect,這里我們就從setupRenderEffect函數(shù)作為切入點(diǎn):
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => { const componentUpdateFn = () => { if (!instance.isMounted) { // 組件尚未掛載,執(zhí)行掛載操作 ... } else { // 組件已經(jīng)掛載,執(zhí)行更新操作 ... }; // 初始化響應(yīng)式副作用函數(shù) const effect = instance.effect = new ReactiveEffect( componentUpdateFn, () => queueJob(update), instance.scope ); const update = instance.update = () => effect.run(); update.id = instance.uid; ... // 執(zhí)行副作用函數(shù) update(); };
我們可以看到在 setupRenderEffect 函數(shù)中:
- 首先,定義了一個(gè) 組件掛載/更新函數(shù) componentUpdateFn,該函數(shù)會(huì)根據(jù)組件實(shí)例的是否已經(jīng)掛載來進(jìn)行不同的操作。
- 然后,將 掛載/更新函數(shù) componentUpdateFn 包裝為一個(gè) effect副作用函數(shù)。
- 最后,執(zhí)行副作用函數(shù),完成掛載或更新操作。
可以看出,組件更新的核心就在于 componentUpdateFn 函數(shù),接下來我們深入來看一下這個(gè)函數(shù)內(nèi)部都執(zhí)行了哪些操作。
componentUpdateFn
我們首先來看實(shí)例尚未掛載的情況下,componentUpdateFn函數(shù)是如何處理掛載的:
const componentUpdateFn = () => { // 組件尚未掛載,執(zhí)行掛載操作 if (!instance.isMounted) { let vnodeHook; const { el, props } = initialVNode; const { bm, m, parent } = instance; ... // 將實(shí)例上的allowRecurse屬性(允許遞歸)設(shè)置為false toggleRecurse(instance, false); // 如果存在onBeforeMount生命周期函數(shù) if (bm) { // 執(zhí)行onBeforeMount中的函數(shù) invokeArrayFns(bm); } ... // 將實(shí)例上的allowRecurse屬性(允許遞歸)設(shè)置為true toggleRecurse(instance, true); if (el && hydrateNode) { ... } else { // 生成子樹的vnode const subTree = (instance.subTree = renderComponentRoot(instance)); // 掛載子樹vnode到容器中 patch(null, subTree, container, anchor, instance, parentSuspense, isSVG); initialVNode.el = subTree.el; } ... // 將實(shí)例上的isMounted屬性設(shè)置為true instance.isMounted = true; initialVNode = container = anchor = null; } else { // 組件已經(jīng)掛載過,執(zhí)行更新操作 ... };
componentUpdateFn在處理組件掛載時(shí)主要做的事情就是:
- 首先,判斷組件是否存在beforeMount生命周期函數(shù),如果存在,則執(zhí)行內(nèi)部定義的函數(shù)。
- 然后,根據(jù)實(shí)例
instance
生成子樹vnode。 - 之后,通過patch函數(shù),將子樹vnode掛載到容器。(因?yàn)槟壳笆菕燧d階段,所以patch函數(shù)第一個(gè)參數(shù)默認(rèn)設(shè)定為了null)
- 最后,將對(duì)應(yīng)的屬性值isMounted進(jìn)行相關(guān)配置,將變量指針置空。
接下來,我們進(jìn)入renderComponentRoot函數(shù),看一看生成子樹vnode的整個(gè)過程是怎樣的。
生成vnode的函數(shù)——renderComponentRoot
function renderComponentRoot( instance: ComponentInternalInstance ): VNode { ... let result ... const proxyToUse = withProxy || proxy // 取出render函數(shù),并執(zhí)行 result = normalizeVNode( render!.call( proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx ) ) ... return result }
上面我們抽離出該函數(shù)的核心,可以看到,renderComponentRoot函數(shù)的關(guān)鍵邏輯就是執(zhí)行了render函數(shù)。
前面章節(jié)中,我們已經(jīng)介紹了render函數(shù)生成的過程,我們還用之前的例子:
模板template:
<div> <span> {{x}} </span> <div>123</div> </div>
經(jīng)過編譯后,生成的render函數(shù)是這個(gè)樣子的:
function render(_ctx, _cache) { with (_ctx) { const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue return (_openBlock(), _createElementBlock("div", null, [ _createElementVNode("span", null, _toDisplayString(x), 1 /* TEXT */), _hoisted_1 ])) } }
最終,我們通過執(zhí)行上面的render函數(shù),得到節(jié)點(diǎn)的虛擬DOM也就是vnode
:
然后,我們將這樣的一個(gè)數(shù)據(jù)結(jié)構(gòu)(vnode)傳入normalizeVNode函數(shù)中做進(jìn)一步的處理,我們看一下normalizeVNode函數(shù)又做了什么操作:
function normalizeVNode(child) { // 如果節(jié)點(diǎn)vnode為空,則創(chuàng)建為注釋節(jié)點(diǎn) if (child == null || typeof child === "boolean") { return createVNode(Comment); } else if (isArray(child)) { // 如果節(jié)點(diǎn)為數(shù)組,則在外層包裹一層根節(jié)點(diǎn)fragment return createVNode( Fragment, null, child.slice() ); } else if (typeof child === "object") { // 如果是對(duì)象形式 return cloneIfMounted(child); } else { // 其他情況,比如創(chuàng)建文本類型的節(jié)點(diǎn) return createVNode(Text, null, String(child)); } } function cloneIfMounted(child) { // 如果節(jié)點(diǎn)已經(jīng)掛載,則直接返回對(duì)應(yīng)的vnode,否則克隆一份返回 return child.el === null && child.patchFlag !== -1 /* HOISTED */ || child.memo ? child : cloneVNode(child); }
在這個(gè)函數(shù)中會(huì)對(duì)傳入的參數(shù)進(jìn)行分情況討論:
- 如果參數(shù)
vnode
為空,則創(chuàng)建為注釋節(jié)點(diǎn)。 - 如果參數(shù)
vnode
為數(shù)組,則在外層包裹一層根節(jié)點(diǎn)Fragment,再執(zhí)行創(chuàng)建vnode的函數(shù)。 - 如果參數(shù)
vnode
為對(duì)象形式,則直接返回或克隆該節(jié)點(diǎn)vnode。 - 其他情況,主要是像文本類型節(jié)點(diǎn)的處理。
我們傳入的 child參數(shù) 是一個(gè)對(duì)象形式,所以會(huì)最終執(zhí)行的是cloneIfMounted函數(shù),而這個(gè)函數(shù)中,會(huì)去判斷 vnode節(jié)點(diǎn) 是否已經(jīng)被掛載過,如果已經(jīng)執(zhí)行過掛載操作,那么其 vnode
的el屬性上就會(huì)被賦值,該函數(shù)就直接將原vnode節(jié)點(diǎn)返回,否則,執(zhí)行拷貝操作再返回。
這里,因?yàn)榈慕M件在該階段還未掛載,所以normalizeVNode函數(shù)最終的返回結(jié)果也是直接將上面render函數(shù)生成的vnode直接返回,而我們最終renderComponentRoot函數(shù)的返回值同樣也是執(zhí)行render函數(shù)得到的vnode。
至此,我們大概理清楚了生成子樹vnode
的函數(shù)renderComponentRoot的邏輯,它的主要工作就是通過執(zhí)行模板編譯后生成的render函數(shù),再進(jìn)行相應(yīng)的處理,得到最終的vnode。
掛載/更新函數(shù)patch
接下來我們開啟下一個(gè)環(huán)節(jié),也是vue中極其重要的一個(gè)函數(shù)——patch函數(shù)。
patch直譯過來就是“補(bǔ)丁”的意思,可以理解為在vue中,組件的掛載和更新都是通過打“補(bǔ)丁”的方式來進(jìn)行的。當(dāng)然,打“補(bǔ)丁”前要先比對(duì)一下,看看兩個(gè)節(jié)點(diǎn)到底是哪些信息不一樣了,然后再進(jìn)行定點(diǎn)的更新。
在進(jìn)入patch函數(shù)之前先說明一下patch函數(shù)的幾個(gè)關(guān)鍵參數(shù):
n1
: 舊vnode節(jié)點(diǎn)n2
: 新vnode節(jié)點(diǎn)container
: 掛載的容器anchor
: 掛載的參考元素
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = isHmrUpdating ? false : !!n2.dynamicChildren) => { // 如果新舊vnode節(jié)點(diǎn)相同,則無須patch if (n1 === n2) { return; } // 如果新舊vnode節(jié)點(diǎn),type類型不同,則直接卸載舊節(jié)點(diǎn) // 這里isSameVNodeType會(huì)判斷規(guī)則為n1.type === n2.type && n1.key === n2.key if (n1 && !isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1); unmount(n1, parentComponent, parentSuspense, true); n1 = null; } ... const { type, ref: ref2, shapeFlag } = n2; // 根據(jù)新節(jié)點(diǎn)的類型,采用不同的函數(shù)進(jìn)行處理 switch (type) { // 處理文本 case Text: processText(n1, n2, container, anchor); break; // 處理注釋 case Comment: processCommentNode(n1, n2, container, anchor); break; // 處理靜態(tài)節(jié)點(diǎn) case Static: if (n1 == null) { mountStaticNode(n2, container, anchor, isSVG); } else if (true) { patchStaticNode(n1, n2, container, isSVG); } break; // 處理Fragment case Fragment: // Fragment ... break; default: if (shapeFlag & 1 /* ELEMENT */) { // element類型 processElement( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ); } else if (shapeFlag & 6 /* COMPONENT */) { // 組件 ... } else if (shapeFlag & 64 /* TELEPORT */) { // teleport ... } else if (shapeFlag & 128 /* SUSPENSE */) { // suspense ... } else if (true) { warn2("Invalid VNode type:", type, `(${typeof type})`); } } ... };
patch函數(shù)整體的處理邏輯就是:
- 比對(duì)新舊節(jié)點(diǎn),如果新舊節(jié)點(diǎn)相同,則無須處理。
- 如果新舊節(jié)點(diǎn)的類型不同,則直接將舊節(jié)點(diǎn)卸載(我們這一節(jié)主要研究掛載階段,所以舊節(jié)點(diǎn)為null,可以先不關(guān)注這一點(diǎn))。
- 根據(jù)新節(jié)點(diǎn)的類型,再分情況進(jìn)行處理。
這里,我們就用處理element類型來舉例,看一下processElement函數(shù),其他情況同理。
processElement函數(shù)
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => { isSVG = isSVG || n2.type === "svg"; if (n1 == null) { // 如果存在舊節(jié)點(diǎn) mountElement( n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ); } else { // 舊節(jié)點(diǎn)不存在 patchElement( n1, n2, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ); } };
processElement函數(shù),會(huì)根據(jù)舊節(jié)點(diǎn)是否存在進(jìn)行分情況討論,這里我們主要看掛載階段的函數(shù)——mountElement。
const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => { let el; let vnodeHook; const { type, props, shapeFlag, transition, dirs } = vnode; // 創(chuàng)建真實(shí)DOM結(jié)構(gòu),并將其保存在在vnode的el屬性上 el = vnode.el = hostCreateElement( vnode.type, isSVG, props && props.is, props ); // 處理文本節(jié)點(diǎn) if (shapeFlag & 8 /* TEXT_CHILDREN */) { hostSetElementText(el, vnode.children); } else if (shapeFlag & 16 /* ARRAY_CHILDREN */) { // 如果節(jié)點(diǎn)類型是數(shù)組,則遞歸的對(duì)子節(jié)點(diǎn)進(jìn)行處理 mountChildren( vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== "foreignObject", slotScopeIds, optimized ); } // 處理vnode上的指令相關(guān)內(nèi)容,并執(zhí)行指令的生命周期鉤子函數(shù) if (dirs) { invokeDirectiveHook(vnode, null, parentComponent, "created"); } setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent); // 處理props相關(guān)內(nèi)容 if (props) { for (const key in props) { if (key !== "value" && !isReservedProp(key)) { hostPatchProp( el, key, null, props[key], isSVG, vnode.children, parentComponent, parentSuspense, unmountChildren ); } } if ("value" in props) { hostPatchProp(el, "value", null, props.value); } if (vnodeHook = props.onVnodeBeforeMount) { invokeVNodeHook(vnodeHook, parentComponent, vnode); } } ... // 處理vnode上的指令相關(guān)內(nèi)容,并執(zhí)行指令的生命周期鉤子函數(shù) if (dirs) { invokeDirectiveHook(vnode, null, parentComponent, "beforeMount"); } ... // 將dom掛載到container hostInsert(el, container, anchor); ... };
在mountElement函數(shù)中,我們終于創(chuàng)建出了期待已久的真實(shí)DOM結(jié)構(gòu)。該函數(shù)的邏輯為:
根據(jù)vnode創(chuàng)建出真實(shí)DOM結(jié)構(gòu),并保存在el屬性上。
根據(jù)子節(jié)點(diǎn)類型來進(jìn)行不同的操作:
- 文本類型,直接生成文本節(jié)點(diǎn)
- 數(shù)組類型,則遞歸的處理子節(jié)點(diǎn)
對(duì)vnode的指令以及props內(nèi)容進(jìn)行處理。
最后將生成的DOM掛載到容器container上,也就最終呈現(xiàn)在頁面上了。
這里可能有的朋友就是想看一看document.createElement
這種API到底在哪里,那就提一下hostCreateElement函數(shù):
hostCreateElement函數(shù)在源碼中是通過解構(gòu)賦值并重命名得來的,它原來的名字叫createElement,改回本名瞬間就直觀了很多~
createElement: (tag, isSVG, is, props) => { const el = isSVG ? doc.createElementNS(svgNS, tag) : doc.createElement(tag, is ? { is } : void 0); if (tag === "select" && props && props.multiple != null) { ; el.setAttribute("multiple", props.multiple); } return el; } // hostInsert指向的函數(shù)就是insert insert: (child, parent, anchor) => { parent.insertBefore(child, anchor || null); },
兩個(gè)工具函數(shù)的邏輯也很好理解,就不再多說了吧。總之,終于是看到了document.createElement
就是舒服了^_^
最后
這一節(jié),我們深入研究了Vue3中,組件的掛載邏輯,整個(gè)的流程雖然過程繁瑣,但要做的事比較清晰,總結(jié)下來就是:
- 判斷當(dāng)前vnode是否已經(jīng)進(jìn)行過掛載操作,來決定是進(jìn)行掛載流程還是更新流程。
- 進(jìn)入掛載流程。
- 執(zhí)行模板編譯階段生成的render函數(shù),得到虛擬dom(vnode)。
- 通過patch函數(shù),對(duì)vnode進(jìn)行分類處理,同時(shí)在這個(gè)階段創(chuàng)建出真實(shí)的DOM結(jié)構(gòu)。
- 將創(chuàng)建的DOM掛載到容器container中,完成最終呈現(xiàn)。
到此這篇關(guān)于源碼淺析Vue3中的組件掛載的文章就介紹到這了,更多相關(guān)Vue3組件掛載內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
一文解決vue2 element el-table自適應(yīng)高度問題
在寫公司后臺(tái)項(xiàng)目的時(shí)候遇到一個(gè)需求,要求表格頁面不能有滾動(dòng)條,所以必須封裝一個(gè)公共方法來實(shí)現(xiàn)表格自適應(yīng)高度,本問小編給大家介紹了如何解決vue2 element el-table自適應(yīng)高度問題,需要的朋友可以參考下2023-11-11vue.config.js中devServer.proxy配置說明及配置正確不生效問題解決
Vue項(xiàng)目devServer.proxy代理配置詳解的是一個(gè)非常常見的需求,下面這篇文章主要給大家介紹了關(guān)于vue.config.js中devServer.proxy配置說明及配置正確不生效問題解決的相關(guān)資料,需要的朋友可以參考下2023-02-02Vue實(shí)現(xiàn)動(dòng)態(tài)顯示表單項(xiàng)填寫進(jìn)度功能
這篇文章主要介紹了Vue實(shí)現(xiàn)動(dòng)態(tài)顯示表單項(xiàng)填寫進(jìn)度功能,此功能可以幫助用戶了解表單填寫的進(jìn)度和當(dāng)前狀態(tài),提高用戶體驗(yàn),通常實(shí)現(xiàn)的方式是在表單中添加進(jìn)度條,根據(jù)用戶填寫狀態(tài)動(dòng)態(tài)更新進(jìn)度條,感興趣的同學(xué)可以參考下文2023-05-05集成vue到j(luò)query/bootstrap項(xiàng)目的方法
下面小編就為大家分享一篇集成vue到j(luò)query/bootstrap項(xiàng)目的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-02-02Vue中對(duì)iframe實(shí)現(xiàn)keep alive無刷新的方法
這篇文章主要介紹了Vue中對(duì)iframe實(shí)現(xiàn)keep alive無刷新的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07解決webpack+Vue引入iView找不到字體文件的問題
今天小編就為大家分享一篇解決webpack+Vue引入iView找不到字體文件的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-09-09解決element-ui table設(shè)置列fixed時(shí)X軸滾動(dòng)條無法拖動(dòng)問題
這篇文章主要介紹了解決element-ui table設(shè)置列fixed時(shí)X軸滾動(dòng)條無法拖動(dòng)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10