<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
上一講我們主要介紹了 Vue 專案的首次渲染流程,在 mountComponent 中註冊了effect 函數,這樣,在元件資料有更新的時候,就會通知到元件的 update 方法進行更新
Vue 中元件更新的方式也是使用了響應式 + 虛擬 DOM 的方式,這個我們在第一講中有介紹過 Vue 1、Vue 2 和 Vue 3 中更新方式的變化,今天我們就來詳細剖析一下 Vue 元件內部如何通過虛擬 DOM 更新頁面的程式碼細節
我們從虛擬 DOM 在 Vue 的執行流程開始講起。在 Vue 中,我們使用虛擬 DOM 來描述頁面的元件,比如下面的 template 雖然格式和 HTML 很像,但是在 Vue 的內部會解析成 JavaScript 函數,這個函數就是用來返回虛擬 DOM:
<div id="app"> <p>hello world</p> <Rate :value="4"></Rate> </div>
上面的 template 會解析成下面的函數,最終返回一個 JavaScript 的物件能夠描述這段HTML:
function render(){ return h('div',{id:"app"},children:[ h('p',{},'hello world'), h(Rate,{value:4}), ]) }
知道虛擬 DOM 是什麼之後,那麼它是怎麼建立的呢?
我們簡單回憶上一講介紹的 mount 函數,在程式碼中,我們使用 createVNode 函數建立專案的虛擬 DOM,可以看到 Vue 內部的虛擬 DOM,也就是 vnode,就是一個物件,通過 type、props、children 等屬性描述整個節點:
const vnode = createVNode( ( rootComponent as ConcreteComponent, rootProps ) function _createVNode() { // 處理屬性和 class if (props) { ... } // 標記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 // 標準化子節點 if (needFullChildrenNormalization) { normalizeChildren(vnode, children) } else if (children) { vnode.shapeFlag |= isString(children) ? ShapeFlags.TEXT_CHILDREN : ShapeFlags.ARRAY_CHILDREN } return vnode }componentUpdateFn
createVNode 負責建立 Vue 中的虛擬 DOM,而上一講中我們講過 mount 函數的核心邏輯就是使用 setupComponent 執行我們寫的 <script setup>
,使用 setupRenderEffect 監聽元件的資料變化;所以我們來到 setupRenderEffect 函數中,去完整地剖析 Vue 中虛擬 DOM 的更新邏輯
我們給元件註冊了 update 方法,這個方法使用 effect 包裹後,當元件內的 ref、reactive 包裹的響應式資料變化的時候就會執行 update 方法,觸發元件內部的更新機制
看下面的程式碼,在 setupRenderEffect 內部的 componentUpdateFn 中,updateComponentPreRenderer 更新了屬性和 slots,並且呼叫 renderComponentRoot 函數建立新的子樹物件 nextTree,然後內部依然是呼叫 patch 函數
可以看到,Vue 原始碼中的實現首次渲染和更新的邏輯都寫在一起,我們在遞迴的時候如果對一個標籤實現更新和渲染,就可以用一個函數實現
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 ) } } // 註冊effect函數 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() }
比較關鍵的就是上面程式碼中 32-39 行的 effect 函數,負責註冊元件,這個函數也是 Vue 元件更新的入口函數
資料更新之後就會執行 patch 函數,下圖就是 patch 函數執行的邏輯圖:
在 patch 函數中,會針對不同的元件型別執行不同的函數,元件我們會執行 processComponent,HTML 標籤我們會執行 processElement:
function path(n1, n2, container){ const { type, shapeFlag } = n2 switch (type) { case Text: processText(n1, n2, container) break // 還有註釋,fragment之類的可以處理,這裡忽略 default: // 通過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) { // 老規矩,沒有n1就是mount if (!n1) { // 初始化 component mountComponent(n2, container) } else { updateComponent(n1, n2, container) } }
由於更新之後不是首次渲染了,patch 函數內部會執行 updateComponent,看下面的 updateComponent 函數內部,shouldUpdateComponent 會判斷元件是否需要更新,實際執行的是 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 標籤和元件構成,元件內部的遞迴處理最終也是對 HTML 標籤的處理,所以,最後元件的更新都會進入到 processElement 內部的 patchElement 函數中
在函數 patchElement 中我們主要就做兩件事,更新節點自己的屬性和更新子元素
先看自身屬性的更新,這裡就能體現出 Vue 3 中效能優化的思想,通過 patchFlag 可以做到按需更新:
如果標記了 FULL_PROPS,就直接呼叫 patchProps;如果標記了 CLASS,說明節點只有 class 屬性是動態的,其他的 style 等屬性都不需要進行判斷和 DOM 操作
這樣就極大的優化了屬性操作的效能
內部執行 hostPatchProp 進行實際的 DOM 操作,你還記得上一講中 hostPatchProp 是從 nodeOps 中定義的嗎,其他動態屬性 STYLE、TEXT 等等也都是一樣的邏輯;Vue 3 的虛擬 DOM 真正做到了按需更新,這也是相比於 React 的一個優勢
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是動態的 if (patchFlag & PatchFlags.CLASS) { if (oldProps.class !== newProps.class) { hostPatchProp(el, 'class', null, newProps.class, isSVG) } } // style樣式是動態的 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 ) } } } } //文字是動態的 if (patchFlag & PatchFlags.TEXT) { if (n1.children !== n2.children) { hostSetElementText(el, n2.children as string) } } } }
而子元素的更新是 patchChildren 函數負責的,這個函數也是虛擬 DOM 中難度最高的一個函數,搞懂它還需要我們下一講中介紹的演演算法知識,今天我們就先理解它主要的實現思路
首先我們把子元素分成了文字、陣列和空三個狀態,新老子元素分別是這三種狀態的一個,構成了不同的執行邏輯;這樣 patchChildren 內部大致有五種情況需要處理:
最複雜的情況就是新的子元素和老的子元素都是陣列
最樸實無華的思路就是把老的子元素全部 unmount,新的子元素全部 mount,這樣雖然可以實現功能,但是沒法複用已經存在的 DOM 元素,比如我們只是在陣列中間新增了一個資料,全部 DOM 都銷燬就有點太可惜了
所以,我們需要判斷出可以複用的 DOM 元素,如果一個虛擬 DOM 沒有改動或者屬性變了,不需要完全銷燬重建,而是更新一下屬性,最大化減少 DOM 的操作,這個任務就會交給 patchKeyedChildren 函數去完成
patchKeyedChildren 函數,做的事情就是儘可能高效地把老的子元素更新成新的子元素,如何高效複用老的子元素中的 DOM 元素是 patchKeyedChildren 函數的難點:
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 ) } } } }
上面的程式碼執行邏輯如下圖所示,根據 flags 判斷子元素的型別後,執行不同的操作函數:
最後就剩下 patchChildren 的實現了,這也是各類虛擬 DOM 框架中最難實現的函數,我們需要實現一個高效的更新演演算法,能夠使用盡可能少的更新次數,來實現從老的子元素到新的子元素的更新;
舉個例子,類似體育課站隊的時候,大家一開始站一排,但是順序是亂的,我們需要儘快把隊伍按照個頭左低右高排列
在 React 中,這種場景的處理邏輯是先進行迴圈,使用的是單側插入的演演算法,我們在排隊的時候挨個對比,如果你站我右邊,並且個頭比我高一點,說明咱倆的相對位置和最終隊伍的位置是一致的,暫時不需要變化,如果你比我個頭矮,就需要去我左邊找到一個正確的位置插隊進去
由於都只向單側插入,最後我們就會把所有的節點移動到正確的位置之上,這就是 React15 框架內虛擬節點 diff 的邏輯,初步實現了 DOM 的複用;而 Vue 2 借鑑了 snabbdom 的演演算法,在此基礎上做了第一層雙端對比的優化
首先 Web 場景之下對一個陣列元素的操作,很少有直接全部替換的,比如我們操作一個表格,大概率是更關心表格某一行的一個欄位、新增一行、刪除一行,或者是對錶格某個欄位進行排序,所以我們可以從純演演算法的場景之中加入實際應用的場景
如果我們只是在表格裡新增一行,那麼可以不要一開始就開始迴圈,而是可以先進行節點的預判
比如,在下面的例子中,新的節點就是在老的節點中新增和刪除了幾個元素,我們在迴圈之前,先進行頭部元素的判斷;在這個例子裡,可以預判出頭部元素的 a、b、c、d 是一樣的節點,說明節點不需要重新建立,我們只需要進行屬性的更新,然後進行隊尾元素的預判,可以判斷出 g 和元素也是一樣的:
a b c d e f g h
a b c d i f j g h
這樣我們虛擬 DOM diff 的邏輯就變成了下面的結構, 現在只需要比較 ef 和 ifg 的區別:
(a b c d) e f (g h)
(a b c) d) i f j (g h)
相比於之前的對比場景,我們需要遍歷的運算量就大大減小了
而且,有很多場景比如新增一行或者刪除一行的簡單場景,預判完畢之後,新老元素有一個處於沒有元素的狀態,我們就可以直接執行 mount 或者 unmout 完成對比的全過程,不需要再進行復雜的遍歷:
(a b c d)
(a b c d) e
(a b c) d
(a b c
雙端對比的原理大致就是這樣;最後雙端對比之後的執行邏輯這一部分需要一些演演算法知識,下面會我詳細介紹,這裡你只需要掌握大概的思路
想讓一個隊伍儘快按照個頭排好序,如果能夠計算出,在隊伍中,個頭從低到高依次遞增的最多的佇列,讓這些人站在原地不動,其餘人穿插到他們中間,就可以最大化減少人員的移動,這就是一個最長底層子序列的演演算法問題
前面也說了,在執行 diff 之前,要根據需要判斷每個虛擬 DOM 節點有哪些屬性需要計算,因為無論響應式資料怎麼變化,靜態的屬性和節點都不會發生變化
所以我們看每個節點 diff 的時候會做什麼,在 renderer.ts 程式碼檔案中就可以看到程式碼,主要就是通過虛擬 DOM 節點的 patchFlag 樹形判斷是否需要更新節點
方法就是使用 & 操作符來判斷操作的型別,比如 patchFlag & PatchFlags.CLASS 來判斷當前元素的 class 是否需要計算 diff;shapeFlag & ShapeFlags.ELEMENT 來判斷當前虛擬 DOM 是 HTML 元素還是 Component 元件;這個“&”其實就是位運算的按位元與
// 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 ) }
上面的程式碼中 & 就是按位元與的操作符,這其實是二進位制上的計算符號,所以我們首先要了解一下什麼是二進位制
我們日常使用的數位都是十進位制數位,比如數位 13 就是 110+3 的運算結果,每個位置都是代表 10 的 n 次方;13 也可以使用二進位制表達,因為二進位制每個位置只能是 0 和 1 兩個數位,每個位置代表的是 2 的 n 次方,13 在二進位制裡是 1101,就是 18+14+02+1*1
而在 JavaScript 中我們可以很方便地使用 toString(2) 的方式,把十進位制數位轉換成二進位制;運算的概念很簡單,就是在二進位制上的“與”和“或”運算:
(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左移動兩位,就是100 就是1*2平方 = 4
二進位制中,我們每個位置只能是 0 或者 1 這兩個值,& 和 | 的概念和 JavaScript 中的 && 和 || 保持一致;兩個二進位制的 & 運算就是隻有兩個二進位制位置都是 1 的時候,結果是 1,其餘情況運算結果都是 0;| 是按位元置進行“或”運算,只有兩個二進位制位置都是 0 的時候,結果是 0,其餘情況運算結果都是 1;並且,還可以通過左移 << 和右移 >> 操作符,實現乘以 2 和除以 2 的效果
由於這些都是在二進位制上的計算,運算的效能通常會比字串和數位的計算效能要好,這也是很多框架內部使用位運算的原因
這麼說估計你不是很理解,我們結合一個 LeetCode 題看看為什麼說二進位制的位運算效能更好
我們來做一下 LeetCode231 題,題目描述很簡單,判斷數位 n 是不是 2 的冪次方,也就是說,判斷數位 n 是不是 2 的整次方,比如 2、4、8;我們可以很輕鬆地寫出 JavaScript 的解答,n 一直除以 2,如果有餘數就是 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 };
不過上面的解答我們可以用位運算來優化
先來分析一下 2 的冪次方的特點
2 的冪次方就是數位 1 左移動若干次,其餘位置全部都是 0,所以 n-1 就是最高位變成0,其餘位置都變成 1,就像十進位制裡的 10000-1 = 9999。這樣,n 和 n-1 每個二進位制位的數位都不一樣,我們可以很輕鬆地用按位元“與”來判斷這個題的答案,如果 n&n-1 是 0 的話,數位 n 就符合 2 的整次冪的特點:
16 10000 16-1 = 15 01111 16&15 == 0 var isPowerOfTwo = function(n) { return n>0 && (n & (n - 1)) === 0 };
所以我們使用位運算提高了程式碼的整體效能
好,搞清楚為什麼用位運算,我們回來看 diff 判斷,如何根據位運算的特點,設計出許可權的組合認證方案
比如 Vue 中的動態屬性,有文字、class、style、props 幾個屬性,我們可以使用二進位制中的一個位置來表示許可權,看下面的程式碼,我們使用左移的方式分別在四個二進位制上標記了 1,代表四種不同的許可權,使用按位元或的方式去實現許可權授予
比如,一個節點如果 TEXT 和 STYLE 都需要修改,我們只需要使用 | 運運算元就可以得到 flag1 的許可權表示,這就是為什麼 Vue 3 中針對虛擬 DOM 型別以及虛擬 DOM 需要動態計算 diff 的樹形都做了標記,你可以在 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 // 許可權校驗 flag1 & PatchFlags.TEXT // 有許可權,結果大於1 flag1 & PatchFlags.CLASS //沒有許可權 是0
然後就到了虛擬 DOM 計算 diff 中的演演算法了
上面我們詳細介紹了在虛擬 diff 計算中,如果新老子元素都是陣列的時候,需要先做首尾的預判,如果新的子元素和老的子元素在預判完畢後,未處理的元素依然是陣列,那麼就需要對兩個陣列計算 diff,最終找到最短的操作路徑,能夠讓老的子元素通過儘可能少的操作,更新成為新的子元素
Vue 3 借鑑了 infero 的演演算法邏輯,就像操場上需要按照個頭從低到高站好一樣,我們採用的思路是先尋找一個現有佇列中由低到高的佇列,讓這個佇列儘可能的長,它們的相對位置不需要變化,而其他元素進行插入和移動位置,這樣就可以做到儘可能少的操作DOM
所以如何尋找這個最長遞增的序列呢?這就是今天的重點演演算法知識了,我們看 LeetCode 第 300 題,題目描述如下, 需要在陣列中找到最長底層的自序列長度:
給你一個整數陣列 nums,找到其中最長嚴格遞增子序列的長度
子序列是由陣列派生而來的序列,刪除(或不刪除)陣列中的元素而不改變其餘元素的順序
例如,[3,6,2,7] 是陣列 [0,3,1,6,2,2,7] 的子序列
=
輸入:nums = [10,9,2,5,3,7,101,18]
輸出:4
解釋:最長遞增子序列是 [2,3,7,101],因此長度為 4
首先我們可以使用動態規劃的思路,通過每一步的遞推,使用 dp 陣列,記錄出每一步操作的最優解,最後得到全域性最優解
在這個例子中,我們可以把 dp[i]
定義成 nums[0]
到 nums[i]
這個區間內,陣列的最長遞增子序列的長度,並且 dp 陣列的初始值設為 1
從左邊向右遞推,如果 nums[i+1]
> nums[i]
,dp[i+1]
就等於 dp[i]+1
;如果 nums[i+1]
< nums[i]
,就什麼都不需要幹,這樣我們在遍歷的過程中,就能根據陣列當前位置之前的最長遞增子序列長度推匯出 i+1 位置的最長遞增子序列長度
所以可以得到如下解法:
/** * @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) }
由於我們需要兩層迴圈,所以這個解法的時間複雜度是 n 的平方,這個解法其實已經不錯了,但是還有更優秀的解法,也就是 Vue 3 中用到的演演算法:貪心 + 二分
我們再看一下這個題,貪心的思路就是在尋找最長遞增的序列,所以,[1,3]要比[1,5]好,也就是說,在這個上升的序列中,我們要讓上升速度儘可能變得慢,這樣才有可能讓後面的元素儘可能也遞增
我們可以建立一個 arr
陣列,用來儲存這種策略下的最長遞增子序列
如果當前遍歷的 nums[i]
大於 arr
的最後一個元素,也就是大於 arr
的最大值時,我們把nums[i]
追加到後面即可,否則我們就在 arr
中尋找一個第一個大於 num[i]
的數位並替換它;因為是 arr
是遞增的數列,所以在尋找插入位置的時候,我們可以使用二分查詢的方式,把整個演演算法的複雜度變成 O(nlgn)
下面的程式碼就是貪心 + 二分的解法,我們可以得到正確的最長遞增子序列的長度:
/** * @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 尾元素時,直接追加到後面,遞增序列長度+1 if (nums[i] > arr[arr.length - 1]) { arr.push(nums[i]) } else { // 否則,查詢遞增子序列中第一個大於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 };
但是貪心 + 二分的這種解法,現在只能得到最長遞增子序列的長度,但是最後得到的 arr 並不一定是最長遞增子序列,因為我們移動的 num[i]
位置可能會不正確,只是得到的陣列長度是正確的,所以我們需要對這個演演算法改造一下,把整個陣列複製一份之後,最後也能得到正確的最長遞增子序列
具體程式碼怎麼寫呢?我們來到 Vue 3 的 renderer.ts 檔案中,函數 getSquenece 就是用來生成最長遞增子序列,看下面的程式碼:
// 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 // 儲存在result最後一個索引的值 result.push(i) continue } u = 0 v = result.length - 1 // 二分查詢,查詢比arrI小的節點,更新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] // 查詢陣列p 找到最終的索引 while (u-- > 0) { result[u] = v v = p[v] } return result }
這段程式碼就是 Vue 3 裡的實現,result 儲存的就是長度是 i 的遞增子序列最小末位置的索引,最後計算出最長遞增子序列
我們得到 increasingNewIndexSequence 佇列後,再去遍歷陣列進行 patch 操作就可以實現完整的 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-- } } }
上面程式碼的思路,我們用下圖演示。做完雙端對比之後,a 和 g 已經計算出可以直接複用 DOM,剩下的佇列中我們需要把 hbfdc 更新成 abdef
首先我們需要使用 keyToNewIndexMap 儲存新節點中每個 key 對應的索引,比如下圖中 key 是 c 的元素的索引就是 2;然後計算出 newIndexOldIndexMap 儲存這個 key 在老的子元素中的位置,我們可以根據 c 的索引是 2,在 newIndexOldIndexMap 中查詢到在老的子元素的位置是 6, 關於 newIndexOldIndexMap 的具體邏輯你可以在上面的程式碼中看到:
以上就是Vue3 如何通過虛擬DOM更新頁面詳解的詳細內容,更多關於Vue3虛擬DOM更新頁面的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45