Vue3?如何通過(guò)虛擬DOM更新頁(yè)面詳解
引言
上一講我們主要介紹了 Vue 項(xiàng)目的首次渲染流程,在 mountComponent 中注冊(cè)了effect 函數(shù),這樣,在組件數(shù)據(jù)有更新的時(shí)候,就會(huì)通知到組件的 update 方法進(jìn)行更新
Vue 中組件更新的方式也是使用了響應(yīng)式 + 虛擬 DOM 的方式,這個(gè)我們?cè)诘谝恢v中有介紹過(guò) Vue 1、Vue 2 和 Vue 3 中更新方式的變化,今天我們就來(lái)詳細(xì)剖析一下 Vue 組件內(nèi)部如何通過(guò)虛擬 DOM 更新頁(yè)面的代碼細(xì)節(jié)
Vue 虛擬 DOM 執(zhí)行流程
我們從虛擬 DOM 在 Vue 的執(zhí)行流程開(kāi)始講起。在 Vue 中,我們使用虛擬 DOM 來(lái)描述頁(yè)面的組件,比如下面的 template 雖然格式和 HTML 很像,但是在 Vue 的內(nèi)部會(huì)解析成 JavaScript 函數(shù),這個(gè)函數(shù)就是用來(lái)返回虛擬 DOM:
<div id="app">
<p>hello world</p>
<Rate :value="4"></Rate>
</div>
上面的 template 會(huì)解析成下面的函數(shù),最終返回一個(gè) JavaScript 的對(duì)象能夠描述這段HTML:
function render(){
return h('div',{id:"app"},children:[
h('p',{},'hello world'),
h(Rate,{value:4}),
])
}
知道虛擬 DOM 是什么之后,那么它是怎么創(chuàng)建的呢?
DOM 的創(chuàng)建
我們簡(jiǎn)單回憶上一講介紹的 mount 函數(shù),在代碼中,我們使用 createVNode 函數(shù)創(chuàng)建項(xiàng)目的虛擬 DOM,可以看到 Vue 內(nèi)部的虛擬 DOM,也就是 vnode,就是一個(gè)對(duì)象,通過(guò) type、props、children 等屬性描述整個(gè)節(jié)點(diǎn):
const vnode = createVNode( (
rootComponent as ConcreteComponent,
rootProps
)
function _createVNode() {
// 處理屬性和 class
if (props) {
...
}
// 標(biāo)記vnode信息
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
: __FEATURE_SUSPENSE__ && isSuspense(type)
? ShapeFlags.SUSPENSE
: isTeleport(type)
? ShapeFlags.TELEPORT
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT
: 0
return createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
isBlockNode,
true
)
}
function createBaseVNode(type,props,children,...){
const vnode = {
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
children,
shapeFlag,
patchFlag,
dynamicProps,
...
} as VNode
// 標(biāo)準(zhǔn)化子節(jié)點(diǎn)
if (needFullChildrenNormalization) {
normalizeChildren(vnode, children)
} else if (children) {
vnode.shapeFlag |= isString(children)
? ShapeFlags.TEXT_CHILDREN
: ShapeFlags.ARRAY_CHILDREN
}
return vnode
}componentUpdateFn
createVNode 負(fù)責(zé)創(chuàng)建 Vue 中的虛擬 DOM,而上一講中我們講過(guò) mount 函數(shù)的核心邏輯就是使用 setupComponent 執(zhí)行我們寫的 <script setup>,使用 setupRenderEffect 監(jiān)聽(tīng)組件的數(shù)據(jù)變化;所以我們來(lái)到 setupRenderEffect 函數(shù)中,去完整地剖析 Vue 中虛擬 DOM 的更新邏輯
我們給組件注冊(cè)了 update 方法,這個(gè)方法使用 effect 包裹后,當(dāng)組件內(nèi)的 ref、reactive 包裹的響應(yīng)式數(shù)據(jù)變化的時(shí)候就會(huì)執(zhí)行 update 方法,觸發(fā)組件內(nèi)部的更新機(jī)制
看下面的代碼,在 setupRenderEffect 內(nèi)部的 componentUpdateFn 中,updateComponentPreRenderer 更新了屬性和 slots,并且調(diào)用 renderComponentRoot 函數(shù)創(chuàng)建新的子樹(shù)對(duì)象 nextTree,然后內(nèi)部依然是調(diào)用 patch 函數(shù)
可以看到,Vue 源碼中的實(shí)現(xiàn)首次渲染和更新的邏輯都寫在一起,我們?cè)谶f歸的時(shí)候如果對(duì)一個(gè)標(biāo)簽實(shí)現(xiàn)更新和渲染,就可以用一個(gè)函數(shù)實(shí)現(xiàn)
const componentUpdateFn = ()=>{
if (!instance.isMounted) {
//首次渲染
instance,
parentSuspense,
isSVG
)
。。。
}else{
let { next, bu, u, parent, vnode } = instance
if (next) {
next.el = vnode.el
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
const nextTree = renderComponentRoot(instance)
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
)
}
}
// 注冊(cè)effect函數(shù)
const effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(instance.update),
instance.scope // track it in component's effect scope
)
const update = (instance.update = effect.run.bind(effect) as S chedulerJo
update()
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()
}
比較關(guān)鍵的就是上面代碼中 32-39 行的 effect 函數(shù),負(fù)責(zé)注冊(cè)組件,這個(gè)函數(shù)也是 Vue 組件更新的入口函數(shù)
patch 函數(shù)
數(shù)據(jù)更新之后就會(huì)執(zhí)行 patch 函數(shù),下圖就是 patch 函數(shù)執(zhí)行的邏輯圖:

在 patch 函數(shù)中,會(huì)針對(duì)不同的組件類型執(zhí)行不同的函數(shù),組件我們會(huì)執(zhí)行 processComponent,HTML 標(biāo)簽我們會(huì)執(zhí)行 processElement:
function path(n1, n2, container){
const { type, shapeFlag } = n2
switch (type) {
case Text:
processText(n1, n2, container)
break
// 還有注釋,fragment之類的可以處理,這里忽略
default:
// 通過(guò)shapeFlag判斷類型
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, anchor)
} else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
processComponent(n1, n2, container)
}
}
}
function processComponent(n1, n2, container) {
// 老規(guī)矩,沒(méi)有n1就是mount
if (!n1) {
// 初始化 component
mountComponent(n2, container)
} else {
updateComponent(n1, n2, container)
}
}
由于更新之后不是首次渲染了,patch 函數(shù)內(nèi)部會(huì)執(zhí)行 updateComponent,看下面的 updateComponent 函數(shù)內(nèi)部,shouldUpdateComponent 會(huì)判斷組件是否需要更新,實(shí)際執(zhí)行的是 instance.update:
const instance = (n2.component = n1.component)!
if (shouldUpdateComponent(n1, n2, optimized)) {
// 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.component = n1.component
n2.el = n1.el
instance.vnode = n2
}
組件的子元素是由 HTML 標(biāo)簽和組件構(gòu)成,組件內(nèi)部的遞歸處理最終也是對(duì) HTML 標(biāo)簽的處理,所以,最后組件的更新都會(huì)進(jìn)入到 processElement 內(nèi)部的 patchElement 函數(shù)中
patchElement 函數(shù)
在函數(shù) patchElement 中我們主要就做兩件事,更新節(jié)點(diǎn)自己的屬性和更新子元素
節(jié)點(diǎn)自身屬性的更新
先看自身屬性的更新,這里就能體現(xiàn)出 Vue 3 中性能優(yōu)化的思想,通過(guò) patchFlag 可以做到按需更新:
如果標(biāo)記了 FULL_PROPS,就直接調(diào)用 patchProps;如果標(biāo)記了 CLASS,說(shuō)明節(jié)點(diǎn)只有 class 屬性是動(dòng)態(tài)的,其他的 style 等屬性都不需要進(jìn)行判斷和 DOM 操作
這樣就極大的優(yōu)化了屬性操作的性能
內(nèi)部執(zhí)行 hostPatchProp 進(jìn)行實(shí)際的 DOM 操作,你還記得上一講中 hostPatchProp 是從 nodeOps 中定義的嗎,其他動(dòng)態(tài)屬性 STYLE、TEXT 等等也都是一樣的邏輯;Vue 3 的虛擬 DOM 真正做到了按需更新,這也是相比于 React 的一個(gè)優(yōu)勢(shì)
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
patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS
const oldProps = n1.props || EMPTY_OBJ
const newProps = n2.props || EMPTY_OBJ
// full diff
patchChildren(
n1,
n2,
el,
null,
parentComponent,
parentSuspense,
areChildrenSVG,
slotScopeIds,
false
)
if (patchFlag > 0) {
if (patchFlag & PatchFlags.FULL_PROPS) {
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
} else {
// class是動(dòng)態(tài)的
if (patchFlag & PatchFlags.CLASS) {
if (oldProps.class !== newProps.class) {
hostPatchProp(el, 'class', null, newProps.class, isSVG)
}
}
// style樣式是動(dòng)態(tài)的
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
}
// 屬性需要diff
if (patchFlag & PatchFlags.PROPS) {
//
const propsToUpdate = n2.dynamicProps!
for (let i = 0; i < propsToUpdate.length; i++) {
const key = propsToUpdate[i]
const prev = oldProps[key]
const next = newProps[key]
// #1471 force patch value
if (next !== prev || key === 'value') {
hostPatchProp(
el,
key,
prev,
next,
isSVG,
n1.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
}
}
//文本是動(dòng)態(tài)的
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children as string)
}
}
}
}
子元素的更新
而子元素的更新是 patchChildren 函數(shù)負(fù)責(zé)的,這個(gè)函數(shù)也是虛擬 DOM 中難度最高的一個(gè)函數(shù),搞懂它還需要我們下一講中介紹的算法知識(shí),今天我們就先理解它主要的實(shí)現(xiàn)思路
首先我們把子元素分成了文本、數(shù)組和空三個(gè)狀態(tài),新老子元素分別是這三種狀態(tài)的一個(gè),構(gòu)成了不同的執(zhí)行邏輯;這樣 patchChildren 內(nèi)部大致有五種情況需要處理:
- 如果新的子元素是空, 老的子元素不為空,直接卸載 unmount 即可
- 如果新的子元素不為空,老的子元素是空,直接創(chuàng)建加載即可
- 如果新的子元素是文本,老的子元素如果是數(shù)組就需要全部 unmount,是文本的話就需要執(zhí)行 hostSetElementText
- 如果新的子元素是數(shù)組,比如是使用 v-for 渲染出來(lái)的列表,老的子元素如果是空或者文本,直接 unmout 后,渲染新的數(shù)組即可
最復(fù)雜的情況就是新的子元素和老的子元素都是數(shù)組
最樸實(shí)無(wú)華的思路就是把老的子元素全部 unmount,新的子元素全部 mount,這樣雖然可以實(shí)現(xiàn)功能,但是沒(méi)法復(fù)用已經(jīng)存在的 DOM 元素,比如我們只是在數(shù)組中間新增了一個(gè)數(shù)據(jù),全部 DOM 都銷毀就有點(diǎn)太可惜了
所以,我們需要判斷出可以復(fù)用的 DOM 元素,如果一個(gè)虛擬 DOM 沒(méi)有改動(dòng)或者屬性變了,不需要完全銷毀重建,而是更新一下屬性,最大化減少 DOM 的操作,這個(gè)任務(wù)就會(huì)交給 patchKeyedChildren 函數(shù)去完成
patchKeyedChildren 函數(shù),做的事情就是盡可能高效地把老的子元素更新成新的子元素,如何高效復(fù)用老的子元素中的 DOM 元素是 patchKeyedChildren 函數(shù)的難點(diǎn):
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
// fast path
if (patchFlag > 0) {
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
// 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) {
// unkeyed
patchUnkeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
}
}
// children has 3 possibilities: text, array or no children.
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// text children fast path
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
}
if (c2 !== c1) {
hostSetElementText(container, c2 as string)
}
} else {
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// prev children was array
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// two arrays, cannot assume anything, do full diff
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// no new children, just unmount old
unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true
}
} else {
// prev children was text OR null
// new children is array OR null
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(container, '')
}
// mount new if array
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
}
}
上面的代碼執(zhí)行邏輯如下圖所示,根據(jù) flags 判斷子元素的類型后,執(zhí)行不同的操作函數(shù):

patchChildren
最后就剩下 patchChildren 的實(shí)現(xiàn)了,這也是各類虛擬 DOM 框架中最難實(shí)現(xiàn)的函數(shù),我們需要實(shí)現(xiàn)一個(gè)高效的更新算法,能夠使用盡可能少的更新次數(shù),來(lái)實(shí)現(xiàn)從老的子元素到新的子元素的更新;
舉個(gè)例子,類似體育課站隊(duì)的時(shí)候,大家一開(kāi)始站一排,但是順序是亂的,我們需要盡快把隊(duì)伍按照個(gè)頭左低右高排列
在 React 中,這種場(chǎng)景的處理邏輯是先進(jìn)行循環(huán),使用的是單側(cè)插入的算法,我們?cè)谂抨?duì)的時(shí)候挨個(gè)對(duì)比,如果你站我右邊,并且個(gè)頭比我高一點(diǎn),說(shuō)明咱倆的相對(duì)位置和最終隊(duì)伍的位置是一致的,暫時(shí)不需要變化,如果你比我個(gè)頭矮,就需要去我左邊找到一個(gè)正確的位置插隊(duì)進(jìn)去
由于都只向單側(cè)插入,最后我們就會(huì)把所有的節(jié)點(diǎn)移動(dòng)到正確的位置之上,這就是 React15 框架內(nèi)虛擬節(jié)點(diǎn) diff 的邏輯,初步實(shí)現(xiàn)了 DOM 的復(fù)用;而 Vue 2 借鑒了 snabbdom 的算法,在此基礎(chǔ)上做了第一層雙端對(duì)比的優(yōu)化
首先 Web 場(chǎng)景之下對(duì)一個(gè)數(shù)組元素的操作,很少有直接全部替換的,比如我們操作一個(gè)表格,大概率是更關(guān)心表格某一行的一個(gè)字段、新增一行、刪除一行,或者是對(duì)表格某個(gè)字段進(jìn)行排序,所以我們可以從純算法的場(chǎng)景之中加入實(shí)際應(yīng)用的場(chǎng)景
如果我們只是在表格里新增一行,那么可以不要一開(kāi)始就開(kāi)始循環(huán),而是可以先進(jìn)行節(jié)點(diǎn)的預(yù)判
比如,在下面的例子中,新的節(jié)點(diǎn)就是在老的節(jié)點(diǎn)中新增和刪除了幾個(gè)元素,我們?cè)谘h(huán)之前,先進(jìn)行頭部元素的判斷;在這個(gè)例子里,可以預(yù)判出頭部元素的 a、b、c、d 是一樣的節(jié)點(diǎn),說(shuō)明節(jié)點(diǎn)不需要重新創(chuàng)建,我們只需要進(jìn)行屬性的更新,然后進(jìn)行隊(duì)尾元素的預(yù)判,可以判斷出 g 和元素也是一樣的:
a b c d e f g h
a b c d i f j g h
這樣我們虛擬 DOM diff 的邏輯就變成了下面的結(jié)構(gòu), 現(xiàn)在只需要比較 ef 和 ifg 的區(qū)別:
(a b c d) e f (g h)
(a b c) d) i f j (g h)
相比于之前的對(duì)比場(chǎng)景,我們需要遍歷的運(yùn)算量就大大減小了
而且,有很多場(chǎng)景比如新增一行或者刪除一行的簡(jiǎn)單場(chǎng)景,預(yù)判完畢之后,新老元素有一個(gè)處于沒(méi)有元素的狀態(tài),我們就可以直接執(zhí)行 mount 或者 unmout 完成對(duì)比的全過(guò)程,不需要再進(jìn)行復(fù)雜的遍歷:
(a b c d)
(a b c d) e
(a b c) d
(a b c
雙端對(duì)比的原理大致就是這樣;最后雙端對(duì)比之后的執(zhí)行邏輯這一部分需要一些算法知識(shí),下面會(huì)我詳細(xì)介紹,這里你只需要掌握大概的思路
想讓一個(gè)隊(duì)伍盡快按照個(gè)頭排好序,如果能夠計(jì)算出,在隊(duì)伍中,個(gè)頭從低到高依次遞增的最多的隊(duì)列,讓這些人站在原地不動(dòng),其余人穿插到他們中間,就可以最大化減少人員的移動(dòng),這就是一個(gè)最長(zhǎng)底層子序列的算法問(wèn)題
位運(yùn)算
前面也說(shuō)了,在執(zhí)行 diff 之前,要根據(jù)需要判斷每個(gè)虛擬 DOM 節(jié)點(diǎn)有哪些屬性需要計(jì)算,因?yàn)闊o(wú)論響應(yīng)式數(shù)據(jù)怎么變化,靜態(tài)的屬性和節(jié)點(diǎn)都不會(huì)發(fā)生變化
所以我們看每個(gè)節(jié)點(diǎn) diff 的時(shí)候會(huì)做什么,在 renderer.ts 代碼文件中就可以看到代碼,主要就是通過(guò)虛擬 DOM 節(jié)點(diǎn)的 patchFlag 樹(shù)形判斷是否需要更新節(jié)點(diǎn)
方法就是使用 & 操作符來(lái)判斷操作的類型,比如 patchFlag & PatchFlags.CLASS 來(lái)判斷當(dāng)前元素的 class 是否需要計(jì)算 diff;shapeFlag & ShapeFlags.ELEMENT 來(lái)判斷當(dāng)前虛擬 DOM 是 HTML 元素還是 Component 組件;這個(gè)“&”其實(shí)就是位運(yùn)算的按位與
// 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)
}
}
// style
// this flag is matched when the element has dynamic style bindings
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
}
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
)
}
上面的代碼中 & 就是按位與的操作符,這其實(shí)是二進(jìn)制上的計(jì)算符號(hào),所以我們首先要了解一下什么是二進(jìn)制
我們?nèi)粘J褂玫臄?shù)字都是十進(jìn)制數(shù)字,比如數(shù)字 13 就是 110+3 的運(yùn)算結(jié)果,每個(gè)位置都是代表 10 的 n 次方;13 也可以使用二進(jìn)制表達(dá),因?yàn)槎M(jìn)制每個(gè)位置只能是 0 和 1 兩個(gè)數(shù)字,每個(gè)位置代表的是 2 的 n 次方,13 在二進(jìn)制里是 1101,就是 18+14+02+1*1
而在 JavaScript 中我們可以很方便地使用 toString(2) 的方式,把十進(jìn)制數(shù)字轉(zhuǎn)換成二進(jìn)制;運(yùn)算的概念很簡(jiǎn)單,就是在二進(jìn)制上的“與”和“或”運(yùn)算:
(13).toString(2) // 1101 0 & 0 // 0 0 & 1 // 0 1 & 0 // 0 1 & 1 // 1 0 | 0 // 0 0 | 1 // 1 1 | 0 // 1 1 | 1 // 1 1 << 2 // 1左移動(dòng)兩位,就是100 就是1*2平方 = 4
二進(jìn)制中,我們每個(gè)位置只能是 0 或者 1 這兩個(gè)值,& 和 | 的概念和 JavaScript 中的 && 和 || 保持一致;兩個(gè)二進(jìn)制的 & 運(yùn)算就是只有兩個(gè)二進(jìn)制位置都是 1 的時(shí)候,結(jié)果是 1,其余情況運(yùn)算結(jié)果都是 0;| 是按位置進(jìn)行“或”運(yùn)算,只有兩個(gè)二進(jìn)制位置都是 0 的時(shí)候,結(jié)果是 0,其余情況運(yùn)算結(jié)果都是 1;并且,還可以通過(guò)左移 << 和右移 >> 操作符,實(shí)現(xiàn)乘以 2 和除以 2 的效果
由于這些都是在二進(jìn)制上的計(jì)算,運(yùn)算的性能通常會(huì)比字符串和數(shù)字的計(jì)算性能要好,這也是很多框架內(nèi)部使用位運(yùn)算的原因
這么說(shuō)估計(jì)你不是很理解,我們結(jié)合一個(gè) LeetCode 題看看為什么說(shuō)二進(jìn)制的位運(yùn)算性能更好
為什么位運(yùn)算性能更好
我們來(lái)做一下 LeetCode231 題,題目描述很簡(jiǎn)單,判斷數(shù)字 n 是不是 2 的冪次方,也就是說(shuō),判斷數(shù)字 n 是不是 2 的整次方,比如 2、4、8;我們可以很輕松地寫出 JavaScript 的解答,n 一直除以 2,如果有余數(shù)就是 false,否則就是 true:
var isPowerOfTwo = function (n) {
if (n === 1) return true
while (n > 2) {
n = n / 2
if (n % 2 !== 0) return false
}
return n === 2
};
不過(guò)上面的解答我們可以用位運(yùn)算來(lái)優(yōu)化
先來(lái)分析一下 2 的冪次方的特點(diǎn)
2 的冪次方就是數(shù)字 1 左移動(dòng)若干次,其余位置全部都是 0,所以 n-1 就是最高位變成0,其余位置都變成 1,就像十進(jìn)制里的 10000-1 = 9999。這樣,n 和 n-1 每個(gè)二進(jìn)制位的數(shù)字都不一樣,我們可以很輕松地用按位“與”來(lái)判斷這個(gè)題的答案,如果 n&n-1 是 0 的話,數(shù)字 n 就符合 2 的整次冪的特點(diǎn):
16
10000
16-1 = 15
01111
16&15 == 0
var isPowerOfTwo = function(n) {
return n>0 && (n & (n - 1)) === 0
};
所以我們使用位運(yùn)算提高了代碼的整體性能
如何運(yùn)用位運(yùn)算
好,搞清楚為什么用位運(yùn)算,我們回來(lái)看 diff 判斷,如何根據(jù)位運(yùn)算的特點(diǎn),設(shè)計(jì)出權(quán)限的組合認(rèn)證方案
比如 Vue 中的動(dòng)態(tài)屬性,有文本、class、style、props 幾個(gè)屬性,我們可以使用二進(jìn)制中的一個(gè)位置來(lái)表示權(quán)限,看下面的代碼,我們使用左移的方式分別在四個(gè)二進(jìn)制上標(biāo)記了 1,代表四種不同的權(quán)限,使用按位或的方式去實(shí)現(xiàn)權(quán)限授予
比如,一個(gè)節(jié)點(diǎn)如果 TEXT 和 STYLE 都需要修改,我們只需要使用 | 運(yùn)算符就可以得到 flag1 的權(quán)限表示,這就是為什么 Vue 3 中針對(duì)虛擬 DOM 類型以及虛擬 DOM 需要?jiǎng)討B(tài)計(jì)算 diff 的樹(shù)形都做了標(biāo)記,你可以在 Vue 3 的源碼中看到下面的配置:
const PatchFlags = {
TEXT:1, // 0001
CLASS: 1<<1, // 0010
STYLE:1<<2, // 0100
PROPS:1<<3 // 1000
}
const flag1 = PatchFlags.TEXT | PatchFlags.STYLE // 0101
// 權(quán)限校驗(yàn)
flag1 & PatchFlags.TEXT // 有權(quán)限,結(jié)果大于1
flag1 & PatchFlags.CLASS //沒(méi)有權(quán)限 是0
最長(zhǎng)遞增子系列
然后就到了虛擬 DOM 計(jì)算 diff 中的算法了
上面我們?cè)敿?xì)介紹了在虛擬 diff 計(jì)算中,如果新老子元素都是數(shù)組的時(shí)候,需要先做首尾的預(yù)判,如果新的子元素和老的子元素在預(yù)判完畢后,未處理的元素依然是數(shù)組,那么就需要對(duì)兩個(gè)數(shù)組計(jì)算 diff,最終找到最短的操作路徑,能夠讓老的子元素通過(guò)盡可能少的操作,更新成為新的子元素
Vue 3 借鑒了 infero 的算法邏輯,就像操場(chǎng)上需要按照個(gè)頭從低到高站好一樣,我們采用的思路是先尋找一個(gè)現(xiàn)有隊(duì)列中由低到高的隊(duì)列,讓這個(gè)隊(duì)列盡可能的長(zhǎng),它們的相對(duì)位置不需要變化,而其他元素進(jìn)行插入和移動(dòng)位置,這樣就可以做到盡可能少的操作DOM
所以如何尋找這個(gè)最長(zhǎng)遞增的序列呢?這就是今天的重點(diǎn)算法知識(shí)了,我們看 LeetCode 第 300 題,題目描述如下, 需要在數(shù)組中找到最長(zhǎng)底層的自序列長(zhǎng)度:
給你一個(gè)整數(shù)數(shù)組 nums,找到其中最長(zhǎng)嚴(yán)格遞增子序列的長(zhǎng)度
子序列是由數(shù)組派生而來(lái)的序列,刪除(或不刪除)數(shù)組中的元素而不改變其余元素的順序
例如,[3,6,2,7] 是數(shù)組 [0,3,1,6,2,2,7] 的子序列
=
輸入:nums = [10,9,2,5,3,7,101,18]
輸出:4
解釋:最長(zhǎng)遞增子序列是 [2,3,7,101],因此長(zhǎng)度為 4
首先我們可以使用動(dòng)態(tài)規(guī)劃的思路,通過(guò)每一步的遞推,使用 dp 數(shù)組,記錄出每一步操作的最優(yōu)解,最后得到全局最優(yōu)解
在這個(gè)例子中,我們可以把 dp[i] 定義成 nums[0] 到 nums[i] 這個(gè)區(qū)間內(nèi),數(shù)組的最長(zhǎng)遞增子序列的長(zhǎng)度,并且 dp 數(shù)組的初始值設(shè)為 1
從左邊向右遞推,如果 nums[i+1] > nums[i],dp[i+1]就等于 dp[i]+1;如果 nums[i+1] < nums[i],就什么都不需要干,這樣我們?cè)诒闅v的過(guò)程中,就能根據(jù)數(shù)組當(dāng)前位置之前的最長(zhǎng)遞增子序列長(zhǎng)度推導(dǎo)出 i+1 位置的最長(zhǎng)遞增子序列長(zhǎng)度
所以可以得到如下解法:
/**
* @param {number[]} nums
* @return {number}
*/
const lengthOfLIS = function (nums) {
let n = nums.length;
if (n == 0) {
return 0;
}
let dp = new Array(n).fill(1);
for (let i = 0; i < n; i++) {
for (let j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
return Math.max(...dp)
}
由于我們需要兩層循環(huán),所以這個(gè)解法的時(shí)間復(fù)雜度是 n 的平方,這個(gè)解法其實(shí)已經(jīng)不錯(cuò)了,但是還有更優(yōu)秀的解法,也就是 Vue 3 中用到的算法:貪心 + 二分
貪心 + 二分
我們?cè)倏匆幌逻@個(gè)題,貪心的思路就是在尋找最長(zhǎng)遞增的序列,所以,[1,3]要比[1,5]好,也就是說(shuō),在這個(gè)上升的序列中,我們要讓上升速度盡可能變得慢,這樣才有可能讓后面的元素盡可能也遞增
我們可以創(chuàng)建一個(gè) arr 數(shù)組,用來(lái)保存這種策略下的最長(zhǎng)遞增子序列
如果當(dāng)前遍歷的 nums[i] 大于 arr 的最后一個(gè)元素,也就是大于 arr 的最大值時(shí),我們把nums[i] 追加到后面即可,否則我們就在 arr 中尋找一個(gè)第一個(gè)大于 num[i] 的數(shù)字并替換它;因?yàn)槭?arr 是遞增的數(shù)列,所以在尋找插入位置的時(shí)候,我們可以使用二分查找的方式,把整個(gè)算法的復(fù)雜度變成 O(nlgn)
下面的代碼就是貪心 + 二分的解法,我們可以得到正確的最長(zhǎng)遞增子序列的長(zhǎng)度:
/**
* @param {number[]} nums
* @return {number}
*/
const lengthOfLIS = function (nums) {
let len = nums.length
if (len <= 1) {
return len
}
let arr = [nums[0]]
for (let i = 0; i < len; i++) {
// nums[i] 大于 arr 尾元素時(shí),直接追加到后面,遞增序列長(zhǎng)度+1
if (nums[i] > arr[arr.length - 1]) {
arr.push(nums[i])
} else {
// 否則,查找遞增子序列中第一個(gè)大于numsp[i]的元素 替換它
// 遞增序列,可以使用二分查找
let left = 0
let right = arr.length - 1
while (left < right) {
let mid = (left + right) >> 1
if (arr[mid] < nums[i]) {
left = mid + 1
} else {
right = mid
}
}
arr[left] = nums[i]
}
}
return arr.length
};
但是貪心 + 二分的這種解法,現(xiàn)在只能得到最長(zhǎng)遞增子序列的長(zhǎng)度,但是最后得到的 arr 并不一定是最長(zhǎng)遞增子序列,因?yàn)槲覀円苿?dòng)的 num[i] 位置可能會(huì)不正確,只是得到的數(shù)組長(zhǎng)度是正確的,所以我們需要對(duì)這個(gè)算法改造一下,把整個(gè)數(shù)組復(fù)制一份之后,最后也能得到正確的最長(zhǎng)遞增子序列
具體代碼怎么寫呢?我們來(lái)到 Vue 3 的 renderer.ts 文件中,函數(shù) getSquenece 就是用來(lái)生成最長(zhǎng)遞增子序列,看下面的代碼:
// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
function getSequence(arr: number[]): number[] {
const p = arr.slice() //賦值一份arr
const result = [0]
let i, j, u, v, c
const len = arr.length
for (i = 0; i < len; i++) {
const arrI = arr[i]
if (arrI !== 0) {
j = result[result.length - 1]
if (arr[j] < arrI) {
p[i] = j // 存儲(chǔ)在result最后一個(gè)索引的值
result.push(i)
continue
}
u = 0
v = result.length - 1
// 二分查找,查找比arrI小的節(jié)點(diǎn),更新result的值
while (u < v) {
c = (u + v) >> 1
if (arr[result[c]] < arrI) {
u = c + 1
} else {
v = c
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1]
}
result[u] = i
}
}
}
u = result.length
v = result[u - 1]
// 查找數(shù)組p 找到最終的索引
while (u-- > 0) {
result[u] = v
v = p[v]
}
return result
}
這段代碼就是 Vue 3 里的實(shí)現(xiàn),result 存儲(chǔ)的就是長(zhǎng)度是 i 的遞增子序列最小末位置的索引,最后計(jì)算出最長(zhǎng)遞增子序列
我們得到 increasingNewIndexSequence 隊(duì)列后,再去遍歷數(shù)組進(jìn)行 patch 操作就可以實(shí)現(xiàn)完整的 diff 流程了:
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) {
// mount new
patch(
null,
nextChild,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (moved) {
// 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--
}
}
}
上面代碼的思路,我們用下圖演示。做完雙端對(duì)比之后,a 和 g 已經(jīng)計(jì)算出可以直接復(fù)用 DOM,剩下的隊(duì)列中我們需要把 hbfdc 更新成 abdef
首先我們需要使用 keyToNewIndexMap 存儲(chǔ)新節(jié)點(diǎn)中每個(gè) key 對(duì)應(yīng)的索引,比如下圖中 key 是 c 的元素的索引就是 2;然后計(jì)算出 newIndexOldIndexMap 存儲(chǔ)這個(gè) key 在老的子元素中的位置,我們可以根據(jù) c 的索引是 2,在 newIndexOldIndexMap 中查詢到在老的子元素的位置是 6, 關(guān)于 newIndexOldIndexMap 的具體邏輯你可以在上面的代碼中看到:

以上就是Vue3 如何通過(guò)虛擬DOM更新頁(yè)面詳解的詳細(xì)內(nèi)容,更多關(guān)于Vue3虛擬DOM更新頁(yè)面的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue動(dòng)態(tài)路由加載時(shí)出現(xiàn)Cannot?find?module?xxx問(wèn)題
這篇文章主要介紹了vue動(dòng)態(tài)路由加載時(shí)出現(xiàn)Cannot?find?module?xxx問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01
使用vue-cli導(dǎo)入Element UI組件的方法
這篇文章給大家介紹了使用vue-cli導(dǎo)入Element UI組件的方法,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友一起看看吧2018-05-05
vue3.0 CLI - 2.6 - 組件的復(fù)用入門教程
這篇文章主要介紹了 vue3.0 CLI - 2.6 - 組件的復(fù)用,非常不錯(cuò),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2018-09-09
解決vue-quill-editor上傳內(nèi)容由于圖片是base64的導(dǎo)致字符太長(zhǎng)的問(wèn)題
vue-quill-editor默認(rèn)插入圖片是直接將圖片轉(zhuǎn)為base64再放入內(nèi)容中,如果圖片較多,篇幅太長(zhǎng),就會(huì)比較煩惱,接下來(lái)通過(guò)本文給大家介紹vue-quill-editor上傳內(nèi)容由于圖片是base64的導(dǎo)致字符太長(zhǎng)的問(wèn)題及解決方法,需要的朋友可以參考下2018-08-08

