Vue3源碼分析組件掛載創(chuàng)建虛擬節(jié)點(diǎn)
前情提要
本文我們接著Vue3源碼系列(1)-createApp發(fā)生了什么?繼續(xù)分析,我們知道調(diào)用createApp
方法之后會(huì)返回一個(gè)app
對(duì)象,緊接著我們會(huì)調(diào)用mount
方法將節(jié)點(diǎn)掛載到頁(yè)面上。所以本文我們從mount
方法開(kāi)始分析組件的掛載流程。
本文主要內(nèi)容
- Vue如何創(chuàng)建組件虛擬節(jié)點(diǎn)、文本虛擬節(jié)點(diǎn)、注釋虛擬節(jié)點(diǎn)、靜態(tài)虛擬節(jié)點(diǎn)。
ShapeFlags
是什么?PatchFlags
是什么?- 我們?cè)?code>Vue中寫(xiě)的
class
和style
形式摻雜不一、何時(shí)進(jìn)行的標(biāo)準(zhǔn)化、如何進(jìn)行標(biāo)準(zhǔn)化。 - 創(chuàng)建虛擬節(jié)點(diǎn)時(shí)對(duì)插槽的處理。
ref
可以寫(xiě)三種形式,字符串形式、響應(yīng)式形式、函數(shù)形式、patch階段如何實(shí)現(xiàn)他們的更新和設(shè)置。
1. Mount函數(shù)
mount(rootContainer) { //判斷當(dāng)前返回的app是否已經(jīng)調(diào)用過(guò)mount方法 if (!isMounted) { //如果當(dāng)前組件已經(jīng)有了app //實(shí)例則已經(jīng)掛載了警告用戶 if (rootContainer.__vue_app__) { console.warn( `There is already an app instance mounted on the host container.\n` + ` If you want to mount another app on the same host container,` + ` you need to unmount the previous app by calling \`app.unmount()\` first.` ); } //創(chuàng)建組件的VNode const vNode = createVNode(rootComponent); //剛才調(diào)用createApp創(chuàng)建的上下文 vNode.appContext = context; render(vNode, rootContainer); //渲染虛擬DOM //標(biāo)記已經(jīng)掛載了 isMounted = true; //建立app與DOM的關(guān)聯(lián) app._container = rootContainer; rootContainer.__vue_app__ = app; //建立app與組件的關(guān)聯(lián) app._instance = vNode.component; } //已經(jīng)調(diào)用過(guò)mount方法 警告用戶 else { console.warn( `App has already been mounted.\n` + `If you want to remount the same app, move your app creation logic ` + `into a factory function and create fresh app instances for each ` + `mount - e.g. \`const createMyApp = () => createApp(App)\`` ); } }
- 這個(gè)函數(shù)主要判斷當(dāng)前
app
是否已經(jīng)調(diào)用過(guò)mount
函數(shù)了,如果已經(jīng)調(diào)用過(guò)mount
函數(shù)了那么警告用戶。 createVnode
根據(jù)編譯后的.vue
文件生成對(duì)應(yīng)的虛擬節(jié)。render
函數(shù)用于將createVnode
生成的虛擬節(jié)點(diǎn)掛載到用戶傳入的container
中。- 在介紹創(chuàng)建虛擬節(jié)點(diǎn)的方法之前我們先來(lái)說(shuō)說(shuō)
shapeFlag
: 它用來(lái)表示當(dāng)前虛擬節(jié)點(diǎn)的類(lèi)型。我們可以通過(guò)對(duì)shapeFlag
做二進(jìn)制運(yùn)算來(lái)描述當(dāng)前節(jié)點(diǎn)的本身是什么類(lèi)型、子節(jié)點(diǎn)是什么類(lèi)型。
export const ShapeFlags = { ELEMENT: 1, //HTML SVG 或普通DOM元素 FUNCTIONAL_COMPONENT: 2, //函數(shù)式組件 STATEFUL_COMPONENT: 4, //有狀態(tài)組件 COMPONENT: 6, //2,4的綜合表示所有組件 TEXT_CHILDREN: 8, //子節(jié)點(diǎn)為純文本 ARRAY_CHILDREN: 16, //子節(jié)點(diǎn)是數(shù)組 SLOTS_CHILDREN: 32, //子節(jié)點(diǎn)包含插槽 TELEPORT: 64, //Teleport SUSPENSE: 128, //suspense };
2. 創(chuàng)建虛擬節(jié)點(diǎn)的幾個(gè)方法
(1) createVNode:用于創(chuàng)建組件的虛擬節(jié)點(diǎn)
function createVNode( type,//編譯后的.vue文件形成的對(duì)象 //<Comp hello="h"></Comp> //給組件傳遞的props props = null, children = null,//子組件 patchFlag = 0,//patch的類(lèi)型 dynamicProps = null,//動(dòng)態(tài)的props isBlockNode = false//是否是block節(jié)點(diǎn) ) { //通過(guò)__vccOpts判斷是否是class組件 if (isClassComponent(type)) { type = type.__vccOpts; } //將非字符串的class轉(zhuǎn)化為字符串 //將代理過(guò)的style淺克隆在轉(zhuǎn)為標(biāo)準(zhǔn)化 if (props) { //對(duì)于代理過(guò)的對(duì)象,我們需要克隆來(lái)使用他們 //因?yàn)橹苯有薷臅?huì)導(dǎo)致觸發(fā)響應(yīng)式 props = guardReactiveProps(props); let { class: klass, style } = props; if (klass && !shared.isString(klass)) { props.class = shared.normalizeClass(klass); } if (shared.isObject(style)) { if (reactivity.isProxy(style) && !shared.isArray(style)) { style = shared.extend({}, style); } props.style = shared.normalizeStyle(style); } } //生成當(dāng)前type的類(lèi)型 let shapeFlag = 0; /* 這部分我修改了源碼,便于讀者理解 suspense teleport放到前面是因?yàn)? 他們本身就是一個(gè)對(duì)象,如果放到后面 會(huì)導(dǎo)致先中標(biāo)isObject那么shapeFlag 的賦值會(huì)出錯(cuò)。 判斷當(dāng)前type的類(lèi)型,賦值給shapeFlag 后續(xù)就可以通過(guò)shapeFlag來(lái)判斷當(dāng)前虛擬 節(jié)點(diǎn)的類(lèi)型。 */ if (isString(type)) { //div span p等是ELEMENT shapeFlag = ShapeFlags.ELEMENT; } else if (isSuspense(type)) { shapeFlag = ShapeFlags.SUSPENSE; } else if (isTeleport(type)) { shapeFlag = ShapeFlags.TELEPORT; } else if (isObject(type)) { //對(duì)象則是有狀態(tài)組件 shapeFlag = ShapeFlags.STATEFUL_COMPONENT; } else if (isFunction(type)) { //如果是函數(shù)代表是無(wú)狀態(tài)組件 shapeFlag = ShapeFlags.FUNCTIONAL_COMPONENT; } //調(diào)用更基層的方法處理 return createBaseVNode( type, props, children, patchFlag, dynamicProps, shapeFlag, isBlockNode, true ); }
createVNode
主要是對(duì)傳遞的type
做出判斷,通過(guò)賦值shapeFlag
來(lái)標(biāo)明當(dāng)前的虛擬節(jié)點(diǎn)的類(lèi)型。- 如果
props
含有style
或者class
要進(jìn)行標(biāo)準(zhǔn)化。 - 例如<div :style="['background:red',{color:'red'}]"></div>其中第一個(gè)是
cssText
形式、第二個(gè)是對(duì)象形式,他們應(yīng)該被轉(zhuǎn)化為對(duì)象類(lèi)型所以轉(zhuǎn)化后應(yīng)該為<div style={color:'red',background:'red'}></div>。當(dāng)然對(duì)于class
也需要標(biāo)準(zhǔn)化:class={hello:true,world:false} => :class="hello"。但是這里處理的其實(shí)是用戶自己寫(xiě)了render
函數(shù),而對(duì)于使用了Vue
自帶的編譯系統(tǒng)之后,是不需要做這一層處理的。我們可以來(lái)看這樣一段編譯后的代碼。
<template> <div :class="{hello:true}" :style="[{color:'red'},'background:red']"> </div> </template> //編譯后 const _hoisted_1 = { class:_normalizeClass({hello:true}), style:_normalizeStyle([{color:'red'},'background:red']) } function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", _hoisted_1)) }
- 所以編譯后的
template
,在調(diào)用createVNode
的時(shí)候傳遞的props
就已經(jīng)是經(jīng)過(guò)處理的了。 - 我們忽略
guardReactiveProps
方法,來(lái)探尋一下normalizeStyle
以及normalizeClass
方法 normalizeClass
: 這個(gè)方法用于標(biāo)準(zhǔn)化class
。用戶可能會(huì)寫(xiě)數(shù)組形式,對(duì)象形式,以及字符串形式,字符串形式和對(duì)象形式很好理解,對(duì)于數(shù)組形式,遞歸調(diào)用了normalizeClass
意味著你可以傳遞多層的數(shù)組形式的參數(shù),例如:[{hello:true},[{yes:true}],'good'] => hello yes good
//{hello:true,yes:false}=>"hello" function normalizeClass(value) { let res = ""; if (isString(value)) { res = value; } else if (isArray(value)) { for (let i = 0; i < value.length; i++) { const normalized = normalizeClass(value[i]); if (normalized) { res += normalized + " "; } } } else if (isObject(value)) { for (const name in value) { if (value[name]) { res += name + " "; } } } return res.trim(); }
normalizeStyle
: 這個(gè)方法用于標(biāo)準(zhǔn)化style
。同樣用戶可以傳遞數(shù)組、對(duì)象、字符串三種形式。字符串形式的就是cssText
形式,這種形式需要調(diào)用parseStringStyle
方法。例如:"background:red;color:red"對(duì)于這樣的字符串需要轉(zhuǎn)化為對(duì)象就需要切割";"和":"符號(hào)得到key和value然后再轉(zhuǎn)化為對(duì)象。同時(shí)這也是parseStringStyle
的作用,這個(gè)函數(shù)就不展開(kāi)講了。而對(duì)象形式則是標(biāo)準(zhǔn)化形式,遍歷即可。
//[{backgroundColor:'red',"color:red;"}]=> //{backgroundColor:'red',color:'red'} export function normalizeStyle(value) { if (isArray(value)) { const res = {}; for (let i = 0; i < value.length; i++) { const item = value[i]; const normalized = isString(item) ? parseStringStyle(item) : normalizeStyle(item); if (normalized) { for (const key in normalized) { res[key] = normalized[key]; } } } return res; } else if (isString(value)) { return value; } else if (isObject(value)) { return value; } }
- 當(dāng)然還有判斷函數(shù)
isTeleport
和isSuspense
,對(duì)于Teleport
和Suspense
他們是Vue
內(nèi)部實(shí)現(xiàn)的組件,所以他們自帶屬性__isTeleport
和__Suspense
屬性。
const isSuspense = type => type.__isSuspense; const isTeleport = type => type.__isTeleport;
(2) createElementVNode:用于創(chuàng)建普通tag的虛擬節(jié)點(diǎn)如<div></div>
特別提示:
createElementVNode就是createBaseVNode方法,創(chuàng)建組件的虛擬節(jié)點(diǎn)方法createVNode必須標(biāo)準(zhǔn)化children,needFullChildrenNormalization=true
function createBaseVNode( type,//創(chuàng)建的虛擬節(jié)點(diǎn)的類(lèi)型 props = null,//傳遞的props children = null,//子節(jié)點(diǎn) patchFlag = 0,//patch類(lèi)型 dynamicProps = null,//動(dòng)態(tài)props shapeFlag = type === Fragment ? 0 : 1,//當(dāng)前虛擬節(jié)點(diǎn)的類(lèi)型 isBlockNode = false,//是否是block needFullChildrenNormalization = false//是否需要標(biāo)準(zhǔn)化children ) { const vnode = { __v_isVNode: true, //這是一個(gè)vnode __v_skip: true, //不進(jìn)行響應(yīng)式代理 type, //.vue文件編譯后的對(duì)象 props, //組件收到的props key: props && normalizeKey(props), //組件key ref: props && normalizeRef(props), //收集到的ref scopeId: getCurrentScopeId(),//當(dāng)前作用域ID slotScopeIds: null, //插槽ID children, //child組件 component: null, //組件實(shí)例 suspense: null,//存放suspense ssContent: null,//存放suspense的default的虛擬節(jié)點(diǎn) ssFallback: null,//存放suspense的fallback的虛擬節(jié)點(diǎn) dirs: null, //解析到的自定義指令 transition: null, el: null, //對(duì)應(yīng)的真實(shí)DOM anchor: null, //插入的錨點(diǎn) target: null,//teleport的參數(shù)to指定的DOM targetAnchor: null,//teleport插入的錨點(diǎn) staticCount: 0, shapeFlag, //表示當(dāng)前vNode的類(lèi)型 patchFlag, //path的模式 dynamicProps, //含有動(dòng)態(tài)的props dynamicChildren: null, //含有的動(dòng)態(tài)children appContext: null, //app上下文 }; //是否需要對(duì)children進(jìn)行標(biāo)準(zhǔn)化 if (needFullChildrenNormalization) { normalizeChildren(vnode, children); //處理SUSPENSE邏輯 if (shapeFlag & ShapeFlags.SUSPENSE) { //賦值ssContent=>default和ssFallback=>fallback type.normalize(vnode); } } //設(shè)置shapeFlags else if (children) { vnode.shapeFlag |= shared.isString(children) ? ShapeFlags.TEXT_CHILDREN : ShapeFlags.ARRAY_CHILDREN; } //警告key不能為NaN if (vnode.key !== vnode.key) { warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type); } //判斷是否加入dynamicChildren if ( getBlockTreeEnabled() > 0 && //允許追蹤 !isBlockNode && //當(dāng)前不是block getCurrentBlock() && //currentBlock存在 //不是靜態(tài)節(jié)點(diǎn),或者是組件 (vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) && vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS ) { //放入dynamicChildren getCurrentBlock().push(vnode); } return vnode; }
createBaseVNode
: 這個(gè)方法用于創(chuàng)建一個(gè)虛擬節(jié)點(diǎn),同時(shí)對(duì)key、ref、chidren(needFullChildrenNormalization為true)進(jìn)行標(biāo)準(zhǔn)化。如果有children
給shapeFlag
賦值。同時(shí)往dynamicChildren
中添加虛擬節(jié)點(diǎn),這個(gè)咱們后面在進(jìn)行講解。normalizeKey
: 用于標(biāo)準(zhǔn)化props
中的key
屬性
//為了便于閱讀修改了源碼 function normalizeKey(props){ const {key} = props return key != null ? key : null }
normalizeRef
: 用于標(biāo)準(zhǔn)化props
中的ref
屬性。ref可以是字符串,可以是reactivity中的ref對(duì)象,也可以是一個(gè)函數(shù)。如果是以上三種形式,將虛擬節(jié)點(diǎn)的ref
屬性包裝成一個(gè)對(duì)象。其中i代表當(dāng)前渲染的組件實(shí)例、ref代表原本的ref值、ref_for代表在這個(gè)標(biāo)簽中傳遞了ref和v-for兩個(gè)屬性。例如: <div ref="a"></div> 字符串形式; <div :ref="a"></div> ref對(duì)象形式; <div :ref="()=>{}"></div>函數(shù)形式
function normalizeRef(props) { const { ref, ref_for, ref_key } = props; if (ref != null) { if (isString(ref) || isRef(ref) || isFunction(ref)) { const res = { //當(dāng)前渲染的組件實(shí)例 i: getCurrentRenderingInstance(), r: ref, k: ref_key, //<div v-for="a in c" ref="b"></div> //同時(shí)使用了ref和v-for會(huì)標(biāo)記 f: !!ref_for, }; return res; } return ref } return null }
- 在介紹
noramlizeChildren
之前,我們必須要介紹一下插槽,因?yàn)檫@個(gè)函數(shù)主要是對(duì)插槽進(jìn)行的處理,插槽其實(shí)就是給組件傳遞children
屬性,在組件內(nèi)部可以復(fù)用這部分template
。首先我們先來(lái)看看對(duì)使用了插槽的組件的編譯后結(jié)果。
<template> <Comp> <template v-slot:default> <div></div> </template> <template v-slot:header></template> </Comp> </template> //編譯后 const _hoisted_1 = _createElementVNode("div", null, null, -1 /* HOISTED */) function render(_ctx, _cache) { const _component_Comp = _resolveComponent("Comp", true) return (_openBlock(), _createBlock(_component_Comp, null, { default: _withCtx(() => [ _hoisted_1 ]), header: _withCtx(() => []), _: 1 /* STABLE */ })) }
- 目前我們對(duì)于
createBlock
簡(jiǎn)單理解為調(diào)用createVNode
方法即可。這個(gè)實(shí)例使用的具名插槽,所以編譯結(jié)果createBlock
的第三個(gè)參數(shù)children
是一個(gè)對(duì)象,鍵就是具名插槽的名稱(chēng),值則是<template>
的編譯結(jié)果。其中"_"
屬性代表的是當(dāng)前插槽的類(lèi)型。
STABLE:1
代表當(dāng)前插槽處于穩(wěn)定狀態(tài),插槽的結(jié)構(gòu)不會(huì)發(fā)生變化。DYNAMIC:2
代表當(dāng)前的插槽處于動(dòng)態(tài)狀態(tài),插槽的結(jié)構(gòu)可能發(fā)生改變。FORWORD:3
代表當(dāng)前的插槽處于轉(zhuǎn)發(fā)狀態(tài)。
//STABLE <Comp> <template v-slot:default></template> </Comp> //DYNAMIC <Comp> <template v-slot:default v-if="a"></template> </Comp> //FORWORD <Comp> <slot name="default"></slot> </Comp>
normalizeChildren
: 標(biāo)準(zhǔn)化虛擬節(jié)點(diǎn)的children
屬性,主要是對(duì)slots
屬性的處理。用戶可能自己實(shí)現(xiàn)了render
函數(shù),那么對(duì)于插槽的創(chuàng)建沒(méi)有直接通過(guò)Vue編譯得到的數(shù)據(jù)完整,需要對(duì)其進(jìn)行標(biāo)準(zhǔn)化。首先判斷當(dāng)前是否傳遞了插槽,如果傳遞了判斷children
的類(lèi)型 數(shù)組類(lèi)型:(不推薦這個(gè)方式)打上ARRAY_CHILDREN
的標(biāo)記;對(duì)象類(lèi)型:(這個(gè)方式是推薦的)但是可能是FORWORD
類(lèi)型,所以當(dāng)前插槽的類(lèi)型應(yīng)當(dāng)繼承當(dāng)前實(shí)例的插槽的類(lèi)型。函數(shù)類(lèi)型:重新包裝children
,其中函數(shù)作為children
的default
屬性。當(dāng)然createVNode(Comp,null,'123')也可以是字符串這將被當(dāng)做是Text
類(lèi)型,最終將標(biāo)準(zhǔn)化的children
和type
賦值到虛擬節(jié)點(diǎn)上。
function normalizeChildren(vnode, children) { let type = 0;//設(shè)置shapeFlag的初始值 const { shapeFlag } = vnode; const currentRenderingInstance = getCurrentRenderingInstance(); if (children == null) { children = null; } //如果children是數(shù)組,設(shè)置shapeFlags為ARRAY_CHILDREN //用戶可以寫(xiě)createVNode(Comp,null,[Vnode1,Vnode2]) //這樣的形式,但是不推薦 else if (shared.isArray(children)) { type = ShapeFlags.ARRAY_CHILDREN; } //處理"<Comp>插槽內(nèi)容</Comp>"這種情況 //如果你一定要自己寫(xiě)render函數(shù)官方推薦 //對(duì)象形式,并返回一個(gè)函數(shù)的類(lèi)型 //createVNode(Comp,null.{default:()=>Vnode}) else if (typeof children === "object") { //處理TELEPORT情況或ELEMENT情況 if (shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.TELEPORT)) { //忽略這里的代碼... } //這里對(duì)vnode打上slot的標(biāo)識(shí) else { type = ShapeFlags.SLOTS_CHILDREN; //獲取當(dāng)前slot的slotFlag const slotFlag = children._; if (!slotFlag && !(InternalObjectKey in children)) { children._ctx = currentRenderingInstance; } //在組件中引用了當(dāng)前slot 例如:<Comp><slot></slot></Comp> //這里的slot是當(dāng)前組件實(shí)例傳遞的插槽就會(huì)被標(biāo)記為FORWARDED else if (slotFlag === SlotFlags.FORWARDED && currentRenderingInstance) { //這里是引用當(dāng)前實(shí)例傳遞的slot所以傳遞給children的slot類(lèi)型 //依然延續(xù)當(dāng)前實(shí)例傳遞的slot if (currentRenderingInstance.slots._ === SlotFlags.STABLE) { children._ = SlotFlags.STABLE; } else { children._ = SlotFlags.DYNAMIC; //添加DYNAMIC_SLOTS vnode.patchFlag |= PatchFlags.DYNAMIC_SLOTS; } } } } //兼容函數(shù)寫(xiě)法 /** * createVnode(Comp,null,()=>h()) * children為作為default */ else if (shared.isFunction(children)) { //重新包裝children children = { default: children, _ctx: currentRenderingInstance }; type = ShapeFlags.SLOTS_CHILDREN; } else { children = String(children); //強(qiáng)制讓teleport children變?yōu)閿?shù)組為了讓他可以在任意處移動(dòng) if (shapeFlag & ShapeFlags.TELEPORT) { type = ShapeFlags.ARRAY_CHILDREN; children = [createTextVNode(children)]; } //child為text else { type = ShapeFlags.TEXT_CHILDREN; } } //掛載children、shapeFlag到vnode上 vnode.children = children; vnode.shapeFlag |= type; }
(3) createCommentVNode:用于創(chuàng)建注釋的虛擬節(jié)點(diǎn)
// Comment = Symbol('comment') function createCommentVNode(text = "", asBlock = false) { return asBlock ? (openBlock(), createBlock(Comment, null, text)) : createVNode(Comment, null, text); }
(4) createTextVNode:用于創(chuàng)建文本的虛擬節(jié)點(diǎn)
// Text = Symbol('text') function createTextVNode(text = " ", flag = 0) { return createVNode(Text, null, text, flag); }
(5) createStaticVNode:用于創(chuàng)建靜態(tài)的虛擬節(jié)點(diǎn),沒(méi)有使用任何變量的標(biāo)簽就是靜態(tài)節(jié)點(diǎn)
//Static = Symbol('static') function createStaticVNode(content, numberOfNodes) { const vnode = createVNode(Static, null, content); vnode.staticCount = numberOfNodes; return vnode; }
3. patch函數(shù)
- 好的。介紹完了幾個(gè)創(chuàng)建虛擬節(jié)點(diǎn)的方法,我們接著
mount
的流程,調(diào)用render
函數(shù) render
: 如果當(dāng)前DOM
實(shí)例已經(jīng)掛載過(guò)了,那么需要先卸載掛載的節(jié)點(diǎn)、調(diào)用patch
執(zhí)行掛載流程、最后執(zhí)行Vue的前置和后置調(diào)度器緩存的函數(shù)。
const render = (vnode, container) => { if (vnode == null) { //已經(jīng)存在了 則卸載 if (container._vnode) { unmount(container._vnode, null, null, true); } } else { //掛載元素 patch(container._vnode || null, vnode, container, null, null, null); } flushPreFlushCbs(); flushPostFlushCbs(); //對(duì)于掛載過(guò)的container設(shè)置_vnode container._vnode = vnode; };
- 這個(gè)函數(shù)比較簡(jiǎn)單,我們主要把注意力集中到
patch
函數(shù)上,這個(gè)函數(shù)相當(dāng)?shù)闹匾?/li> - 在介紹
patch
函數(shù)之前我們同樣介紹一個(gè)重要的標(biāo)識(shí)符PatchFlags---靶向更新標(biāo)識(shí)。在編譯階段會(huì)判斷當(dāng)前的節(jié)點(diǎn)是否包含動(dòng)態(tài)的props、動(dòng)態(tài)style、動(dòng)態(tài)class、fragment是否穩(wěn)定、當(dāng)key屬性是動(dòng)態(tài)的時(shí)候需要全量比較props等。這樣就可以在更新階段判斷patchFlag
來(lái)實(shí)現(xiàn)靶向更新。比較特殊的有HOISTED:-1
表示靜態(tài)節(jié)點(diǎn)不需要diff(HMR的時(shí)候還是需要,用戶可能手動(dòng)直接改變靜態(tài)節(jié)點(diǎn)),BAIL
表示應(yīng)該結(jié)束patch
。
const PatchFlags = { DEV_ROOT_FRAGMENT: 2048, //動(dòng)態(tài)插槽 DYNAMIC_SLOTS: 1024, //不帶key的fragment UNKEYED_FRAGMENT: 256, //帶key的fragment KEYED_FRAGMENT: 128, //穩(wěn)定的fragment STABLE_FRAGMENT: 64, //帶有監(jiān)聽(tīng)事件的節(jié)點(diǎn) HYDRATE_EVENTS: 32, FULL_PROPS: 16, //具有動(dòng)態(tài):key,key改變需要全量比較 PROPS: 8, //動(dòng)態(tài)屬性但不包含style class屬性 STYLE: 4, //動(dòng)態(tài)的style CLASS: 2, //動(dòng)態(tài)的class TEXT: 1, //動(dòng)態(tài)的文本 HOISTED: -1, //靜態(tài)節(jié)點(diǎn) BAIL: -2, //表示diff應(yīng)該結(jié)束 };
patch
: 主要比較beforeVNode
和currentVNode
的不同,執(zhí)行不同的更新或者掛載流程。如果beforeVNode
為null
則為掛載流程反之則為更新流程。同時(shí)需要判斷當(dāng)前VNode
的類(lèi)型調(diào)用不同的處理函數(shù)。例如:對(duì)于普通HTML類(lèi)型
調(diào)用processElement
,對(duì)于組件類(lèi)型調(diào)用processComponent
。我們本節(jié)主要講的就是組件的掛載流程。- 首先判斷
beforeVNode
和currentVNode
是否為同一個(gè)對(duì)象,如果是同一個(gè)對(duì)象表示不需要更新。 - 然后判斷
beforeVNode
和currentVNode
的type與key
是否相等,如果不等,表示節(jié)點(diǎn)需要被卸載。例如:<div></div> => <p></p>
節(jié)點(diǎn)發(fā)生了改變,需要被卸載
。這里需要注意的是:Vue的diff進(jìn)行的是同層同節(jié)點(diǎn)比較,type和key將作為新舊節(jié)點(diǎn)是否是同一個(gè)的判斷標(biāo)準(zhǔn)。
const patch = ( beforeVNode,//之前的Vnode currentVNode,//當(dāng)前的Vnode container,//掛載的容器DOM anchor = null,//掛載的錨點(diǎn) parentComponent = null,//父組件 parentSuspense = null,//父suspense isSVG = false,//是否是SVG slotScopeIds = null,//當(dāng)前的插槽作用域ID //是否開(kāi)啟優(yōu)化 optimized = !!currentVNode.dynamicChildren ) => { //兩個(gè)VNode相等 不做處理 if (beforeVNode === currentVNode) { return null; } //如果不是同一個(gè)節(jié)點(diǎn),卸載 if (beforeVNode && !isSameVNodeType(beforeVNode, currentVNode)) { anchor = getNextHostNode(beforeVNode); unmount(beforeVNode, parentComponent, parentSuspense, true); beforeVNode = null; } if (currentVNode.patchFlag === PatchFlags.BAIL) { optimized = false; currentVNode.dynamicChildren = null; } const { type, ref, shapeFlag } = currentVNode; switch (type) { case Text: //處理Text break; case Comment: //處理注釋節(jié)點(diǎn) break; case Static: //處理靜態(tài)節(jié)點(diǎn) break; case Fragment: //處理Fragment節(jié)點(diǎn) break; default: if (shapeFlag & ShapeFlags) { //處理Element類(lèi)型 } else if (shapeFlag & 6) { //處理組件類(lèi)型 processComponent( beforeVNode, currentVNode, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ); } else if (shapeFlag & ShapeFlags.TELEPORT) { //處理Teleport } else if (shapeFlag & ShapeFlags.SUSPENSE) { //處理Suspense } //都不匹配報(bào)錯(cuò) else { console.warn("Invalid VNode type:", type, `(${typeof type})`); } } //設(shè)置setupState和refs中的ref if (ref != null && parentComponent) { setRef( ref, beforeVNode && beforeVNode.ref, parentSuspense, currentVNode || beforeVNode, !currentVNode ); } };
isSameVNodeType
: 主要判斷新舊虛擬節(jié)點(diǎn)是否是同一個(gè)節(jié)點(diǎn)。
function isSameVNodeType(beforeVNode,currentVNode){ return ( beforeVNode.type === currentVNode.type && beforeVNode.key === currentVNode.key ) }
getNextHostNode
: 用于獲取當(dāng)前虛擬節(jié)點(diǎn)的真實(shí)DOM的下一個(gè)兄弟節(jié)點(diǎn)。
const getNextHostNode = (vnode) => { //如果當(dāng)前虛擬節(jié)點(diǎn)類(lèi)型是組件,組件沒(méi)有真實(shí)DOM //找到subTree(render返回的節(jié)點(diǎn)) if (vnode.shapeFlag & ShapeFlags.COMPONENT) { return getNextHostNode(vnode.component.subTree); } //調(diào)用suspense的next方法獲取 if (vnode.shapeFlag & ShapeFlags.SUSPENSE) { return vnode.suspense.next(); } //獲取當(dāng)前節(jié)點(diǎn)的下一個(gè)兄弟節(jié)點(diǎn) return hostNextSibling(vnode.anchor || vnode.el); }; //runtime-dom傳遞的方法 const nextSibling = node => node.nextSibling,
setRef
: 設(shè)置ref
屬性。同時(shí)在更新階段ref也需要被更新。
- 如果
rawRef
為一個(gè)數(shù)組遍歷這個(gè)數(shù)組分別調(diào)用setRef
方法。 - 獲取
refValue
,如果當(dāng)前虛擬節(jié)點(diǎn)是組件則是組件的expose
或proxy
(這取決你有沒(méi)有設(shè)置expose
屬性,expose
具體使用請(qǐng)查詢(xún)官方文檔),否則就是當(dāng)前虛擬節(jié)點(diǎn)的真實(shí)DOM。 - 清除掉setupState、refs中的oldRef。如果設(shè)置的ref屬性值是響應(yīng)式的ref創(chuàng)建的,清空
ref.value
。 - 創(chuàng)建
doSet
方法,如果存在refValue
,則在DOM更新后再執(zhí)行doSet,否則現(xiàn)在就執(zhí)行。
function setRef( rawRef,//當(dāng)前的ref oldRawRef,//之前的ref parentSuspense, vnode, isUnmount = false ) { //是數(shù)組,分別設(shè)置 if (shared.isArray(rawRef)) { rawRef.forEach((r, i) => setRef( r, oldRawRef && (shared.isArray(oldRawRef) ? oldRawRef[i] : oldRawRef), parentSuspense, vnode, isUnmount ) ); return; } //1.如果當(dāng)前節(jié)點(diǎn)是一個(gè)組件,那么傳遞給ref屬性的將會(huì)是expose //或者proxy //2.如果不是組件那么refValue為當(dāng)前節(jié)點(diǎn)的DOM const refValue = vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT ? getExposeProxy(vnode.component) || vnode.component.proxy : vnode.el; //如果卸載了則value為null const value = isUnmount ? null : refValue; //i:instance r:ref k:ref_key f:ref_for //當(dāng)同時(shí)含有ref和for關(guān)鍵詞的時(shí)候ref_for為true //之前createVNode的時(shí)候調(diào)用了normalizeRef將 //ref設(shè)置為了一個(gè)包裝后的對(duì)象。 const { i: owner, r: ref } = rawRef; //警告 if (!owner) { warn( `Missing ref owner context. ref cannot be used on hoisted vnodes. ` + `A vnode with ref must be created inside the render function.` ); return; } const oldRef = oldRawRef && oldRawRef.r; //獲取當(dāng)前實(shí)例的refs屬性,初始化refs const refs = Object.keys(owner.refs).length === 0 ? (owner.refs = {}) : owner.refs; //這里是setup函數(shù)調(diào)用的返回值 const setupState = owner.setupState; /* ref可以是一個(gè)字符串<div ref="a"></div> ref可以是一個(gè)響應(yīng)式ref對(duì)象<div :ref="a"></div> ref還可以是一個(gè)函數(shù)<div :ref="f"></div> setup(){ retunr {a:ref(null),f(refValue){}} } */ //新舊ref不同,清除oldRef if (oldRef != null && oldRef !== ref) { //如果ref傳遞的字符串類(lèi)型 //將當(dāng)前實(shí)例的refs屬性對(duì)應(yīng)的oldRef設(shè)置為null //清除setupState中的oldRef if (shared.isString(oldRef)) { refs[oldRef] = null; if (shared.hasOwn(setupState, oldRef)) { setupState[oldRef] = null; } } //如果是響應(yīng)式的ref,清空value else if (reactivity.isRef(oldRef)) { oldRef.value = null; } } //如果ref是一個(gè)函數(shù)(動(dòng)態(tài)ref) //<div :ref="(el,refs)=>{}"></div> //調(diào)用這個(gè)函數(shù)傳遞value和refs if (shared.isFunction(ref)) { //vue的錯(cuò)誤處理函數(shù),包裹了try catch //錯(cuò)誤監(jiān)聽(tīng)就是依靠這個(gè)函數(shù),不詳細(xì)展開(kāi) //簡(jiǎn)單理解為ref.call(owner,value,refs) callWithErrorHandling(ref, owner, 12, [value, refs]); } else { //判斷ref類(lèi)型,因?yàn)樽址畆ef和響應(yīng)式ref處理不同 const _isString = shared.isString(ref); const _isRef = reactivity.isRef(ref); if (_isString || _isRef) { //因?yàn)槠L(zhǎng),放到下面講解,此處省略deSet函數(shù)實(shí)現(xiàn) const doSet = function(){} //放入Vue調(diào)度的后置隊(duì)列,在DOM更新后再設(shè)置ref if (value) { doSet.id = -1; queuePostRenderEffect(doSet, parentSuspense); } else { doSet(); } } else { warn("Invalid template ref type:", ref, `(${typeof ref})`); } } }
doSet
: 主要對(duì)setupState、refs
中的ref屬性進(jìn)行設(shè)置。
- 如果在一個(gè)標(biāo)簽中使用了
v-for
和ref
,那么設(shè)置的ref
必須是一個(gè)數(shù)組,v-for可能渲染多個(gè)節(jié)點(diǎn)。如果設(shè)置的ref是響應(yīng)式創(chuàng)建的,那么修改value
值。 - 如果沒(méi)有同時(shí)使用
v-for
和ref
,修改對(duì)應(yīng)的setupState
和refs
中的ref
即可。
const doSet = () => { //<div v-for="a in b" :ref="c"></div> if (rawRef.f) { const existing = _isString ? refs[ref] : ref.value; //已經(jīng)卸載了 要移除 if (isUnmount) { shared.isArray(existing) && shared.remove(existing, refValue); } else { //不是數(shù)組,包裝成數(shù)組,方便后續(xù)push if (!shared.isArray(existing)) { if (_isString) { refs[ref] = [refValue]; //同時(shí)需要修改setupState中的ref if (shared.hasOwn(setupState, ref)) { setupState[ref] = refs[ref]; } } //如果是響應(yīng)式的ref,修改value else { ref.value = [refValue]; } } //已經(jīng)存在了push else if (!existing.includes(refValue)) { existing.push(refValue); } } } //<div ref="a"></div> else if (_isString) { refs[ref] = value; if (shared.hasOwn(setupState, ref)) { setupState[ref] = value; } } //<div :ref="a"></div> else if (_isRef) { ref.value = value; //設(shè)置ref_key為value if (rawRef.k) refs[rawRef.k] = value; } else { warn("Invalid template ref type:", ref, `(${typeof ref})`); } };
processComponent
: 處理組件的掛載和更新。如果beforeVNode為null則執(zhí)行掛載流程;否則執(zhí)行更新流程。
//處理Component類(lèi)型的元素 const processComponent = ( beforeVNode, //之前的Vnode 第一次掛載為null currentVNode, //當(dāng)前的Vnode container, //掛載的容器 anchor,//插入的錨點(diǎn) parentComponent, //父組件 parentSuspense,//父suspense isSVG,//是否是SVG slotScopeIds,//插槽的作用域ID optimized//是否開(kāi)啟優(yōu)化 ) => { currentVNode.slotScopeIds = slotScopeIds; //不存在beforeVNode掛載 if (beforeVNode == null) { mountComponent( currentVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized ); } //更新 else { updateComponent(beforeVNode, currentVNode, optimized); } };
4. 總結(jié)
- 到這里我們就分析完了組件掛載之前的所有流程。組件的掛載流程我們將在下一小節(jié)繼續(xù)討論。
mount
方法主要調(diào)用了createVNode
方法創(chuàng)建虛擬節(jié)點(diǎn),然后調(diào)用render
函數(shù)進(jìn)行了渲染。- 然后我們分析了創(chuàng)建文本、注釋、靜態(tài)、HTML元素、組件五種類(lèi)型的虛擬節(jié)點(diǎn)的創(chuàng)建方法。
- 最后我們講解了
patch
階段如何設(shè)置ref屬性。
以上就是Vue3源碼分析組件掛載創(chuàng)建虛擬節(jié)點(diǎn)的詳細(xì)內(nèi)容,更多關(guān)于Vue3組件掛載創(chuàng)建虛擬節(jié)點(diǎn)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue如何獲取new Date().getTime()時(shí)間戳
在Web開(kāi)發(fā)中,前端使用Vue.js獲取的是毫秒級(jí)時(shí)間戳,而PHP后端則是秒級(jí)時(shí)間戳,處理此類(lèi)問(wèn)題時(shí),需要將PHP的時(shí)間戳乘以1000轉(zhuǎn)換為毫秒級(jí),以保證數(shù)據(jù)的一致性和正確的邏輯判斷2024-10-10Element-UI中<el-cascader?/>回顯失敗問(wèn)題的完美解決
我們?cè)谑褂胑l-cascader控件往數(shù)據(jù)庫(kù)保存的都是最后一級(jí)的數(shù)據(jù),那如果再次編輯此條數(shù)據(jù)時(shí),直接給el-cascader傳入最后一級(jí)的數(shù)據(jù),它是不會(huì)自動(dòng)勾選的,下面這篇文章主要給大家介紹了關(guān)于Element-UI中<el-cascader?/>回顯失敗問(wèn)題的完美解決辦法,需要的朋友可以參考下2023-01-01webpack轉(zhuǎn)vite的詳細(xì)操作流程與問(wèn)題總結(jié)
Vite是新一代的前端開(kāi)發(fā)與構(gòu)建工具,相比于傳統(tǒng)的webpack,Vite 有著極速的本地項(xiàng)目啟動(dòng)速度(通常不超過(guò)5s)以及極速的熱更新速度(幾乎無(wú)感知),下面這篇文章主要給大家介紹了關(guān)于webpack轉(zhuǎn)vite的詳細(xì)操作流程與問(wèn)題總結(jié)的相關(guān)資料,需要的朋友可以參考下2023-03-03vue項(xiàng)目引入Iconfont圖標(biāo)庫(kù)的教程圖解
這篇文章主要介紹了vue項(xiàng)目引入Iconfont圖標(biāo)庫(kù)的相關(guān)知識(shí),非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-10-10VUE識(shí)別訪問(wèn)設(shè)備是pc端還是移動(dòng)端的實(shí)現(xiàn)步驟
經(jīng)常在項(xiàng)目中會(huì)有支持pc與手機(jī)端需求,并且pc與手機(jī)端是兩個(gè)不一樣的頁(yè)面,這時(shí)就要求判斷設(shè)置,下面這篇文章主要給大家介紹了關(guān)于VUE識(shí)別訪問(wèn)設(shè)備是pc端還是移動(dòng)端的相關(guān)資料,需要的朋友可以參考下2023-05-05Vue2.0 給Tab標(biāo)簽頁(yè)和頁(yè)面切換過(guò)渡添加樣式的方法
下面小編就為大家分享一篇Vue2.0 給Tab標(biāo)簽頁(yè)和頁(yè)面切換過(guò)渡添加樣式的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-03-03vue3+Element?Plus?v-model實(shí)現(xiàn)父子組件數(shù)據(jù)同步的案例代碼
這篇文章主要介紹了vue3+Element?Plus?v-model實(shí)現(xiàn)父子組件數(shù)據(jù)同步,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-01-01C#實(shí)現(xiàn)將一個(gè)字符轉(zhuǎn)換為整數(shù)
下面小編就為大家分享一篇C#實(shí)現(xiàn)將一個(gè)字符轉(zhuǎn)換為整數(shù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2017-12-12vue?require.context()的用法實(shí)例詳解
require.context是webpack提供的一個(gè)api,通常用于批量注冊(cè)組件,下面這篇文章主要給大家介紹了關(guān)于vue?require.context()用法的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-04-04