簡單聊一聊Vue3組件更新過程
前言
組件渲染的過程,本質上就是把各種把各種類型的 vnode 渲染成真實 DOM。我們也知道了組件是由模板、組件描述對象和數(shù)據(jù)構成的,數(shù)據(jù)的變化會影響組件的變化。組件的渲染過程中創(chuàng)建了一個帶副作用的渲染函數(shù),當數(shù)據(jù)變化的時候就會執(zhí)行這個渲染函數(shù)來觸發(fā)組件的更新。本文我們就具體分析一下組件的更新過程。
副作用渲染函數(shù)更新組件的過程
我們先來回顧一下帶副作用渲染函數(shù) setupRenderEffect 的實現(xiàn),但是這次我們要重點關注更新組件部分的邏輯:
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) = >{
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {} else {
let {
next,
vnode
} = instance
if (next) {
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
const nextTree = renderComponentRoot(instance) const prevTree = instance.subTree
instance.subTree = nextTree
patch(prevTree, nextTree, hostParentNode(prevTree.el), getNextHostNode(prevTree), instance, parentSuspense, isSVG)
next.el = nextTree.el
}
},
prodEffectOptions)
}可以看到,更新組件主要做三件事情:更新組件 vnode 節(jié)點、渲染新的子樹 vnode、根據(jù)新舊子樹vnode 執(zhí)行 patch 邏輯。
首先是更新組件 vnode 節(jié)點,這里會有一個條件判斷,判斷組件實例中是否有新的組件 vnode(用next 表示),有則更新組件 vnode,沒有 next 指向之前的組件 vnode。為什么需要判斷,這其實涉及一個組件更新策略的邏輯,我們稍后會講。
接著是渲染新的子樹 vnode,因為數(shù)據(jù)發(fā)生了變化,模板又和數(shù)據(jù)相關,所以渲染生成的子樹 vnode也會發(fā)生相應的變化。
最后就是核心的 patch 邏輯,用來找出新舊子樹 vnode 的不同,并找到一種合適的方式更新 DOM,接下來我們就來分析這個過程。
核心邏輯:patch流程
我們先來看 patch 流程的實現(xiàn)代碼:
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) = >{
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense, true) n1 = null
}
const {
type,
shapeFlag
} = n2
switch (type) {
case Text:
break
case Comment:
break
case Static:
break
case Fragment:
break
default:
if (shapeFlag & 1) {
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
} else if (shapeFlag & 6) {
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
} else if (shapeFlag & 64) {} else if (shapeFlag & 128) {}
}
}
function isSameVNodeType(n1, n2) {
return n1.type === n2.type && n1.key === n2.key
}在這個過程中,首先判斷新舊節(jié)點是否是相同的 vnode 類型,如果不同,比如一個 div 更新成一個ul,那么最簡單的操作就是刪除舊的 div 節(jié)點,再去掛載新的 ul 節(jié)點。
如果是相同的 vnode 類型,就需要走 diff 更新流程了,接著會根據(jù)不同的 vnode 類型執(zhí)行不同的處理邏輯,這里我們仍然只分析普通元素類型和組件類型的處理過程。
1.處理組件
如何處理組件的呢?舉個例子,我們在父組件 App 中里引入了 Hello 組件:
<template>
<div>
<p>This is an app.</p>
<hello :msg="msg"></hello>
<button @click="toggle">Toggle msg</button>
</div>
</template>
<script>
export default {
data () {
return { msg: 'Vue' }
},
methods: {
toggle () {
this.msg = this.msg === 'Vue' ? 'World' : 'Vue'
}
}
}
</script>Hello 組件中是 <div>包裹著一個 <p> 標簽, 如下所示:
<template>
<div>
<p>Hello, {{ msg }}</p>
</div>
</template>
<script>
export default {
props: { msg: String }
}
</script>點擊 App 組件中的按鈕執(zhí)行 toggle 函數(shù),就會修改 data 中的 msg,并且會觸發(fā) App 組件的重新渲染。
結合前面對渲染函數(shù)的流程分析,這里 App 組件的根節(jié)點是 div 標簽,重新渲染的子樹 vnode 節(jié)點是一個普通元素的 vnode,應該先走 processElement 邏輯。組件的更新最終還是要轉換成內部真實DOM 的更新,而實際上普通元素的處理流程才是真正做 DOM 的更新,由于稍后我們會詳細分析普通元素的處理流程,所以我們先跳過這里,繼續(xù)往下看。
和渲染過程類似,更新過程也是一個樹的深度優(yōu)先遍歷過程,更新完當前節(jié)點后,就會遍歷更新它的子節(jié)點,因此在遍歷的過程中會遇到 hello 這個組件 vnode 節(jié)點,就會執(zhí)行到 processComponent 處理邏輯中,我們再來看一下它的實現(xiàn),我們重點關注一下組件更新的相關邏輯:
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) = >{
if (n1 == null) {} else {
updateComponent(n1, n2, parentComponent, optimized)
}
}
const updateComponent = (n1, n2, parentComponent, optimized) = >{
const instance = (n2.component = n1.component) if (shouldUpdateComponent(n1, n2, parentComponent, optimized)) {
instance.next = n2 invalidateJob(instance.update) instance.update()
} else {
n2.component = n1.component n2.el = n1.el
}
}可以看到,processComponent 主要通過執(zhí)行 updateComponent 函數(shù)來更新子組件,updateComponent 函數(shù)在更新子組件的時候,會先執(zhí)行 shouldUpdateComponent 函數(shù),根據(jù)新舊子組件 vnode 來判斷是否需要更新子組件。這里你只需要知道,在 shouldUpdateComponent 函數(shù)的內部,主要是通過檢測和對比組件 vnode 中的 props、chidren、dirs、transiton 等屬性,來決定子組件是否需要更新。
這是很好理解的,因為在一個組件的子組件是否需要更新,我們主要依據(jù)子組件 vnode 是否存在一些會影響組件更新的屬性變化進行判斷,如果存在就會更新子組件。
雖然 Vue.js 的更新粒度是組件級別的,組件的數(shù)據(jù)變化只會影響當前組件的更新,但是在組件更新的過程中,也會對子組件做一定的檢查,判斷子組件是否也要更新,并通過某種機制避免子組件重復更新。
我們接著看 updateComponent 函數(shù),如果 shouldUpdateComponent 返回 true ,那么在它的最后,先執(zhí)行 invalidateJob(instance.update)避免子組件由于自身數(shù)據(jù)變化導致的重復更新,然后又執(zhí)行了子組件的副作用渲染函數(shù) instance.update 來主動觸發(fā)子組件的更新。
再回到副作用渲染函數(shù)中,有了前面的講解,我們再看組件更新的這部分代碼,就能很好地理解它的邏輯了:
let {
next,
vnode
} = instance
if (next) {
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
const updateComponentPreRender = (instance, nextVNode, optimized) = >{
nextVNode.component = instance
const prevProps = instance.vnode.props
instance.vnode = nextVNode
instance.next = null
updateProps(instance, nextVNode.props, prevProps, optimized) updateSlots(instance, nextVNode.children)
}結合上面的代碼,我們在更新組件的 DOM 前,需要先更新組件 vnode 節(jié)點信息,包括更改組件實例的 vnode 指針、更新 props 和更新插槽等一系列操作,因為組件在稍后執(zhí)行 renderComponentRoot時會重新渲染新的子樹 vnode ,它依賴了更新后的組件 vnode 中的 props 和 slots 等數(shù)據(jù)。
所以我們現(xiàn)在知道了一個組件重新渲染可能會有兩種場景,一種是組件本身的數(shù)據(jù)變化,這種情況下next 是 null;另一種是父組件在更新的過程中,遇到子組件節(jié)點,先判斷子組件是否需要更新,如果需要則主動執(zhí)行子組件的重新渲染方法,這種情況下 next 就是新的子組件 vnode。
你可能還會有疑問,這個子組件對應的新的組件 vnode 是什么時候創(chuàng)建的呢?答案很簡單,它是在父組件重新渲染的過程中,通過 renderComponentRoot 渲染子樹 vnode 的時候生成,因為子樹 vnode是個樹形結構,通過遍歷它的子節(jié)點就可以訪問到其對應的組件 vnode。再拿我們前面舉的例子說,當App 組件重新渲染的時候,在執(zhí)行 renderComponentRoot 生成子樹 vnode 的過程中,也生成了hello 組件對應的新的組件 vnode。
所以 processComponent 處理組件 vnode,本質上就是去判斷子組件是否需要更新,如果需要則遞歸執(zhí)行子組件的副作用渲染函數(shù)來更新,否則僅僅更新一些 vnode 的屬性,并讓子組件實例保留對組件vnode 的引用,用于子組件自身數(shù)據(jù)變化引起組件重新渲染的時候,在渲染函數(shù)內部可以拿到新的組件vnode。
前面也說過,組件是抽象的,組件的更新最終還是會落到對普通 DOM 元素的更新。所以接下來我們詳細分析一下組件更新中對普通元素的處理流程。
2.處理普通元素
我們再來看如何處理普通元素,我把之前的示例稍加修改,將其中的 Hello 組件刪掉,如下所示:
<template>
<div>
<p>This is {{msg}}</p>
<button @click="toggle">Toggle msg</button>
</div>
</template>
<script>
export default {
data () {
return { msg: 'Vue' }
},
methods: {
toggle () {
this.msg === 'Vue' ? 'World' : 'Vue'
}
}
}
</script>當我們點擊 App 組件中的按鈕會執(zhí)行 toggle 函數(shù),然后修改 data 中的 msg,這就觸發(fā)了 App 組件的重新渲染。
App 組件的根節(jié)點是 div 標簽,重新渲染的子樹 vnode 節(jié)點是一個普通元素的 vnode,所以應該先走processElement 邏輯,我們來看這個函數(shù)的實現(xiàn):
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) = >{
isSVG = isSVG || n2.type === 'svg'
if (n1 == null) {} else {
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
}
}
const patchElement = (n1, n2, parentComponent, parentSuspense, isSVG, optimized) = >{
const el = (n2.el = n1.el) const oldProps = (n1 && n1.props) || EMPTY_OBJ const newProps = n2.props || EMPTY_OBJ
patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG) const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
patchChildren(n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG)
}可以看到,更新元素的過程主要做兩件事情:更新 props 和更新子節(jié)點。其實這是很好理解的,因為一個 DOM 節(jié)點元素就是由它自身的一些屬性和子節(jié)點構成的。
首先是更新 props,這里的 patchProps 函數(shù)就是在更新 DOM 節(jié)點的 class、style、event 以及其它的一些 DOM 屬性。
其次是更新子節(jié)點,我們來看一下這里的 patchChildren 函數(shù)的實現(xiàn):
const patchChildren = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized = false) = >{
const c1 = n1 && n1.children const prevShapeFlag = n1 ? n1.shapeFlag: 0 const c2 = n2.children const {
shapeFlag
} = n2
if (shapeFlag & 8) {
if (prevShapeFlag & 16) {
unmountChildren(c1, parentComponent, parentSuspense)
}
if (c2 !== c1) {
hostSetElementText(container, c2)
}
} else {
if (prevShapeFlag & 16) {
if (shapeFlag & 16) {
patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
} else {
unmountChildren(c1, parentComponent, parentSuspense, true)
}
} else {
if (prevShapeFlag & 8) {
hostSetElementText(container, '')
}
if (shapeFlag & 16) {
mountChildren(c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
}
}
}對于一個元素的子節(jié)點 vnode 可能會有三種情況:純文本、vnode 數(shù)組和空。那么根據(jù)排列組合對于新舊子節(jié)點來說就有九種情況,我們可以通過三張圖來表示。
首先來看一下舊子節(jié)點是純文本的情況:
- 如果新子節(jié)點也是純文本,那么做簡單地文本替換即可;
- 如果新子節(jié)點是空,那么刪除舊子節(jié)點即可;
- 如果新子節(jié)點是 vnode 數(shù)組,那么先把舊子節(jié)點的文本清空,再去舊子節(jié)點的父容器下添加多個新子節(jié)點。

接下來看一下舊子節(jié)點是空的情況:
- 如果新子節(jié)點是純文本,那么在舊子節(jié)點的父容器下添加新文本節(jié)點即可;
- 如果新子節(jié)點也是空,那么什么都不需要做;
- 如果新子節(jié)點是 vnode 數(shù)組,那么直接去舊子節(jié)點的父容器下添加多個新子節(jié)點即可。

最后來看一下舊子節(jié)點是 vnode 數(shù)組的情況:
- 如果新子節(jié)點是純文本,那么先刪除舊子節(jié)點,再去舊子節(jié)點的父容器下添加新文本節(jié)點;
- 如果新子節(jié)點是空,那么刪除舊子節(jié)點即可;
- 如果新子節(jié)點也是 vnode 數(shù)組,那么就需要做完整的 diff 新舊子節(jié)點了,這是最復雜的情況,內部運用了核心 diff 算法。

總結
到此這篇關于Vue3組件更新過程的文章就介紹到這了,更多相關Vue3組件更新過程內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
一文詳細了解Vue?3.0中的onMounted和onUnmounted鉤子函數(shù)
Vue3.0引入了新的組件生命周期鉤子函數(shù)onMounted和onUnmounted,分別用于組件掛載后和卸載前的操作,這些鉤子函數(shù)為開發(fā)者提供了更多靈活性,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2024-10-10
element實現(xiàn)二級菜單和頂部導航聯(lián)動的示例
本文主要介紹了element實現(xiàn)二級菜單和頂部導航聯(lián)動的示例,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02
vue實現(xiàn)后臺管理權限系統(tǒng)及頂欄三級菜單顯示功能
這篇文章主要介紹了vue實現(xiàn)后臺管理權限系統(tǒng)及頂欄三級菜單顯示功能,本文圖文并茂給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-06-06

