Vue3源碼通過render?patch?了解diff
引言
上一篇中,我們理清了createApp
走的流程,最后通過createAppAPI
創(chuàng)建了app
。雖然app
上的各種屬性和方法也都已經(jīng)有所了解,但其中的mount
和unmount
方法,都是通過調(diào)用render
函數(shù)來完成的。盡管我們很好奇render
函數(shù)的故事,可是baseCreateRenderer
函數(shù)有2000+
行,基本都和render
相關(guān),因此拆解到本文里敘述,以下方法都定義在baseCreateRenderer
函數(shù)中。
render
render
也不神秘了,畢竟在上一篇文章中露過面,當(dāng)然這里也順帶提一下 baseCreateRenderer
從參數(shù)options
中解構(gòu)的一些方法,基本都是些增刪改查、復(fù)制節(jié)點(diǎn)的功能,見名知義了。主要看看render
,接收vnode
、container
、isSvg
三個(gè)參數(shù)。調(diào)用unmount
卸載或者調(diào)用patch
進(jìn)行節(jié)點(diǎn)比較從而繼續(xù)下一步。
- 判斷
vnode
是否為null
。如果對(duì)上一篇文章還有印象,那么就會(huì)知道,相當(dāng)于是判斷調(diào)用的是app.mount
還是app.unmount
方法,因?yàn)?code>app.unmount方法傳入的vnode
就是null
。那么這里對(duì)應(yīng)的就是在app.unmount
里使用unmount
函數(shù)來卸載;而在app.mount
里進(jìn)行patch
比較。 - 調(diào)用
flushPostFlushCbs()
,其中的單詞Post
的含義,看過第一篇講解watch
的同學(xué)也許能猜出來,表示執(zhí)行時(shí)機(jī)是在組件更新后。這個(gè)函數(shù)便是執(zhí)行組件更新后的一些回調(diào)。 - 把
vnode
掛到container
上,即舊的虛擬DOM
。
const { insert: hostInsert, remove: hostRemove, patchProp: hostPatchProp, createElement: hostCreateElement, createText: hostCreateText, createComment: hostCreateComment, setText: hostSetText, setElementText: hostSetElementText, parentNode: hostParentNode, nextSibling: hostNextSibling, setScopeId: hostSetScopeId = NOOP, cloneNode: hostCloneNode, insertStaticContent: hostInsertStaticContent } = options // render const render: RootRenderFunction = (vnode, container, isSVG) => { if (vnode == null) { if (container._vnode) { unmount(container._vnode, null, null, true) } } else { // 新舊節(jié)點(diǎn)的對(duì)比 patch(container._vnode || null, vnode, container, null, null, null, isSVG) } flushPostFlushCbs() // 記錄舊節(jié)點(diǎn) container._vnode = vnode }
patch
patch
函數(shù)里主要對(duì)新舊節(jié)點(diǎn)也就是虛擬DOM
的對(duì)比,常說的vue
里的diff
算法,便是從patch
開始。結(jié)合render
函數(shù)來看,我們知道,舊的虛擬DOM
存儲(chǔ)在container._vnode
上。那么diff
的方式就在patch
中了:
新舊節(jié)點(diǎn)相同,直接返回;
舊節(jié)點(diǎn)存在,且新舊節(jié)點(diǎn)類型不同,則舊節(jié)點(diǎn)不可復(fù)用,將其卸載(unmount
),錨點(diǎn)anchor
移向下一個(gè)節(jié)點(diǎn);
新節(jié)點(diǎn)是否靜態(tài)節(jié)點(diǎn)標(biāo)記;
根據(jù)新節(jié)點(diǎn)的類型,相應(yīng)地調(diào)用不同類型的處理方法:
- 文本:
processText
; - 注釋:
processCommentNode
; - 靜態(tài)節(jié)點(diǎn):
mountStaticNode
或patchStaticNode
; - 文檔片段:
processFragment
; - 其它。
在 其它 這一項(xiàng)中,又根據(jù)形狀標(biāo)記 shapeFlag
等,判斷是 元素節(jié)點(diǎn)、組件節(jié)點(diǎn),或是Teleport
、Suspense
等,然后調(diào)用相應(yīng)的process
去處理。最后處理template
中的ref
// Note: functions inside this closure should use `const xxx = () => {}` // style in order to prevent being inlined by minifiers. const patch: PatchFn = ( n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren ) => { // 新舊節(jié)點(diǎn)相同,直接返回 if (n1 === n2) { return } // 舊節(jié)點(diǎn)存在,且新舊節(jié)點(diǎn)類型不同,卸載舊節(jié)點(diǎn),錨點(diǎn)anchor后移 // patching & not same type, unmount old tree if (n1 && !isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense, true) n1 = null } // 是否靜態(tài)節(jié)點(diǎn)優(yōu)化 if (n2.patchFlag === PatchFlags.BAIL) { optimized = false n2.dynamicChildren = null } // const { type, ref, shapeFlag } = n2 switch (type) { case Text: processText(n1, n2, container, anchor) break case Comment: processCommentNode(n1, n2, container, anchor) break case Static: if (n1 == null) { mountStaticNode(n2, container, anchor, isSVG) } else if (__DEV__) { patchStaticNode(n1, n2, container, isSVG) } break case Fragment: processFragment( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) break default: if (shapeFlag & ShapeFlags.ELEMENT) { processElement( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else if (shapeFlag & ShapeFlags.COMPONENT) { processComponent( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else if (shapeFlag & ShapeFlags.TELEPORT) { ;(type as typeof TeleportImpl).process( n1 as TeleportVNode, n2 as TeleportVNode, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals ) } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { ;(type as typeof SuspenseImpl).process( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals ) } else if (__DEV__) { warn('Invalid VNode type:', type, `(${typeof type})`) } } // 處理 template 中的 ref // set ref if (ref != null && parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2) } }
processText
文本節(jié)點(diǎn)的處理十分簡(jiǎn)單,沒有舊節(jié)點(diǎn)則新建并插入新節(jié)點(diǎn);有舊節(jié)點(diǎn),且節(jié)點(diǎn)內(nèi)容不一致,則設(shè)置為新節(jié)點(diǎn)的內(nèi)容。
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => { if (n1 == null) { hostInsert( (n2.el = hostCreateText(n2.children as string)), container, anchor ) } else { const el = (n2.el = n1.el!) if (n2.children !== n1.children) { hostSetText(el, n2.children as string) } } }
processCommontNode
不支持動(dòng)態(tài)的注視節(jié)點(diǎn),因此只要舊節(jié)點(diǎn)存在,就使用舊節(jié)點(diǎn)的內(nèi)容。
const processCommentNode: ProcessTextOrCommentFn = ( n1, n2, container, anchor ) => { if (n1 == null) { hostInsert( (n2.el = hostCreateComment((n2.children as string) || '')), container, anchor ) } else { // there's no support for dynamic comments n2.el = n1.el } }
mountStaticNode 和 patchStaticNode
事實(shí)上靜態(tài)節(jié)點(diǎn)沒啥好比較的,畢竟是靜態(tài)的。當(dāng)沒有舊節(jié)點(diǎn)時(shí),則通過mountStaticNode
創(chuàng)建并插入新節(jié)點(diǎn);即使有舊節(jié)點(diǎn),也僅在_DEV_
條件下在hmr
,才會(huì)使用patchStaticVnode
做一下比較并通過removeStaticNode
移除某些舊節(jié)點(diǎn)。
const mountStaticNode = ( n2: VNode, container: RendererElement, anchor: RendererNode | null, isSVG: boolean ) => { // static nodes are only present when used with compiler-dom/runtime-dom // which guarantees presence of hostInsertStaticContent. ;[n2.el, n2.anchor] = hostInsertStaticContent!( n2.children as string, container, anchor, isSVG, n2.el, n2.anchor ) } /** * Dev / HMR only */ const patchStaticNode = ( n1: VNode, n2: VNode, container: RendererElement, isSVG: boolean ) => { // static nodes are only patched during dev for HMR if (n2.children !== n1.children) { const anchor = hostNextSibling(n1.anchor!) // 移除已有的靜態(tài)節(jié)點(diǎn),并插入新的節(jié)點(diǎn) // remove existing removeStaticNode(n1) // insert new ;[n2.el, n2.anchor] = hostInsertStaticContent!( n2.children as string, container, anchor, isSVG ) } else { n2.el = n1.el n2.anchor = n1.anchor } } // removeStaticNode:從 n1.el 至 n1.anchor 的內(nèi)容被遍歷移除 const removeStaticNode = ({ el, anchor }: VNode) => { let next while (el && el !== anchor) { next = hostNextSibling(el) hostRemove(el) el = next } hostRemove(anchor!) }
processFragment
vue3
的單文件組件里,不再需要加一個(gè)根節(jié)點(diǎn),因?yàn)槭褂昧宋臋n片段fragment
來承載子節(jié)點(diǎn),最后再一并添加到文檔中。
若舊的片段節(jié)點(diǎn)為空,則插入起始錨點(diǎn),掛載新的子節(jié)點(diǎn);
舊的片段不為空:
- 存在優(yōu)化條件時(shí):使用
patchBlockChildren
優(yōu)化diff
; - 不存在優(yōu)化條件時(shí):使用
patchChildren
進(jìn)行全量diff
。
const processFragment = ( n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { // 錨點(diǎn) const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))! const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))! let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2 // 開發(fā)環(huán)境熱更新時(shí),強(qiáng)制全量diff if ( __DEV__ && // #5523 dev root fragment may inherit directives (isHmrUpdating || patchFlag & PatchFlags.DEV_ROOT_FRAGMENT) ) { // HMR updated / Dev root fragment (w/ comments), force full diff patchFlag = 0 optimized = false dynamicChildren = null } // 檢查是否是插槽 // check if this is a slot fragment with :slotted scope ids if (fragmentSlotScopeIds) { slotScopeIds = slotScopeIds ? slotScopeIds.concat(fragmentSlotScopeIds) : fragmentSlotScopeIds } // 當(dāng)舊的片段為空時(shí),掛載新的片段的子節(jié)點(diǎn) if (n1 == null) { hostInsert(fragmentStartAnchor, container, anchor) hostInsert(fragmentEndAnchor, container, anchor) // a fragment can only have array children // since they are either generated by the compiler, or implicitly created // from arrays. mountChildren( n2.children as VNodeArrayChildren, container, fragmentEndAnchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else { // 當(dāng)舊片段不為空時(shí),啟用優(yōu)化則使用patchBlockChildren if ( patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT && dynamicChildren && // #2715 the previous fragment could've been a BAILed one as a result // of renderSlot() with no valid children n1.dynamicChildren ) { // a stable fragment (template root or <template v-for>) doesn't need to // patch children order, but it may contain dynamicChildren. patchBlockChildren( n1.dynamicChildren, dynamicChildren, container, parentComponent, parentSuspense, isSVG, slotScopeIds ) // 開發(fā)環(huán)境,熱更新 處理靜態(tài)子節(jié)點(diǎn) if (__DEV__ && parentComponent && parentComponent.type.__hmrId) { traverseStaticChildren(n1, n2) } else if ( // #2080 if the stable fragment has a key, it's a <template v-for> that may // get moved around. Make sure all root level vnodes inherit el. // #2134 or if it's a component root, it may also get moved around // as the component is being moved. n2.key != null || (parentComponent && n2 === parentComponent.subTree) ) { traverseStaticChildren(n1, n2, true /* shallow */) } } else { // 不可優(yōu)化時(shí),使用patchChildren處理 // keyed / unkeyed, or manual fragments. // for keyed & unkeyed, since they are compiler generated from v-for, // each child is guaranteed to be a block so the fragment will never // have dynamicChildren. patchChildren( n1, n2, container, fragmentEndAnchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } } }
patchBlockChildren
在文檔片段中的diff
中,當(dāng)符合優(yōu)化條件時(shí),則調(diào)用patchBlockChildren
來進(jìn)行優(yōu)化的diff
。這里主要以新節(jié)點(diǎn)的子節(jié)點(diǎn)長(zhǎng)度為準(zhǔn),遍歷新舊節(jié)點(diǎn)的子節(jié)點(diǎn),更新了每個(gè)子節(jié)點(diǎn)的container
然后進(jìn)行patch
。
// The fast path for blocks. const patchBlockChildren: PatchBlockChildrenFn = ( oldChildren, newChildren, fallbackContainer, parentComponent, parentSuspense, isSVG, slotScopeIds ) => { for (let i = 0; i < newChildren.length; i++) { const oldVNode = oldChildren[i] const newVNode = newChildren[i] // Determine the container (parent element) for the patch. const container = // oldVNode may be an errored async setup() component inside Suspense // which will not have a mounted element oldVNode.el && // - In the case of a Fragment, we need to provide the actual parent // of the Fragment itself so it can move its children. (oldVNode.type === Fragment || // - In the case of different nodes, there is going to be a replacement // which also requires the correct parent container !isSameVNodeType(oldVNode, newVNode) || // - In the case of a component, it could contain anything. oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT)) ? hostParentNode(oldVNode.el)! : // In other cases, the parent container is not actually used so we // just pass the block element here to avoid a DOM parentNode call. fallbackContainer patch( oldVNode, newVNode, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, true ) } }
patchChildren
在沒有優(yōu)化條件時(shí),使用patchChildren
對(duì)子節(jié)點(diǎn)進(jìn)行全量的diff
。
const patchChildren: PatchChildrenFn = ( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized = false ) => { const c1 = n1 && n1.children const prevShapeFlag = n1 ? n1.shapeFlag : 0 const c2 = n2.children const { patchFlag, shapeFlag } = n2 // 走綠色通道:用patchFlag來保證children是數(shù)組 // fast path if (patchFlag > 0) { if (patchFlag & PatchFlags.KEYED_FRAGMENT) { // 有key屬性的時(shí)候,根據(jù)key來進(jìn)行diff // this could be either fully-keyed or mixed (some keyed some not) // presence of patchFlag means children are guaranteed to be arrays patchKeyedChildren( c1 as VNode[], c2 as VNodeArrayChildren, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) return } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) { // 沒有key // unkeyed patchUnkeyedChildren( c1 as VNode[], c2 as VNodeArrayChildren, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) return } } // 沒有patchFlag的保證,則children可能為文本、數(shù)組或空 // 根據(jù)形狀標(biāo)識(shí)來判斷 // children has 3 possibilities: text, array or no children. if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { // 文本子節(jié)點(diǎn)的綠色通道 // text children fast path if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { unmountChildren(c1 as VNode[], parentComponent, parentSuspense) } if (c2 !== c1) { hostSetElementText(container, c2 as string) } } else { // 舊的子節(jié)點(diǎn)是數(shù)組 if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { // prev children was array if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 新舊子節(jié)點(diǎn)都是數(shù)組,需要進(jìn)行全量diff // two arrays, cannot assume anything, do full diff patchKeyedChildren( c1 as VNode[], c2 as VNodeArrayChildren, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else { // 新的子節(jié)點(diǎn)為空,則只需要卸載舊的子節(jié)點(diǎn) // no new children, just unmount old unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true) } } else { // 舊的子節(jié)點(diǎn)為文本節(jié)點(diǎn)或者空,新的為數(shù)組或空 // prev children was text OR null // new children is array OR null if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { // 舊的為文本節(jié)點(diǎn),先將其文本置空 hostSetElementText(container, '') } // 新的為數(shù)組,則通過mountChildren掛載子節(jié)點(diǎn) // mount new if array if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { mountChildren( c2 as VNodeArrayChildren, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } } } }
patchKeyedChildren
使用patchKeyedChildren
來比較兩組有key
,或者有key
和沒有key
混合的children
,屬于diff
的核心內(nèi)容了。
從前往后依次對(duì)比相同索引位置的節(jié)點(diǎn)類型,當(dāng)遇到節(jié)點(diǎn)類型不同則退出比較;
再從后往前對(duì)比相同倒序位置上的節(jié)點(diǎn)類型,遇到不同類型則退出比較;
如果舊節(jié)點(diǎn)組遍歷完,而新節(jié)點(diǎn)組還有內(nèi)容,則掛載新節(jié)點(diǎn)組里的剩余內(nèi)容;
如果新節(jié)點(diǎn)組遍歷完,而舊節(jié)點(diǎn)組還有內(nèi)容,則卸載舊節(jié)點(diǎn)組里的剩余內(nèi)容;
如果都沒有遍歷完:
- 將新節(jié)點(diǎn)組的剩余內(nèi)容以
key=>index
的形式存入Map
; - 遍歷剩余的舊子節(jié)點(diǎn),在
Map
中找到相同的key
對(duì)應(yīng)的index
; - 如果舊子節(jié)點(diǎn)沒有
key
,則找到新子節(jié)點(diǎn)組的剩余子節(jié)點(diǎn)中尚未被匹配到且類型相同的節(jié)點(diǎn)對(duì)應(yīng)的index
; - 求出最大遞增子序列;
- 卸載不匹配的舊子節(jié)點(diǎn)、掛載未被匹配的新子節(jié)點(diǎn),移動(dòng)需要移動(dòng)的可復(fù)用子節(jié)點(diǎn)。
// can be all-keyed or mixed const patchKeyedChildren = ( c1: VNode[], c2: VNodeArrayChildren, container: RendererElement, parentAnchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { let i = 0 const l2 = c2.length // 兩組各自的尾節(jié)點(diǎn) let e1 = c1.length - 1 // prev ending index let e2 = l2 - 1 // next ending index // (從前往后) // 以兩組中最短的一組為基準(zhǔn) // 從頭結(jié)點(diǎn)開始,依次比較同一位置的節(jié)點(diǎn)類型,若頭節(jié)點(diǎn)類型相同,則對(duì)兩個(gè)節(jié)點(diǎn)進(jìn)行patch進(jìn)行比較; // 若類型不同則退出循環(huán) // 1. sync from start // (a b) c // (a b) d e while (i <= e1 && i <= e2) { const n1 = c1[i] const n2 = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) if (isSameVNodeType(n1, n2)) { patch( n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else { break } // i++ } // (從后往前) // 從尾節(jié)點(diǎn)開始,尾節(jié)點(diǎn)類型相同,則通過patch比較尾節(jié)點(diǎn); // 若類型不同則退出循環(huán) // 2. sync from end // a (b c) // d e (b c) while (i <= e1 && i <= e2) { const n1 = c1[e1] const n2 = (c2[e2] = optimized ? cloneIfMounted(c2[e2] as VNode) : normalizeVNode(c2[e2])) if (isSameVNodeType(n1, n2)) { patch( n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else { break } e1-- e2-- } // 經(jīng)過前后兩輪比較之后,剩下的就是中間那部分類型不同的子節(jié)點(diǎn)了 // 若舊的子節(jié)點(diǎn)組已經(jīng)遍歷完,而新的子節(jié)點(diǎn)組還有剩余內(nèi)容 // 通過patch處理剩下的新的子節(jié)點(diǎn)中的內(nèi)容,由于舊的子節(jié)點(diǎn)為空, // 因此相當(dāng)于在patch內(nèi)部掛載剩余的新的子節(jié)點(diǎn) // 3. common sequence + mount // (a b) // (a b) c // i = 2, e1 = 1, e2 = 2 // (a b) // c (a b) // i = 0, e1 = -1, e2 = 0 if (i > e1) { if (i <= e2) { const nextPos = e2 + 1 const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor while (i <= e2) { patch( null, (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])), container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) i++ } } } // 舊的子節(jié)點(diǎn)還有剩余內(nèi)容而新的子節(jié)點(diǎn)組已經(jīng)遍歷完,則卸載舊子節(jié)點(diǎn)組剩余的那部分 // 4. common sequence + unmount // (a b) c // (a b) // i = 2, e1 = 2, e2 = 1 // a (b c) // (b c) // i = 0, e1 = 0, e2 = -1 else if (i > e2) { while (i <= e1) { unmount(c1[i], parentComponent, parentSuspense, true) i++ } } // 新舊子節(jié)點(diǎn)組都沒有遍歷完,如下注釋中[]里的部分 // 5. unknown sequence // [i ... e1 + 1]: a b [c d e] f g // [i ... e2 + 1]: a b [e d c h] f g // i = 2, e1 = 4, e2 = 5 else { // 拿到上次比較完的起點(diǎn) const s1 = i // prev starting index const s2 = i // next starting index // 5.1 build key:index map for newChildren const keyToNewIndexMap: Map<string | number | symbol, number> = new Map() // 用Map存儲(chǔ)新的子節(jié)點(diǎn)組的key和對(duì)應(yīng)的index, key=>index 并給出重復(fù)的key的警告 for (i = s2; i <= e2; i++) { const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) if (nextChild.key != null) { if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) { warn( `Duplicate keys found during update:`, JSON.stringify(nextChild.key), `Make sure keys are unique.` ) } keyToNewIndexMap.set(nextChild.key, i) } } // 5.2 loop through old children left to be patched and try to patch // matching nodes & remove nodes that are no longer present let j // 已比較的數(shù)量 let patched = 0 // 未比較的數(shù)量 const toBePatched = e2 - s2 + 1 let moved = false // used to track whether any node has moved let maxNewIndexSoFar = 0 // works as Map<newIndex, oldIndex> // Note that oldIndex is offset by +1 // and oldIndex = 0 is a special value indicating the new node has // no corresponding old node. // used for determining longest stable subsequence // 以新的子節(jié)點(diǎn)組中未完成比較的節(jié)點(diǎn)為基準(zhǔn) const newIndexToOldIndexMap = new Array(toBePatched) // 先用0來填充,標(biāo)記為沒有key的節(jié)點(diǎn)。 ps:直接fill(0)不就好了么 for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0 // 處理舊的子節(jié)點(diǎn)組 for (i = s1; i <= e1; i++) { const prevChild = c1[i] // 當(dāng)已經(jīng)比較完了(patched >= toBePatched),卸載舊的子節(jié)點(diǎn) if (patched >= toBePatched) { // all new children have been patched so this can only be a removal unmount(prevChild, parentComponent, parentSuspense, true) continue } let newIndex // 當(dāng)舊的子節(jié)點(diǎn)的key存在,取出key在新的子節(jié)點(diǎn)組中對(duì)應(yīng)的index if (prevChild.key != null) { newIndex = keyToNewIndexMap.get(prevChild.key) } else { // 若舊的子節(jié)點(diǎn)沒有key,找出沒有key且類型相同的節(jié)點(diǎn)對(duì)應(yīng)在新子節(jié)點(diǎn)組中的index // key-less node, try to locate a key-less node of the same type for (j = s2; j <= e2; j++) { if ( newIndexToOldIndexMap[j - s2] === 0 && isSameVNodeType(prevChild, c2[j] as VNode) ) { newIndex = j break } } } // newIndex不存在,即根據(jù)key來找,發(fā)現(xiàn)舊的子節(jié)點(diǎn)不可復(fù)用,則卸載舊的子節(jié)點(diǎn) if (newIndex === undefined) { unmount(prevChild, parentComponent, parentSuspense, true) } else { // 找到了可復(fù)用的節(jié)點(diǎn),在newIndexToOldIndexMap中標(biāo)記 i+1, // 用于最大上升子序列算法 newIndexToOldIndexMap[newIndex - s2] = i + 1 // 刷新目前找到的最大的新子節(jié)點(diǎn)的index,做節(jié)點(diǎn)移動(dòng)標(biāo)記 if (newIndex >= maxNewIndexSoFar) { maxNewIndexSoFar = newIndex } else { moved = true } // 再遞歸詳細(xì)比較兩個(gè)節(jié)點(diǎn) patch( prevChild, c2[newIndex] as VNode, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) // 已對(duì)比的數(shù)量+1 patched++ } } // 當(dāng)需要移動(dòng)時(shí),采用最大遞增子序列算法,從而最大限度減少節(jié)點(diǎn)移動(dòng)次數(shù) // 5.3 move and mount // generate longest stable subsequence only when nodes have moved const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : EMPTY_ARR j = increasingNewIndexSequence.length - 1 // 倒序遍歷,好處是可以使用上一次對(duì)比的節(jié)點(diǎn)作為錨點(diǎn) // looping backwards so that we can use last patched node as anchor for (i = toBePatched - 1; i >= 0; i--) { const nextIndex = s2 + i const nextChild = c2[nextIndex] as VNode const anchor = nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor if (newIndexToOldIndexMap[i] === 0) { // 等于0說明未被舊的子節(jié)點(diǎn)匹配到,屬于全新的不可復(fù)用的子節(jié)點(diǎn),則通過patch進(jìn)行掛載 // mount new patch( null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else if (moved) { // 當(dāng)計(jì)算出來的最大上升子序列為空數(shù)組, // 或者當(dāng)前節(jié)點(diǎn)不處于最大上升子序列中 // move if: // There is no stable subsequence (e.g. a reverse) // OR current node is not among the stable sequence if (j < 0 || i !== increasingNewIndexSequence[j]) { move(nextChild, container, anchor, MoveType.REORDER) } else { j-- } } } } }
patchUnkeyedChildren
沒有key
的時(shí)候就很直接了,只依照最短的那組的長(zhǎng)度,來按位置進(jìn)行比較。而后該卸載就卸載,該掛載就掛載。
const patchUnkeyedChildren = ( c1: VNode[], c2: VNodeArrayChildren, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { c1 = c1 || EMPTY_ARR c2 = c2 || EMPTY_ARR const oldLength = c1.length const newLength = c2.length const commonLength = Math.min(oldLength, newLength) let i for (i = 0; i < commonLength; i++) { const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) patch( c1[i], nextChild, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } if (oldLength > newLength) { // remove old unmountChildren( c1, parentComponent, parentSuspense, true, false, commonLength ) } else { // mount new mountChildren( c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, commonLength ) } }
mountChildren
mountChildren
用于掛載子節(jié)點(diǎn),主要是遍歷子節(jié)點(diǎn),處理每個(gè)子節(jié)點(diǎn),得到復(fù)制的或者標(biāo)準(zhǔn)化的單個(gè)子節(jié)點(diǎn),然后遞歸調(diào)用patch
。
const mountChildren: MountChildrenFn = ( children, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, start = 0 ) => { for (let i = start; i < children.length; i++) { const child = (children[i] = optimized ? cloneIfMounted(children[i] as VNode) : normalizeVNode(children[i])) patch( null, child, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } }
unmountChildren
遍歷子節(jié)點(diǎn)組,調(diào)用unmount
方法卸載子節(jié)點(diǎn)。
const unmountChildren: UnmountChildrenFn = ( children, parentComponent, parentSuspense, doRemove = false, optimized = false, start = 0 ) => { for (let i = start; i < children.length; i++) { unmount(children[i], parentComponent, parentSuspense, doRemove, optimized) } }
move
在有key
的子節(jié)點(diǎn)比較中,出現(xiàn)了需要移動(dòng)子節(jié)點(diǎn)的情況,而移動(dòng)就是通過move
來完成的。按照不同的節(jié)點(diǎn)類型,處理方式有所差異。
const move: MoveFn = ( vnode, container, anchor, moveType, parentSuspense = null ) => { const { el, type, transition, children, shapeFlag } = vnode // 對(duì)于組件節(jié)點(diǎn),遞歸處理subTree if (shapeFlag & ShapeFlags.COMPONENT) { move(vnode.component!.subTree, container, anchor, moveType) return } // 處理異步組件<Suspense> if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { vnode.suspense!.move(container, anchor, moveType) return } // 處理<Teleport> if (shapeFlag & ShapeFlags.TELEPORT) { ;(type as typeof TeleportImpl).move(vnode, container, anchor, internals) return } // 文檔片段,處理起始錨點(diǎn)和子節(jié)點(diǎn) if (type === Fragment) { hostInsert(el!, container, anchor) for (let i = 0; i < (children as VNode[]).length; i++) { move((children as VNode[])[i], container, anchor, moveType) } hostInsert(vnode.anchor!, container, anchor) return } // 靜態(tài)節(jié)點(diǎn) if (type === Static) { moveStaticNode(vnode, container, anchor) return } // 處理<Transition>的鉤子 // single nodes const needTransition = moveType !== MoveType.REORDER && shapeFlag & ShapeFlags.ELEMENT && transition if (needTransition) { if (moveType === MoveType.ENTER) { transition!.beforeEnter(el!) hostInsert(el!, container, anchor) queuePostRenderEffect(() => transition!.enter(el!), parentSuspense) } else { const { leave, delayLeave, afterLeave } = transition! const remove = () => hostInsert(el!, container, anchor) const performLeave = () => { leave(el!, () => { remove() afterLeave && afterLeave() }) } if (delayLeave) { delayLeave(el!, remove, performLeave) } else { performLeave() } } } else { hostInsert(el!, container, anchor) } }
processElement
processElement
內(nèi)容很簡(jiǎn)單,判斷一下是否要當(dāng)作svg
處理;之后,如果舊節(jié)點(diǎn)為空,則直接通過mountElement
掛載新的元素節(jié)點(diǎn),否則通過patchElement
對(duì)元素節(jié)點(diǎn)進(jìn)行對(duì)比。
const processElement = ( n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { isSVG = isSVG || (n2.type as string) === 'svg' if (n1 == null) { mountElement( n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else { patchElement( n1, n2, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } }
mountElement
假如此時(shí)舊節(jié)點(diǎn)為空,那么就會(huì)調(diào)用mountElement
,我們來看看它是怎么做的。
- 若
vndoe
上的el
屬性存在,開發(fā)環(huán)境下則簡(jiǎn)單對(duì)el
進(jìn)行復(fù)制;不存在則新建; - 先進(jìn)行子節(jié)點(diǎn)的掛載,因?yàn)槟承?code>props依賴于子節(jié)點(diǎn)的渲染;
- 指令的
created
階段; - 處理
props
并設(shè)置scopeId
; - 開發(fā)環(huán)境下設(shè)置
el.__vnode
和el.vueParentComponent
的取值,并設(shè)置為不可枚舉; - 指令的
beforeMounted
階段; - 動(dòng)畫組件
Transition
的beforeEnter
鉤子; - 執(zhí)行
vnode
上的鉤子、Transition
的enter
鉤子、指令的mounted
鉤子等
const mountElement = ( vnode: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { let el: RendererElement let vnodeHook: VNodeHook | undefined | null const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode if ( !__DEV__ && vnode.el && hostCloneNode !== undefined && patchFlag === PatchFlags.HOISTED ) { // vnode的el元素存在,僅在生產(chǎn)環(huán)境下對(duì)可復(fù)用的靜態(tài)節(jié)點(diǎn)進(jìn)行復(fù)制 // If a vnode has non-null el, it means it's being reused. // Only static vnodes can be reused, so its mounted DOM nodes should be // exactly the same, and we can simply do a clone here. // only do this in production since cloned trees cannot be HMR updated. el = vnode.el = hostCloneNode(vnode.el) } else { // vnode上的元素不存在則新建 el = vnode.el = hostCreateElement( vnode.type as string, isSVG, props && props.is, props ) // 注釋:由于某些props依賴于子節(jié)點(diǎn)的渲染,先掛載子節(jié)點(diǎn) // mount children first, since some props may rely on child content // being already rendered, e.g. `<select value>` if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { // 設(shè)置元素文本 hostSetElementText(el, vnode.children as string) } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 掛載子節(jié)點(diǎn) mountChildren( vnode.children as VNodeArrayChildren, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', slotScopeIds, optimized ) } // 指令的created階段 if (dirs) { invokeDirectiveHook(vnode, null, parentComponent, 'created') } // 處理元素的props // props if (props) { for (const key in props) { if (key !== 'value' && !isReservedProp(key)) { hostPatchProp( el, key, null, props[key], isSVG, vnode.children as VNode[], parentComponent, parentSuspense, unmountChildren ) } } /** * Special case for setting value on DOM elements: * - it can be order-sensitive (e.g. should be set *after* min/max, #2325, #4024) * - it needs to be forced (#1471) * #2353 proposes adding another renderer option to configure this, but * the properties affects are so finite it is worth special casing it * here to reduce the complexity. (Special casing it also should not * affect non-DOM renderers) */ if ('value' in props) { hostPatchProp(el, 'value', null, props.value) } if ((vnodeHook = props.onVnodeBeforeMount)) { invokeVNodeHook(vnodeHook, parentComponent, vnode) } } // scopeId setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent) } // __DEV__環(huán)境下處理 __vnode屬性和父組件為不可枚舉 if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { Object.defineProperty(el, '__vnode', { value: vnode, enumerable: false }) Object.defineProperty(el, '__vueParentComponent', { value: parentComponent, enumerable: false }) } // 執(zhí)行指令中的 beforeMount 階段 if (dirs) { invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount') } // #1583 For inside suspense + suspense not resolved case, enter hook should call when suspense resolved // #1689 For inside suspense + suspense resolved case, just call it // 是否需要執(zhí)行動(dòng)畫組件鉤子 const needCallTransitionHooks = (!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) && transition && !transition.persisted if (needCallTransitionHooks) { transition!.beforeEnter(el) } hostInsert(el, container, anchor) if ( (vnodeHook = props && props.onVnodeMounted) || needCallTransitionHooks || dirs ) { // 加入組件更新后的副作用執(zhí)行隊(duì)列,在合適的時(shí)機(jī)執(zhí)行入隊(duì)的函數(shù) // 這里是一些鉤子函數(shù)、trasition的鉤子、指令在mounted階段的鉤子 queuePostRenderEffect(() => { vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode) needCallTransitionHooks && transition!.enter(el) dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted') }, parentSuspense) } }
patchElement
patchElemengt
相當(dāng)重要,因?yàn)槠渌湍愣鄡?nèi)容的patch
,最終經(jīng)過遞歸,依然會(huì)走到patchElement
。當(dāng)新舊元素節(jié)點(diǎn)都存在時(shí),就會(huì)調(diào)用patchElement
進(jìn)行對(duì)比??梢钥吹巾樞颍?/p>
beforeUpdated -> 子節(jié)點(diǎn) -> class/style -> 其它props/attrs -> updated
關(guān)閉recurse
,處理beforeUpdated
鉤子;
處理指定的beforeUpdated
階段,再啟用recurse
;
在__DEV__
環(huán)境下的熱更新時(shí),則會(huì)清理優(yōu)化標(biāo)記,從而強(qiáng)制對(duì)節(jié)點(diǎn)進(jìn)行全量的比較(full diff
);
處理動(dòng)態(tài)子節(jié)點(diǎn):
- 當(dāng)新節(jié)點(diǎn)中有動(dòng)態(tài)子節(jié)點(diǎn),則通過
patchBlockChildren
來和舊節(jié)點(diǎn)的動(dòng)態(tài)子節(jié)點(diǎn)進(jìn)行對(duì)比; - 否則,如果沒有優(yōu)化(
!optimized
),則使用patchChildren
對(duì)子節(jié)點(diǎn)進(jìn)行全量diff
;
判斷patchFlag > 0
,大于0
時(shí)則元素的render
代碼由compiler
生成,有優(yōu)化buff
:
- 如果
props
中有動(dòng)態(tài)的key
,則優(yōu)化無效,進(jìn)行全量diff
; - 處理動(dòng)態(tài)類名和動(dòng)態(tài)
style
,優(yōu)化diff
; - 處理其它的
prop/attr
,如果其中有動(dòng)態(tài)的key
,則優(yōu)化無效; - 處理文本:當(dāng)元素只有文本子節(jié)點(diǎn)時(shí),則將文本子節(jié)點(diǎn)設(shè)置為新的元素節(jié)點(diǎn)的內(nèi)容;
patchFlag <= 0
,且沒有設(shè)置優(yōu)化時(shí),對(duì)props
進(jìn)行全量diff
;
updated
階段。
const patchElement = ( n1: VNode, n2: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { const el = (n2.el = n1.el!) let { patchFlag, dynamicChildren, dirs } = n2 // #1426 take the old vnode's patch flag into account since user may clone a // compiler-generated vnode, which de-opts to FULL_PROPS patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS const oldProps = n1.props || EMPTY_OBJ const newProps = n2.props || EMPTY_OBJ let vnodeHook: VNodeHook | undefined | null // 關(guān)閉recurse,在 beforeUpdated 階段不允許自己調(diào)用 // disable recurse in beforeUpdate hooks parentComponent && toggleRecurse(parentComponent, false) // beforeUpdated鉤子 if ((vnodeHook = newProps.onVnodeBeforeUpdate)) { invokeVNodeHook(vnodeHook, parentComponent, n2, n1) } // 指令的 beforeUpdated 鉤子 if (dirs) { invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate') } // 允許自己調(diào)用 parentComponent && toggleRecurse(parentComponent, true) // 開發(fā)環(huán)境呢下,關(guān)閉優(yōu)化,全量diff if (__DEV__ && isHmrUpdating) { // HMR updated, force full diff patchFlag = 0 optimized = false dynamicChildren = null } const areChildrenSVG = isSVG && n2.type !== 'foreignObject' // 新節(jié)點(diǎn)的動(dòng)態(tài)子節(jié)點(diǎn)不為空,則比較新舊節(jié)點(diǎn)的動(dòng)態(tài)子節(jié)點(diǎn) if (dynamicChildren) { patchBlockChildren( n1.dynamicChildren!, dynamicChildren, el, parentComponent, parentSuspense, areChildrenSVG, slotScopeIds ) // 開發(fā)環(huán)境 遞歸遍歷靜態(tài)子節(jié)點(diǎn) if (__DEV__ && parentComponent && parentComponent.type.__hmrId) { traverseStaticChildren(n1, n2) } // 沒有優(yōu)化,全量 diff } else if (!optimized) { // full diff patchChildren( n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG, slotScopeIds, false ) } // 注釋:patchFlag 標(biāo)識(shí)的存在意味著元素的 render 代碼是由 compiler 生成的, // 且可以在 patch 時(shí)走快道,此時(shí)能保證新舊節(jié)點(diǎn)形狀相同,即它們?cè)谠茨0逯姓锰幱谙嗤奈恢? // 此時(shí)的對(duì)比是有著各種優(yōu)化的 if (patchFlag > 0) { // the presence of a patchFlag means this element's render code was // generated by the compiler and can take the fast path. // in this path old node and new node are guaranteed to have the same shape // (i.e. at the exact same position in the source template) if (patchFlag & PatchFlags.FULL_PROPS) { // 當(dāng)props中含有動(dòng)態(tài)的key,需要進(jìn)行全量 diff // element props contain dynamic keys, full diff needed patchProps( el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG ) } else { // 處理動(dòng)態(tài)類名綁定 // class // this flag is matched when the element has dynamic class bindings. if (patchFlag & PatchFlags.CLASS) { if (oldProps.class !== newProps.class) { hostPatchProp(el, 'class', null, newProps.class, isSVG) } } // 處理動(dòng)態(tài)的 style 綁定 // style // this flag is matched when the element has dynamic style bindings if (patchFlag & PatchFlags.STYLE) { hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG) } // 處理動(dòng)態(tài)的 prop/attr 綁定,有迭代緩存,優(yōu)化比較速度 // 如果 `prop/attr`的 key 是動(dòng)態(tài)的,那么這種優(yōu)化則會(huì)失效 // props // This flag is matched when the element has dynamic prop/attr bindings // other than class and style. The keys of dynamic prop/attrs are saved for // faster iteration. // Note dynamic keys like :[foo]="bar" will cause this optimization to // bail out and go through a full diff because we need to unset the old key if (patchFlag & PatchFlags.PROPS) { // if the flag is present then dynamicProps must be non-null const propsToUpdate = n2.dynamicProps! for (let i = 0; i < propsToUpdate.length; i++) { const key = propsToUpdate[i] const prev = oldProps[key] const next = newProps[key] // value屬性會(huì)被強(qiáng)行對(duì)比 // #1471 force patch value if (next !== prev || key === 'value') { hostPatchProp( el, key, prev, next, isSVG, n1.children as VNode[], parentComponent, parentSuspense, unmountChildren ) } } } } // 處理文本:僅在元素只有文本子節(jié)點(diǎn)時(shí)觸發(fā) // text // This flag is matched when the element has only dynamic text children. if (patchFlag & PatchFlags.TEXT) { if (n1.children !== n2.children) { hostSetElementText(el, n2.children as string) } } } else if (!optimized && dynamicChildren == null) { // 沒有優(yōu)化,全量 diff // unoptimized, full diff patchProps( el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG ) } // updated 鉤子 入隊(duì) if ((vnodeHook = newProps.onVnodeUpdated) || dirs) { queuePostRenderEffect(() => { vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1) dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated') }, parentSuspense) } }
在patchElement
中,注意到當(dāng)新節(jié)點(diǎn)具有動(dòng)態(tài)子節(jié)點(diǎn)時(shí),調(diào)用了patchBlockChildren
來進(jìn)行子節(jié)點(diǎn)的比較,而在沒有動(dòng)態(tài)子節(jié)點(diǎn)且不符合優(yōu)化條件時(shí),則使用patchChildren
來比較。這與processFragment
類似。
而當(dāng)patchFlag <= 0
且沒有設(shè)置優(yōu)化時(shí),對(duì)props
進(jìn)行全量diff
。分別遍歷新的props
和舊的props
,最后刷新value
的值。
const patchProps = ( el: RendererElement, vnode: VNode, oldProps: Data, newProps: Data, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean ) => { if (oldProps !== newProps) { // 遍歷新的props for (const key in newProps) { // empty string is not valid prop if (isReservedProp(key)) continue const next = newProps[key] const prev = oldProps[key] // 先不比較 value // defer patching value if (next !== prev && key !== 'value') { hostPatchProp( el, key, prev, next, isSVG, vnode.children as VNode[], parentComponent, parentSuspense, unmountChildren ) } } // 遍歷舊的props if (oldProps !== EMPTY_OBJ) { for (const key in oldProps) { if (!isReservedProp(key) && !(key in newProps)) { hostPatchProp( el, key, oldProps[key], null, isSVG, vnode.children as VNode[], parentComponent, parentSuspense, unmountChildren ) } } } // 最后處理 value if ('value' in newProps) { hostPatchProp(el, 'value', oldProps.value, newProps.value) } } }
processComponent
當(dāng)被patch
的節(jié)點(diǎn)類型是組件時(shí),通過processComponent
來處理。
當(dāng)舊組件節(jié)點(diǎn)存在時(shí),則調(diào)用updateComponent
進(jìn)行更新;
否則:
- 當(dāng)新組件節(jié)點(diǎn)為
KeepAlive
時(shí),調(diào)用其上下文對(duì)象上的activate
方法; - 否則,使用
mountComponent
掛載新的組件節(jié)點(diǎn);
const processComponent = ( n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { n2.slotScopeIds = slotScopeIds if (n1 == null) { if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { ;(parentComponent!.ctx as KeepAliveContext).activate( n2, container, anchor, isSVG, optimized ) } else { mountComponent( n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } } else { updateComponent(n1, n2, optimized) } }
mountComponent
mountComponent
在舊的組件節(jié)點(diǎn)不存在時(shí)被調(diào)用。所有的mountXXX
最常見的調(diào)用時(shí)機(jī)都是首次渲染時(shí),舊節(jié)點(diǎn)都是空的。
const mountComponent: MountComponentFn = ( initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) => { // 2.x compat may pre-create the component instance before actually // mounting const compatMountInstance = __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component const instance: ComponentInternalInstance = compatMountInstance || (initialVNode.component = createComponentInstance( initialVNode, parentComponent, parentSuspense )) // 注冊(cè)熱更新 if (__DEV__ && instance.type.__hmrId) { registerHMR(instance) } // 掛載性能檢測(cè) if (__DEV__) { pushWarningContext(initialVNode) startMeasure(instance, `mount`) } // 注入renderer的內(nèi)部?jī)?nèi)容 // inject renderer internals for keepAlive if (isKeepAlive(initialVNode)) { ;(instance.ctx as KeepAliveContext).renderer = internals } /** 這里備注一下 internals 的內(nèi)容 * const internals: RendererInternals = { * p: patch, * um: unmount, * m: move, * r: remove, * mt: mountComponent, * mc: mountChildren, * pc: patchChildren, * pbc: patchBlockChildren, * n: getNextHostNode, * o: options * } */ // 處理props和插槽 // resolve props and slots for setup context if (!(__COMPAT__ && compatMountInstance)) { // 檢測(cè)初始化性能 if (__DEV__) { startMeasure(instance, `init`) } // 處理setup:這個(gè)函數(shù)里使用其它方法,初始化了props和插槽,且調(diào)用了setup setupComponent(instance) if (__DEV__) { endMeasure(instance, `init`) } } // 處理異步的setup // setup() is async. This component relies on async logic to be resolved // before proceeding if (__FEATURE_SUSPENSE__ && instance.asyncDep) { parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect) // Give it a placeholder if this is not hydration // TODO handle self-defined fallback if (!initialVNode.el) { const placeholder = (instance.subTree = createVNode(Comment)) processCommentNode(null, placeholder, container!, anchor) } return } // 接下來根據(jù)setup返回內(nèi)容進(jìn)行渲染 // todo 閱讀該函數(shù)的內(nèi)容 setupRenderEffect( instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized ) // mount性能檢測(cè)結(jié)束點(diǎn) if (__DEV__) { popWarningContext() endMeasure(instance, `mount`) } }
updateComponent
當(dāng)舊的組件節(jié)點(diǎn)存在時(shí),對(duì)組件節(jié)點(diǎn)的處理會(huì)進(jìn)入到更新階段,也就是updateComponent
。以舊組件為基準(zhǔn)拿到實(shí)例instance
,通過shouldUpdateComponent
判斷是否要更新組件。如果不需要更新,則只復(fù)制一下屬性;否則,當(dāng)實(shí)例是異步組件時(shí),則只更新props
和插槽;當(dāng)實(shí)例是同步組件時(shí),則設(shè)置next
為新的組件節(jié)點(diǎn),并調(diào)用組件的update
方法進(jìn)行更新。
const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => { const instance = (n2.component = n1.component)! if (shouldUpdateComponent(n1, n2, optimized)) { if ( __FEATURE_SUSPENSE__ && instance.asyncDep && !instance.asyncResolved ) { // async & still pending - just update props and slots // since the component's reactive effect for render isn't set-up yet if (__DEV__) { pushWarningContext(n2) } // 更新組件的預(yù)渲染:即處理props和插槽 updateComponentPreRender(instance, n2, optimized) if (__DEV__) { popWarningContext() } return } else { // normal update instance.next = n2 // in case the child component is also queued, remove it to avoid // double updating the same child component in the same flush. invalidateJob(instance.update) // instance.update is the reactive effect. instance.update() } } else { // no update needed. just copy over properties n2.el = n1.el instance.vnode = n2 } }
updateComponentPreRender
組件的預(yù)渲染,即在這里處理props
和插槽。
const updateComponentPreRender = ( instance: ComponentInternalInstance, nextVNode: VNode, optimized: boolean ) => { nextVNode.component = instance const prevProps = instance.vnode.props instance.vnode = nextVNode instance.next = null updateProps(instance, nextVNode.props, prevProps, optimized) updateSlots(instance, nextVNode.children, optimized) pauseTracking() // props update may have triggered pre-flush watchers. // flush them before the render update. flushPreFlushCbs(undefined, instance.update) resetTracking() }
setupRenderEffect
相當(dāng)重要的一個(gè)函數(shù)。用componentUpdateFn
來創(chuàng)建一個(gè)effect
。最后執(zhí)行的update
函數(shù)以及實(shí)例的update
方法,都是執(zhí)行effect.run
。而effect.run
內(nèi)部會(huì)進(jìn)行與依賴收集相關(guān)的操作,還會(huì)調(diào)用新建effect
時(shí)傳入的函數(shù)componentUpdateFn
。這里可以看到**componentUpdateFn
分為掛載和更新兩部分**。
const setupRenderEffect: SetupRenderEffectFn = ( instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized ) => { const componentUpdateFn = () => { if (!instance.isMounted) { let vnodeHook: VNodeHook | null | undefined const { el, props } = initialVNode const { bm, m, parent } = instance const isAsyncWrapperVNode = isAsyncWrapper(initialVNode) // 在beforeMounted期間 不允許effect自己調(diào)用 toggleRecurse(instance, false) // beforeMount hook if (bm) { invokeArrayFns(bm) } // onVnodeBeforeMount if ( !isAsyncWrapperVNode && (vnodeHook = props && props.onVnodeBeforeMount) ) { invokeVNodeHook(vnodeHook, parent, initialVNode) } if ( __COMPAT__ && isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) ) { instance.emit('hook:beforeMount') } toggleRecurse(instance, true) if (el && hydrateNode) { // vnode has adopted host node - perform hydration instead of mount. const hydrateSubTree = () => { if (__DEV__) { startMeasure(instance, `render`) } instance.subTree = renderComponentRoot(instance) if (__DEV__) { endMeasure(instance, `render`) } if (__DEV__) { startMeasure(instance, `hydrate`) } hydrateNode!( el as Node, instance.subTree, instance, parentSuspense, null ) if (__DEV__) { endMeasure(instance, `hydrate`) } } if (isAsyncWrapperVNode) { ;(initialVNode.type as ComponentOptions).__asyncLoader!().then( // note: we are moving the render call into an async callback, // which means it won't track dependencies - but it's ok because // a server-rendered async wrapper is already in resolved state // and it will never need to change. () => !instance.isUnmounted && hydrateSubTree() ) } else { hydrateSubTree() } } else { if (__DEV__) { startMeasure(instance, `render`) } const subTree = (instance.subTree = renderComponentRoot(instance)) if (__DEV__) { endMeasure(instance, `render`) } if (__DEV__) { startMeasure(instance, `patch`) } patch( null, subTree, container, anchor, instance, parentSuspense, isSVG ) if (__DEV__) { endMeasure(instance, `patch`) } initialVNode.el = subTree.el } // mounted鉤子入隊(duì) // mounted hook if (m) { queuePostRenderEffect(m, parentSuspense) } // onVnodeMounted if ( !isAsyncWrapperVNode && (vnodeHook = props && props.onVnodeMounted) ) { const scopedInitialVNode = initialVNode queuePostRenderEffect( () => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode), parentSuspense ) } if ( __COMPAT__ && isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) ) { queuePostRenderEffect( () => instance.emit('hook:mounted'), parentSuspense ) } // <KeepAlive>組件的activated鉤子,可能包含從子組件注入的鉤子 // activated hook for keep-alive roots. // #1742 activated hook must be accessed after first render // since the hook may be injected by a child keep-alive if ( initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE || (parent && isAsyncWrapper(parent.vnode) && parent.vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) ) { instance.a && queuePostRenderEffect(instance.a, parentSuspense) // 兼容 if ( __COMPAT__ && isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) ) { queuePostRenderEffect( () => instance.emit('hook:activated'), parentSuspense ) } } // 變更組件掛載狀態(tài) instance.isMounted = true if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { devtoolsComponentAdded(instance) } // #2458: deference mount-only object parameters to prevent memleaks initialVNode = container = anchor = null as any } else { // updateComponent // This is triggered by mutation of component's own state (next: null) // OR parent calling processComponent (next: VNode) let { next, bu, u, parent, vnode } = instance let originNext = next let vnodeHook: VNodeHook | null | undefined if (__DEV__) { pushWarningContext(next || instance.vnode) } // beforeUpdated 期間也不允許effect自調(diào)用 // Disallow component effect recursion during pre-lifecycle hooks. toggleRecurse(instance, false) if (next) { next.el = vnode.el updateComponentPreRender(instance, next, optimized) } else { next = vnode } // beforeUpdate hook if (bu) { invokeArrayFns(bu) } // onVnodeBeforeUpdate if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) { invokeVNodeHook(vnodeHook, parent, next, vnode) } // 考慮兼容 if ( __COMPAT__ && isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) ) { instance.emit('hook:beforeUpdate') } toggleRecurse(instance, true) // render if (__DEV__) { startMeasure(instance, `render`) } const nextTree = renderComponentRoot(instance) if (__DEV__) { endMeasure(instance, `render`) } const prevTree = instance.subTree instance.subTree = nextTree if (__DEV__) { startMeasure(instance, `patch`) } // 更新則比較新舊subTree patch( prevTree, nextTree, // parent may have changed if it's in a teleport hostParentNode(prevTree.el!)!, // anchor may have changed if it's in a fragment getNextHostNode(prevTree), instance, parentSuspense, isSVG ) if (__DEV__) { endMeasure(instance, `patch`) } next.el = nextTree.el if (originNext === null) { // self-triggered update. In case of HOC, update parent component // vnode el. HOC is indicated by parent instance's subTree pointing // to child component's vnode updateHOCHostEl(instance, nextTree.el) } // 處理updated鉤子 // updated hook if (u) { queuePostRenderEffect(u, parentSuspense) } // onVnodeUpdated if ((vnodeHook = next.props && next.props.onVnodeUpdated)) { queuePostRenderEffect( () => invokeVNodeHook(vnodeHook!, parent, next!, vnode), parentSuspense ) } if ( __COMPAT__ && isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) ) { queuePostRenderEffect( () => instance.emit('hook:updated'), parentSuspense ) } if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { devtoolsComponentUpdated(instance) } if (__DEV__) { popWarningContext() } } } // 使用componentUpdateFn創(chuàng)建effect // create reactive effect for rendering const effect = (instance.effect = new ReactiveEffect( componentUpdateFn, () => queueJob(update), instance.scope // track it in component's effect scope )) const update: SchedulerJob = (instance.update = () => effect.run()) update.id = instance.uid // allowRecurse // #1801, #2043 component render effects should allow recursive updates toggleRecurse(instance, true) // 用于開發(fā)調(diào)試 if (__DEV__) { effect.onTrack = instance.rtc ? e => invokeArrayFns(instance.rtc!, e) : void 0 effect.onTrigger = instance.rtg ? e => invokeArrayFns(instance.rtg!, e) : void 0 update.ownerInstance = instance } // 調(diào)用一次更新 update() }
unmount
舊節(jié)點(diǎn)的卸載通過unmount
來處理,其中根據(jù)節(jié)點(diǎn)類型不同,又有著不同的函數(shù)來實(shí)施卸載。
經(jīng)過置空ref
、判斷與處理KeepAlive
、beforeUnmounted
的鉤子函數(shù)和指令、判斷組件的類型并相應(yīng)卸載、處理unmounted
鉤子和指令等過程。
const unmount: UnmountFn = ( vnode, parentComponent, parentSuspense, doRemove = false, optimized = false ) => { const { type, props, ref, children, dynamicChildren, shapeFlag, patchFlag, dirs } = vnode // 置空ref // unset ref if (ref != null) { setRef(ref, null, parentSuspense, vnode, true) } // 組件被緩存,則調(diào)用<KeepAlive>的失活方法 deactivate if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) { ;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode) return } // 是否調(diào)用指令和鉤子 const shouldInvokeDirs = shapeFlag & ShapeFlags.ELEMENT && dirs const shouldInvokeVnodeHook = !isAsyncWrapper(vnode) // beforeUnmounted 鉤子 let vnodeHook: VNodeHook | undefined | null if ( shouldInvokeVnodeHook && (vnodeHook = props && props.onVnodeBeforeUnmount) ) { invokeVNodeHook(vnodeHook, parentComponent, vnode) } if (shapeFlag & ShapeFlags.COMPONENT) { // 卸載組件 unmountComponent(vnode.component!, parentSuspense, doRemove) } else { // 卸載異步組件 if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { vnode.suspense!.unmount(parentSuspense, doRemove) return } // 處理指令的 beforeUnmounted 階段 if (shouldInvokeDirs) { invokeDirectiveHook(vnode, null, parentComponent, 'beforeUnmount') } // 卸載 Teleport if (shapeFlag & ShapeFlags.TELEPORT) { ;(vnode.type as typeof TeleportImpl).remove( vnode, parentComponent, parentSuspense, optimized, internals, doRemove ) } else if ( dynamicChildren && // #1153: fast path should not be taken for non-stable (v-for) fragments (type !== Fragment || (patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT)) ) { // 對(duì)于優(yōu)化過的塊狀節(jié)點(diǎn),僅需移除動(dòng)態(tài)子節(jié)點(diǎn) // fast path for block nodes: only need to unmount dynamic children. unmountChildren( dynamicChildren, parentComponent, parentSuspense, false, true ) } else if ( // 文檔片段 移除其子節(jié)點(diǎn) (type === Fragment && patchFlag & (PatchFlags.KEYED_FRAGMENT | PatchFlags.UNKEYED_FRAGMENT)) || (!optimized && shapeFlag & ShapeFlags.ARRAY_CHILDREN) ) { unmountChildren(children as VNode[], parentComponent, parentSuspense) } // 處理節(jié)點(diǎn)自身 if (doRemove) { remove(vnode) } } // 處理 unmounted 鉤子以及指令中的 unmounted 階段 if ( (shouldInvokeVnodeHook && (vnodeHook = props && props.onVnodeUnmounted)) || shouldInvokeDirs ) { queuePostRenderEffect(() => { vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode) shouldInvokeDirs && invokeDirectiveHook(vnode, null, parentComponent, 'unmounted') }, parentSuspense) } }
remove
使用remove
來移除一個(gè)節(jié)點(diǎn)。根據(jù)節(jié)點(diǎn)類型與環(huán)境,執(zhí)行的邏輯也稍有差別。
const remove: RemoveFn = vnode => { const { type, el, anchor, transition } = vnode if (type === Fragment) { if ( __DEV__ && vnode.patchFlag > 0 && vnode.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT && transition && !transition.persisted ) { // __DEV__環(huán)境 // 遍歷移除子節(jié)點(diǎn) ;(vnode.children as VNode[]).forEach(child => { if (child.type === Comment) { hostRemove(child.el!) } else { remove(child) } }) } else { // 移除片段 removeFragment(el!, anchor!) } return } // 移除靜態(tài)節(jié)點(diǎn) if (type === Static) { removeStaticNode(vnode) return } /** 遍歷移除靜態(tài)節(jié)點(diǎn) * const removeStaticNode = ({ el, anchor }: VNode) => { * let next * while (el && el !== anchor) { * next = hostNextSibling(el) * hostRemove(el) * el = next * } * hostRemove(anchor!) * } */ const performRemove = () => { // 移除el hostRemove(el!) if (transition && !transition.persisted && transition.afterLeave) { // 動(dòng)畫的 afterLeave 鉤子 transition.afterLeave() } } if ( vnode.shapeFlag & ShapeFlags.ELEMENT && transition && !transition.persisted ) { const { leave, delayLeave } = transition const performLeave = () => leave(el!, performRemove) // 推遲 leave 動(dòng)畫 if (delayLeave) { delayLeave(vnode.el!, performRemove, performLeave) } else { performLeave() } } else { // 執(zhí)行 performRemove() } }
removeFragment
直接遍歷移除所有包含的節(jié)點(diǎn),這一點(diǎn)與移除靜態(tài)節(jié)點(diǎn)十分相似。
const removeFragment = (cur: RendererNode, end: RendererNode) => { // For fragments, directly remove all contained DOM nodes. // (fragment child nodes cannot have transition) let next while (cur !== end) { next = hostNextSibling(cur)! hostRemove(cur) cur = next } hostRemove(end) }
unmountComponent
對(duì)于組件的卸載,步驟稍微多一點(diǎn)。畢竟除了要遍歷卸載子組件樹,要處理組件的鉤子函數(shù),甚至考慮異步組件。
const unmountComponent = ( instance: ComponentInternalInstance, parentSuspense: SuspenseBoundary | null, doRemove?: boolean ) => { if (__DEV__ && instance.type.__hmrId) { unregisterHMR(instance) } const { bum, scope, update, subTree, um } = instance // 調(diào)用 beforeUnmounted 鉤子 // beforeUnmount hook if (bum) { invokeArrayFns(bum) } if ( __COMPAT__ && isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) ) { instance.emit('hook:beforeDestroy') } // 停止副作用 // stop effects in component scope scope.stop() // 關(guān)閉 update,卸載子組件樹 // update may be null if a component is unmounted before its async // setup has resolved. if (update) { // so that scheduler will no longer invoke it update.active = false unmount(subTree, instance, parentSuspense, doRemove) } // 調(diào)用unmounted鉤子 // unmounted hook if (um) { queuePostRenderEffect(um, parentSuspense) } // 向后兼容:destroyed 鉤子 if ( __COMPAT__ && isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) ) { queuePostRenderEffect( () => instance.emit('hook:destroyed'), parentSuspense ) } // 更改狀態(tài)為已卸載 queuePostRenderEffect(() => { instance.isUnmounted = true }, parentSuspense) // 處理<Suspense> // A component with async dep inside a pending suspense is unmounted before // its async dep resolves. This should remove the dep from the suspense, and // cause the suspense to resolve immediately if that was the last dep. if ( __FEATURE_SUSPENSE__ && parentSuspense && parentSuspense.pendingBranch && !parentSuspense.isUnmounted && instance.asyncDep && !instance.asyncResolved && instance.suspenseId === parentSuspense.pendingId ) { parentSuspense.deps-- if (parentSuspense.deps === 0) { parentSuspense.resolve() } } if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { devtoolsComponentRemoved(instance) } }
unmountChildren
卸載子節(jié)點(diǎn),遍歷遞歸unmount
方法進(jìn)行卸載。
const unmountChildren: UnmountChildrenFn = ( children, parentComponent, parentSuspense, doRemove = false, optimized = false, start = 0 ) => { for (let i = start; i < children.length; i++) { unmount(children[i], parentComponent, parentSuspense, doRemove, optimized) } }
小結(jié)
render
只是個(gè)引子,絕大部分功能如節(jié)點(diǎn)掛載、節(jié)點(diǎn)更新都被patch
涵蓋了。diff
算法在同層級(jí)進(jìn)行遍歷比較,核心內(nèi)容都在patchKeyedChildren
中,首尾節(jié)點(diǎn)各自循環(huán)一輪,對(duì)于中間的節(jié)點(diǎn),則利用Map
來映射key
和節(jié)點(diǎn)在新子節(jié)點(diǎn)組中的index
,再遍歷剩余的舊子節(jié)點(diǎn)組,在Map
中找相同的key
里確定這個(gè)舊節(jié)點(diǎn)是否可復(fù)用。沒有key
的情況則使用patchUnkeyedChildren
進(jìn)行diff
,簡(jiǎn)單粗暴。
以上就是Vue3源碼通過render patch 了解diff的詳細(xì)內(nèi)容,更多關(guān)于Vue3 render patch了解diff的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue進(jìn)入頁面時(shí)不在頂部,檢測(cè)滾動(dòng)返回頂部按鈕問題及解決方法
這篇文章主要介紹了vue進(jìn)入頁面時(shí)不在頂部,檢測(cè)滾動(dòng)返回頂部按鈕問題及解決方法,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-10-10vue+axios全局添加請(qǐng)求頭和參數(shù)操作
這篇文章主要介紹了vue+axios全局添加請(qǐng)求頭和參數(shù)操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-07-07Vue中使用Element的Table組件實(shí)現(xiàn)嵌套表格
本文主要介紹了Vue中使用Element的Table組件實(shí)現(xiàn)嵌套表格,通過type="expand"設(shè)置了一個(gè)展開按鈕,點(diǎn)擊該按鈕會(huì)顯示與當(dāng)前行關(guān)聯(lián)的子表格內(nèi)容,感興趣的可以了解一下2024-01-01vue使用html2PDF實(shí)現(xiàn)將內(nèi)容導(dǎo)出為PDF
將 HTML 轉(zhuǎn)換為 PDF 進(jìn)行下載是一個(gè)比較常見的功能,這篇文章將通過html2PDF實(shí)現(xiàn)將頁面內(nèi)容導(dǎo)出為PDF,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-11-11詳解@Vue/Cli 3 Invalid Host header 錯(cuò)誤解決辦法
這篇文章主要介紹了詳解@Vue/Cli 3 Invalid Host header 錯(cuò)誤解決辦法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-01-01element帶輸入建議el-autocomplete的使用
本文主要介紹了element帶輸入建議el-autocomplete的使用,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03element-ui中select下拉框加載大數(shù)據(jù)渲染優(yōu)化方式
這篇文章主要介紹了element-ui中select下拉框加載大數(shù)據(jù)渲染優(yōu)化方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11