diff 算法深入一下?
一、前言
有同學問:能否詳細說一下 diff 算法。
簡單說:diff 算法是一種優化手段,將前後兩個模塊進行差異化比較,修補 (更新) 差異的過程叫做 patch,也叫打補丁。
詳細的說,請閱讀這篇文章,有疑問的地方歡迎聯繫「松寶寫代碼」一起討論。
文章主要解決的問題:
-
1、爲什麼要說這個 diff 算法?
-
2、虛擬 dom 的 diff 算法
-
3、爲什麼使用虛擬 dom?
-
4、diff 算法的複雜度和特點?
-
5、vue 的模板文件是如何被編譯渲染的?
-
6、vue2.x 和 vue3.x 中的 diff 有區別嗎
-
7、diff 算法的源頭 snabbdom 算法
-
8、diff 算法與 snabbdom 算法的差異地方?
二、爲什麼要說這個 diff 算法?
因爲 diff 算法是 vue2.x , vue3.x 以及 react 中關鍵核心點,理解 diff 算法,更有助於理解各個框架本質。
說到「diff 算法」,不得不說「虛擬 Dom」,因爲這兩個息息相關。
比如:
-
vue 的響應式原理?
-
vue 的 template 文件是如何被編譯的?
-
介紹一下 Virtual Dom 算法?
-
爲什麼要用 virtual dom 呢?
-
diff 算法複雜度以及最大的特點?
-
vue2.x 的 diff 算法中節點比較情況?
等等
三、虛擬 dom 的 diff 算法
我們先來說說虛擬 Dom,就是通過 JS 模擬實現 DOM ,接下來難點就是如何判斷舊對象和新對象之間的差異。
Dom 是多叉樹結構,如果需要完整的對比兩棵樹的差異,那麼算法的時間複雜度 O(n ^ 3),這個複雜度很難讓人接收,尤其在 n 很大的情況下,於是 React 團隊優化了算法,實現了 O(n) 的複雜度來對比差異。
實現 O(n) 複雜度的關鍵就是隻對比同層的節點,而不是跨層對比,這也是考慮到在實際業務中很少會去跨層的移動 DOM 元素。
虛擬 DOM 差異算法的步驟分爲 2 步:
-
首先從上至下,從左往右遍歷對象,也就是樹的深度遍歷,這一步中會給每個節點添加索引,便於最後渲染差異
-
一旦節點有子元素,就去判斷子元素是否有不同
3.1 vue 中 diff 算法
實際 diff 算法比較中,節點比較主要有 5 種規則的比較
-
1、如果新舊 VNode 都是靜態的,同時它們的 key 相同(代表同一節點),並且新的 VNode 是 clone 或者是標記了 once(標記 v-once 屬性,只渲染一次),那麼只需要替換 elm 以及 componentInstance 即可。
-
2、新老節點均有 children 子節點,則對子節點進行 diff 操作,調用 updateChildren,這個 updateChildren 也是 diff 的核心。
-
3、如果老節點沒有子節點而新節點存在子節點,先清空老節點 DOM 的文本內容,然後爲當前 DOM 節點加入子節點。
-
4、當新節點沒有子節點而老節點有子節點的時候,則移除該 DOM 節點的所有子節點。
-
5、當新老節點都無子節點的時候,只是文本的替換
部分源碼 https://github.com/vuejs/vue/blob/8a219e3d4cfc580bbb3420344600801bd9473390/src/core/vdom/patch.js#L501 如下:
function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
if (oldVnode === vnode) {
return;
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode);
}
const elm = (vnode.elm = oldVnode.elm);
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
} else {
vnode.isAsyncPlaceholder = true;
}
return;
}
if (
isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance;
return;
}
let i;
const data = vnode.data;
if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
i(oldVnode, vnode);
}
const oldCh = oldVnode.children;
const ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode "i");
if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode);
}
if (isUndef(vnode.text)) {
// 定義了子節點,且不相同,用diff算法對比
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
// 新節點有子元素。舊節點沒有
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
// 檢查key
checkDuplicateKeys(ch);
}
// 清空舊節點的text屬性
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '');
// 添加新的Vnode
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
// 如果舊節點的子節點有內容,新的沒有。那麼直接刪除舊節點子元素的內容
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1);
// 如上。只是判斷是否爲文本節點
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '');
}
// 如果文本節點不同,替換節點內容
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text);
}
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode);
}
}
3.2 React diff 算法
在 reconcileChildren 函數的入參中
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
-
workInProgress:作爲父節點傳入,新生成的第一個 fiber 的 return 會被指向它。
-
current.child:舊 fiber 節點,diff 生成新 fiber 節點時會用新生成的 ReactElement 和它作比較。
-
nextChildren:新生成的 ReactElement,會以它爲標準生成新的 fiber 節點。
-
renderLanes:本次的渲染優先級,最終會被掛載到新 fiber 的 lanes 屬性上。
diff 的兩個主體是:oldFiber(current.child)和 newChildren(nextChildren,新的 ReactElement),它們是兩個不一樣的數據結構。
部分源碼
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
lanes: Lanes,
): Fiber | null {
/* * returnFiber:currentFirstChild的父級fiber節點
* currentFirstChild:當前執行更新任務的WIP(fiber)節點
* newChildren:組件的render方法渲染出的新的ReactElement節點
* lanes:優先級相關
* */
// resultingFirstChild是diff之後的新fiber鏈表的第一個fiber。
let resultingFirstChild: Fiber | null = null;
// resultingFirstChild是新鏈表的第一個fiber。
// previousNewFiber用來將後續的新fiber接到第一個fiber之後
let previousNewFiber: Fiber | null = null;
// oldFiber節點,新的child節點會和它進行比較
let oldFiber = currentFirstChild;
// 存儲固定節點的位置
let lastPlacedIndex = 0;
// 存儲遍歷到的新節點的索引
let newIdx = 0;
// 記錄目前遍歷到的oldFiber的下一個節點
let nextOldFiber = null;
// 該輪遍歷來處理節點更新,依據節點是否可複用來決定是否中斷遍歷
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// newChildren遍歷完了,oldFiber鏈沒有遍歷完,此時需要中斷遍歷
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
// 用nextOldFiber存儲當前遍歷到的oldFiber的下一個節點
nextOldFiber = oldFiber.sibling;
}
// 生成新的節點,判斷key與tag是否相同就在updateSlot中
// 對DOM類型的元素來說,key 和 tag都相同纔會複用oldFiber
// 並返回出去,否則返回null
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);
// newFiber爲 null說明 key 或 tag 不同,節點不可複用,中斷遍歷
if (newFiber === null) {
if (oldFiber === null) {
// oldFiber 爲null說明oldFiber此時也遍歷完了
// 是以下場景,D爲新增節點
// 舊 A - B - C
// 新 A - B - C - D oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
// shouldTrackSideEffects 爲true表示是更新過程
if (oldFiber && newFiber.alternate === null) {
// newFiber.alternate 等同於 oldFiber.alternate
// oldFiber爲WIP節點,它的alternate 就是 current節點
// oldFiber存在,並且經過更新後的新fiber節點它還沒有current節點,
// 說明更新後展現在屏幕上不會有current節點,而更新後WIP
// 節點會稱爲current節點,所以需要刪除已有的WIP節點
deleteChild(returnFiber, oldFiber);
}
}
// 記錄固定節點的位置
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 將新fiber連接成以sibling爲指針的單向鏈表
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
// 將oldFiber節點指向下一個,與newChildren的遍歷同步移動
oldFiber = nextOldFiber;
}
// 處理節點刪除。新子節點遍歷完,說明剩下的oldFiber都是沒用的了,可以刪除.
if (newIdx === newChildren.length) {
// newChildren遍歷結束,刪除掉oldFiber鏈中的剩下的節點
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 處理新增節點。舊的遍歷完了,能複用的都複用了,所以意味着新的都是新插入的了
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
// 基於新生成的ReactElement創建新的Fiber節點
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
// 記錄固定節點的位置lastPlacedIndex
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 將新生成的fiber節點連接成以sibling爲指針的單向鏈表
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// 執行到這是都沒遍歷完的情況,把剩餘的舊子節點放入一個以key爲鍵,值爲oldFiber節點的map中
// 這樣在基於oldFiber節點新建新的fiber節點時,可以通過key快速地找出oldFiber
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 節點移動
for (; newIdx < newChildren.length; newIdx++) {
// 基於map中的oldFiber節點來創建新fiber
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// 因爲newChildren中剩餘的節點有可能和oldFiber節點一樣,只是位置換了,
// 但也有可能是是新增的.
// 如果newFiber的alternate不爲空,則說明newFiber不是新增的。
// 也就說明着它是基於map中的oldFiber節點新建的,意味着oldFiber已經被使用了,所以需
// 要從map中刪去oldFiber
existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);
}
}
// 移動節點,多節點diff的核心,這裏真正會實現節點的移動
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 將新fiber連接成以sibling爲指針的單向鏈表
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
// 此時newChildren遍歷完了,該移動的都移動了,那麼刪除剩下的oldFiber
existingChildren.forEach((child) => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
四、爲什麼使用虛擬 dom?
很多時候手工優化 dom 確實會比 virtual dom 效率高,對於比較簡單的 dom 結構用手工優化沒有問題,但當頁面結構很龐大,結構很複雜時,手工優化會花去大量時間,而且可維護性也不高,不能保證每個人都有手工優化的能力。至此,virtual dom 的解決方案應運而生。
virtual dom 是 “解決過多的操作 dom 影響性能” 的一種解決方案。
virtual dom 很多時候都不是最優的操作,但它具有普適性,在效率、可維護性之間達到平衡。
virutal dom 的意義:
-
1、提供一種簡單對象去代替複雜的 dom 對象,從而優化 dom 操作
-
2、提供一箇中間層,js 去寫 ui,ios 安卓之類的負責渲染,就像 reactNative 一樣。
五、diff 算法的複雜度和特點?
vue2.x 的 diff 位於 patch.js 文件中,該算法來源於 snabbdom,複雜度爲 O(n)。瞭解 diff 過程可以讓我們更高效的使用框架。react 的 diff 其實和 vue 的 diff 大同小異。
最大特點:比較只會在同層級進行, 不會跨層級比較。
<!-- 之前 -->
<div> <!-- 層級1 -->
<p> <!-- 層級2 -->
<b> aoy </b> <!-- 層級3 -->
<span>diff</Span>
</p>
</div>
<!-- 之後 -->
<div> <!-- 層級1 -->
<p> <!-- 層級2 -->
<b> aoy </b> <!-- 層級3 -->
</p>
<span>diff</Span>
</div>
對比之前和之後:可能期望將<span>
直接移動到<p>
的後邊,這是最優的操作。
但是實際的 diff 操作是:
-
1、移除
<p>
裏的<span>
; -
2、在創建一個新的
<span>
插到<p>
的後邊。 因爲新加的<span>
在層級 2,舊的在層級 3,屬於不同層級的比較。
六、vue 的模板文件是如何被編譯渲染的?
vue 中也使用 diff 算法,有必要了解一下 Vue 是如何工作的。通過這個問題,我們可以很好的掌握,diff 算法在整個編譯過程中,哪個環節,做了哪些操作,然後使用 diff 算法後輸出什麼?
解釋:
1、mount 函數
mount 函數主要是獲取 template,然後進入 compileToFunctions 函數。
2、compileToFunction 函數
compileToFunction 函數主要是將 template 編譯成 render 函數。首先讀取緩存,沒有緩存就調用 compile 方法拿到 render 函數的字符串形式,在通過 new Function 的方式生成 render 函數。
// 有緩存的話就直接在緩存裏面拿
const key = options && options.delimiters ? String(options.delimiters) + template : template;
if (cache[key]) {
return cache[key];
}
const res = {};
const compiled = compile(template, options); // compile 後面會詳細講
res.render = makeFunction(compiled.render); //通過 new Function 的方式生成 render 函數並緩存
const l = compiled.staticRenderFns.length;
res.staticRenderFns = new Array(l);
for (let i = 0; i < l; i++) {
res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i]);
}
// ......
return (cache[key] = res); // 記錄至緩存中
3、compile 函數
compile 函數將 template 編譯成 render 函數的字符串形式。後面我們主要講解 render
完成 render 方法生成後,會進入到 mount 進行 DOM 更新。該方法核心邏輯如下:
// 觸發 beforeMount 生命週期鉤子
callHook(vm, 'beforeMount');
// 重點:新建一個 Watcher 並賦值給 vm._watcher
vm._watcher = new Watcher(
vm,
function updateComponent() {
vm._update(vm._render(), hydrating);
},
noop,
);
hydrating = false;
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm;
-
首先會 new 一個 watcher 對象(主要是將模板與數據建立聯繫),在 watcher 對象創建後,
-
會運行傳入的方法 vm._update(vm._render(), hydrating) 。 其中的 vm._render() 主要作用就是運行前面 compiler 生成的 render 方法,並返回一個 vNode 對象。
-
vm.update() 則會對比新的 vdom 和當前 vdom,並把差異的部分渲染到真正的 DOM 樹上。(watcher 背後的實現原理:vue2.x 的響應式原理)
上面提到的 compile 就是將 template 編譯成 render 函數的字符串形式。核心代碼如下:
export function compile(template: string, options: CompilerOptions): CompiledResult {
const AST = parse(template.trim(), options); //1. parse
optimize(AST, options); //2.optimize
const code = generate(AST, options); //3.generate
return {
AST,
render: code.render,
staticRenderFns: code.staticRenderFns,
};
}
compile 這個函數主要有三個步驟組成:
-
parse,
-
optimize
-
generate
分別輸出一個包含
-
AST 字符串
-
staticRenderFns 的對象字符串
-
render 函數 的字符串。
parse 函數:主要功能是將 template 字符串解析成 AST(抽象語法樹)。前面定義的 ASTElement 的數據結構,parse 函數就是將 template 裏的結構(指令,屬性,標籤) 轉換爲 AST 形式存進 ASTElement 中,最後解析生成 AST。
optimize 函數(src/compiler/optomizer.js): 主要功能是標記靜態節點。後面 patch 過程中對比新舊 VNode 樹形結構做優化。被標記爲 static 的節點在後面的 diff 算法中會被直接忽略,不做詳細比較。
generate 函數(src/compiler/codegen/index.js): 主要功能根據 AST 結構拼接生成 render 函數的字符串。
const code = AST ? genElement(AST) : '_c("div")';
staticRenderFns = prevStaticRenderFns;
onceCount = prevOnceCount;
return {
render: `with(this){return ${code}}`, //最外層包一個 with(this) 之後返回
staticRenderFns: currentStaticRenderFns,
};
其中 genElement 函數(src/compiler/codgen/index.js)是根據 AST 的屬性調用不同的方法生成字符串返回。
總之:
就是 compile 函數中三個核心步驟介紹,
-
compile 之後我們得到 render 函數的字符串形式,後面通過 new Function 得到真正的渲染函數。
-
數據發生變化後,會執行 watcher 中的_update 函數(src/core/instance/lifecycle.js),_update 函數會執行這個渲染函數,輸出一個新的 VNode 樹形結構的數據。
-
然後調用 patch 函數,拿到這個新的 VNode 與舊的 VNode 進行對比,只有發生了變化的節點纔會被更新到新的真實 DOM 樹上。
4、patch 函數
patch 函數 就是新舊 VNode 對比的 diff 函數,主要是爲了優化 dom,通過算法使操作 dom 的行爲降低到最低, diff 算法來源於 snabbdom,是 VDOM 思想的核心。snabbdom 的算法是爲了 DOM 操作跨級增刪節點較少的這一目標進行優化, 它只會在同層級進行,不會跨層級比較。
總結一下
-
compile 函數主要是將 template 轉換爲 AST,優化 AST,再將 AST 轉換爲 render 函數的字符串形式。
-
再通過 new Function 得到真正的 render 函數,render 函數與數據通過 Watcher 產生關聯。
-
在數據反生變化的時候調用 patch 函數,執行 render 函數,生成新的 VNode,與舊的 VNode 進行 diff,最終更新 DOM 樹。
七、vue2.x,vue3.x,React 中的 diff 有區別嗎?
總的來說:
-
vue2.x 的核心 diff 算法採用雙端比較的算法,同時從新舊 children 的兩端開始進行比較,藉助 key 可以複用的節點。
-
vue3.x 借鑑了一些別的算法 inferno(https://github.com/infernojs/inferno) 解決:1、處理相同的前置和後置元素的預處理;2、一旦需要進行 DOM 移動,我們首先要做的就是找到 source 的最長遞增子序列。
在創建 VNode 就確定類型,以及在 mount/patch 的過程中採用位運算來判斷一個 VNode 的類型,在這個優化的基礎上再配合 Diff 算法,性能得到提升。
可以看一下 vue3.x 的源碼: https://github.com/vuejs/vue/blob/8a219e3d4cfc580bbb3420344600801bd9473390/src/core/vdom/patch.js
- react 通過 key 和 tag 來對節點進行取捨,可直接將複雜的比對攔截掉,然後降級成節點的移動和增刪這樣比較簡單的操作。
對 oldFiber 和新的 ReactElement 節點的比對,將會生成新的 fiber 節點,同時標記上 effectTag,這些 fiber 會被連到 workInProgress 樹中,作爲新的 WIP 節點。樹的結構因此被一點點地確定,而新的 workInProgress 節點也基本定型。在 diff 過後,workInProgress 節點的 beginWork 節點就完成了,接下來會進入 completeWork 階段。
八、diff 算法的源頭 snabbdom 算法
snabbdom 算法: https://github.com/snabbdom/snabbdom
定位:一個專注於簡單性、模塊化、強大功能和性能的虛擬 DOM 庫。
1、snabbdom 中定義 Vnode 的類型
snabbdom 中定義 Vnode 的類型 (https://github.com/snabbdom/snabbdom/blob/27e9c4d5dca62b6dabf9ac23efb95f1b6045b2df/src/vnode.ts#L12)
export interface VNode {
sel: string | undefined; // selector的縮寫
data: VNodeData | undefined; // 下面VNodeData接口的內容
children: Array<VNode | string> | undefined; // 子節點
elm: Node | undefined; // element的縮寫,存儲了真實的HTMLElement
text: string | undefined; // 如果是文本節點,則存儲text
key: Key | undefined; // 節點的key,在做列表時很有用
}
export interface VNodeData {
props?: Props;
attrs?: Attrs;
class?: Classes;
style?: VNodeStyle;
dataset?: Dataset;
on?: On;
attachData?: AttachData;
hook?: Hooks;
key?: Key;
ns?: string; // for SVGs
fn?: () => VNode; // for thunks
args?: any[]; // for thunks
is?: string; // for custom elements v1
[key: string]: any; // for any other 3rd party module
}
2、init 函數分析
init 函數的地址:
https://github.com/snabbdom/snabbdom/blob/27e9c4d5dca62b6dabf9ac23efb95f1b6045b2df/src/init.ts#L63
init() 函數接收一個模塊數組 modules 和可選的 domApi 對象作爲參數,返回一個函數,即 patch() 函數。
domApi 對象的接口包含了很多 DOM 操作的方法。
3、patch 函數分析
源碼:
https://github.com/snabbdom/snabbdom/blob/27e9c4d5dca62b6dabf9ac23efb95f1b6045b2df/src/init.ts#L367
-
init() 函數返回了一個 patch() 函數
-
patch() 函數接收兩個 VNode 對象作爲參數,並返回一個新 VNode。
4、h 函數分析
源碼:
https://github.com/snabbdom/snabbdom/blob/27e9c4d5dca62b6dabf9ac23efb95f1b6045b2df/src/h.ts#L33
h() 函數接收多種參數,其中必須有一個 sel 參數,作用是將節點內容掛載到該容器中,並返回一個新 VNode。
九、diff 算法與 snabbdom 算法的差異地方?
在 vue2.x 不是完全 snabbdom 算法,而是基於 vue 的場景進行了一些修改和優化,主要體現在判斷 key 和 diff 部分。
1、在 snabbdom 中 通過 key 和 sel 就判斷是否爲同一節點,那麼在 vue 中,增加了一些判斷 在滿足 key 相等的同時會判斷,tag 名稱是否一致,是否爲註釋節點,是否爲異步節點,或者爲 input 時候類型是否相同等。
https://github.com/vuejs/vue/blob/8a219e3d4cfc580bbb3420344600801bd9473390/src/core/vdom/patch.js#L35
/**
* @param a 被對比節點
* @param b 對比節點
* 對比兩個節點是否相同
* 需要組成的條件:key相同,tag相同,是否都爲註釋節點,是否同時定義了data,如果是input標籤,那麼type必須相同
*/
function sameVnode(a, b) {
return (
a.key === b.key &&
((a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)) ||
(isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)))
);
}
2、diff 差異,patchVnode 是對比模版變化的函數,可能會用到 diff 也可能直接更新。
https://github.com/vuejs/vue/blob/8a219e3d4cfc580bbb3420344600801bd9473390/src/core/vdom/patch.js#L404
function updateChildren(
parentElm,
oldCh,
newCh,
insertedVnodeQueue,
removeOnly
) {
let oldStartIdx = 0;
let newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx, idxInOld, vnodeToMove, refElm;
const canMove = !removeOnly;
if (process.env.NODE_ENV !== "production") {
checkDuplicateKeys(newCh);
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(
oldStartVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(
oldEndVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(
oldStartVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
);
canMove &&
nodeOps.insertBefore(
parentElm,
oldStartVnode.elm,
nodeOps.nextSibling(oldEndVnode.elm)
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(
oldEndVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
);
canMove &&
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (isUndef(oldKeyToIdx))
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
if (isUndef(idxInOld)) {
// New element
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
);
} else {
// vnodeToMove將要移動的節點
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(
vnodeToMove,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
);
oldCh[idxInOld] = undefined;
canMove &&
nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
// same key but different element. treat as new element
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
);
}
}
// vnodeToMove將要移動的節點
newStartVnode = newCh[++newStartIdx];
}
}
// 舊節點完成,新的沒完成
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
refElm,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
// 新的完成,老的沒完成
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx);
}
}
更多閱讀
引用
-
https://github.com/vuejs/vue/blob/8a219e3d4cfc580bbb3420344600801bd9473390/src/core/vdom/patch.js#L501
-
https://github.com/infernojs/inferno
-
https://github.com/vuejs/vue/blob/8a219e3d4cfc580bbb3420344600801bd9473390/src/core/vdom/patch.js
-
https://github.com/snabbdom/snabbdom
-
https://github.com/snabbdom/snabbdom/blob/27e9c4d5dca62b6dabf9ac23efb95f1b6045b2df/src/vnode.ts#L12
-
https://github.com/snabbdom/snabbdom/blob/27e9c4d5dca62b6dabf9ac23efb95f1b6045b2df/src/init.ts#L63
-
https://github.com/snabbdom/snabbdom/blob/27e9c4d5dca62b6dabf9ac23efb95f1b6045b2df/src/init.ts#L367
-
https://github.com/snabbdom/snabbdom/blob/27e9c4d5dca62b6dabf9ac23efb95f1b6045b2df/src/h.ts#L33
-
https://github.com/vuejs/vue/blob/8a219e3d4cfc580bbb3420344600801bd9473390/src/core/vdom/patch.js#L35
-
https://github.com/vuejs/vue/blob/8a219e3d4cfc580bbb3420344600801bd9473390/src/core/vdom/patch.js#L404
後言
數據平臺前端團隊,在公司內負責大數據相關產品的研發。我們在前端技術上保持着非常強的熱情,除了數據產品相關的研發外,在數據可視化、海量數據處理優化、web excel、WebIDE、私有化部署、工程工具都方面都有很多的探索和積累,有興趣可以與我們聯繫。
投遞簡歷,更多精彩文章,歡迎關注 “豆皮範兒”
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Y6eD7RB74mE6ceEGMFBGug