>[success] # patch 方法分析(三)
~~~
1.上一個章節分析后,patch更新主要分為兩塊,一塊是新老虛擬dom相同,一塊是不同,這里是對
相同位置的分析
~~~
>[info] ## 分析patch 里面patchVnode
~~~
1.在patch 里如果 oldVnode 和 vnode 相同(key 和 sel 相同),調用 patchVnode(),找節點的差異并更新 DOM
2.在這里主要對比 oldVnode 和 vnode 的差異,把差異渲染到 DOM
3.分析執行過程(分析過程直接看虛擬dom比較的代碼,不分析鉤子部分):
3.1.如果 vnode.text 未定義
3.1.1.如果 oldVnode.children 和 vnode.children 都有值
3.1.1.1.調用 updateChildren()
3.1.1.2.使用 diff 算法對比子節點,更新子節點
3.1.2.如果 vnode.children 有值, oldVnode.children 無值
3.1.2.1.清空 DOM 元素
3.1.2.2.調用 addVnodes() ,批量添加子節點
3.1.3.如果 oldVnode.children 有值, vnode.children 無值
3.1.3.1.調用 removeVnodes() ,批量移除子節點
3.1.4.如果 oldVnode.text 有值
3.1.4.1.清空 DOM 元素的內容
3.2.如果設置了 vnode.text 并且和和 oldVnode.text 不等
3.2.1.如果老節點有子節點,全部移除
3.2.2.設置 DOM 元素的 textContent 為 vnode.text
~~~
~~~
// 如果 vnode.text 未定義,有text 就沒有children
if (isUndef(vnode.text)) {
// 如果新老節點都有 children
if (isDef(oldCh) && isDef(ch)) {
// 使用 diff 算法對比子節點,更新子節點
if (oldCh !== ch) updateChildren(elm, oldCh, ch,
insertedVnodeQueue);
} else if (isDef(ch)) {
// 如果新節點有 children,老節點沒有 children
// 如果老節點有text,清空dom 元素的內容
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
// 批量添加子節點
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// 如果老節點有children,新節點沒有children
// 批量移除子節點
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
// 如果老節點有 text,清空 DOM 元素
api.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
// 如果沒有設置 vnode.text
if (isDef(oldCh)) {
// 如果老節點有 children,移除
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
// 設置 DOM 元素的 textContent 為 vnode.text
api.setTextContent(elm, vnode.text!);
~~~
>[info] ## updateChildren -- diff
~~~
1.'patchVnode'是對具有相同父節點(這里判斷依據是key和sel選擇器相同則認為是相同父節點),
新老虛擬dom此時要比也就是自己的子節點是否相同,其他情況上面分析過,其中最復雜的情況就是
新老子節點都是有children 情況,最笨的方法拿每一個老節點虛擬dom和新節點的去比較,如下圖嵌套三層
的情況下時間復雜度為 O(n^3)
2.如何優化?
2.1.需要先假設一個前提,'在DOM 操作的時候我們很少很少會把一個父節點移動/更新到某一個子節點'
2.2.此時我們要做的就是同級比較即可,每一層和每一層去比較,如果依舊采用的思路是拿每一個老節點
虛擬dom和新節點的去比較,雖然變成了層和層的去比較此時下圖的時間復雜度為 每段O(n^2)比較和,時間上
感覺比之前快了
2.3.現在再換一個思路利用指針進行找同級別的子節點依次比較,然后再找下一級別的節點比較,這樣算法的
時間復 雜度為 O(n)
2.4.計算上的問題解決了,考慮的就是dom復用,有時候重新排序只是新老dom節點位置發生變化,如果能復用
相同的dom而不是重新創建dom這樣就又可以減少一部分性能開銷
~~~

>[danger] ##### 情況分析
~~~
1.oldStartVnode / newStartVnode (舊開始節點 / 新開始節點)
2.oldEndVnode / newEndVnode (舊結束節點 / 新結束節點)
3.oldStartVnode / oldEndVnode (舊開始節點 / 新結束節點)
4.oldEndVnode / newStartVnode (舊結束節點 / 新開始節點)
5.前四種比較都不同,則使用新開始節點的key值在【舊開始節點-舊結束節點】的子數組中找相同key的節點。
若沒有在子數組中找到相同key節點則新開始節點是新節點,創建一個該節點的真實DOM節點插入到舊開始節
點指向的真實DOM節點之前;若找到相同key的節點獲取該舊節點,比較新、舊節點的選擇器是否相同,
選擇器不同則創建一個該節點的真實DOM節點插入到舊開始節點指向的真實DOM節點之前,選擇器相同則
對比兩個節點的差異執行相應的操作(如更新DOM節點),并將子數組中key節點指向到真實DOM節點移動
到到舊開始索引指向的真實DOM節點之前(移動的是真實DOM而不是虛擬DOM,虛擬DOM無變化)。
最后將新開始索引指向下一個節點
~~~
[參考視頻動畫鏈接](https://www.bilibili.com/video/BV1b5411V7i3?t=479)

>[danger] ##### 代碼實現部分
~~~
function updateChildren (parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue) {
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: KeyToIndexMap | undefined
let idxInOld: number
let elmToMove: VNode
let before: any
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 索引變化后,可能會把節點設置為空
if (oldStartVnode == null) {
// 節點為空移動索引
oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
// 比較開始和結束節點的四種情況
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 1. 比較老開始節點和新開始節點
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 2.比較老結束節點和新的結束節點
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 3.比較老開始節點和新的結束節點
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// 4.比較老結束節點和新的開始節點
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 開始節點和結束節點都不相同
// 使用newStartNode 的key在老節點數組中找相同節點
// 先設置記錄 key 和index對象
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
// 遍歷 newStartVnode,從舊節點中找相同key的oldVnode的索引
idxInOld = oldKeyToIdx[newStartVnode.key as string]
// 如果是新的vnode
if (isUndef(idxInOld)) { // New element
// 如果沒找到,newStartNode 是新節點
// 創建元素插入DOM樹
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
// 如果找到相同key的老節點,記錄到elmToMove遍歷
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
// 如果新舊節點的選擇器不同
// 創建新開始節點對應的DOM元素,插入到DOM樹種
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
// 如果相同,patchVnode()
// 把elmToMove 對應的DOM元素,移動到左邊
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined as any
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
}
}
// 重新給newStartVnode 復制,指向下一個新節點
newStartVnode = newCh[++newStartIdx]
}
}
// 循環結束,老節點數組先遍歷完成或者新節點數組先遍歷完成
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
// 如果老節點數組先遍歷完成,說明有新的節點剩余
// 把剩余的新節點都插入到右邊
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else {
// 如果新節點數組先遍歷完成,說明老節點有剩余
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
}
~~~
>[danger] ##### 虛擬dom中key的好處
~~~
Key 是用來優化 Diff 算法的。Diff算法核心在于同層次節點比較,Key 就是用于在比較同層次新、舊節點時,
判斷其是否相同。
Key 一般用于生成一列同類型節點時使用,這種情況下,當修改這些同類型節點的某個內容、變更位置、刪除、
添加等時,此時界面需要更新視圖,Vue 會調用 patch 方法通過對比新、舊節點的變化來更新視圖。
其從根節點開始若新、舊 VNode 相同,則調用 patchVnode
patchVnode 中若新節點沒有文本,且新節點和舊節點都有有子節點,則需對子節點進行 Diff 操作,即調用 updateChildren,Key 就在 updateChildren 起了大作用
updateChildren 中會遍歷對比上步中的新、舊節點的子節點,并按 Diff 算法通過 sameVnode 來判斷要對比
的節點是否相同
若這里的子節點未設置 Key,則此時的每個新、舊子節點在執行 sameVnode 時會判定相同,然后再次執行一次 patchVnode 來對比這些子節點的子節點
若設置了 Key,當執行 sameVnode
若 Key 不同 sameVnode 返回 false,然后執行后續判斷;
若 Key 相同 sameVnode 返回 true,然后再執行 patchVnode 來對比這些子節點的子節點
即,使用了 Key 后,可以優化新、舊節點的對比判斷,減少了遍歷子節點的層次,少使用很多次 patchVnode
~~~
- Vue--基礎篇章
- Vue -- 介紹
- Vue -- MVVM
- Vue -- 創建Vue實例
- Vue -- 模板語法
- Vue -- 指令用法
- v-cloak -- 遮蓋
- v-bind -- 標簽屬性動態綁定
- v-on -- 綁定事件
- v-model -- 雙向數據綁定
- v-for -- 只是循環沒那么簡單
- 小知識點 -- 計劃內屬性
- key -- 屬性為什么要加
- 案例說明
- v-if/v-show -- 顯示隱藏
- v-for 和 v-if 同時使用
- v-pre -- 不渲染大大胡語法
- v-once -- 只渲染一次
- Vue -- class和style綁定
- Vue -- filter 過濾器
- Vue--watch/computed/fun
- watch -- 巧妙利用watch思想
- Vue -- 自定義指令
- Vue -- $方法
- Vue--生命周期
- Vue -- 專屬ajax
- Vue -- transition過渡動畫
- 前面章節的案例
- 案例 -- 跑馬燈效果
- 案例 -- 選項卡內容切換
- 案例-- 篩選商品
- 案例 -- 搜索/刪除/更改
- 案例 -- 用computed做多選
- 案例 -- checked 多選
- Vue--組件篇章
- component -- 介紹
- component -- 使用全局組件
- component -- 使用局部組件
- component -- 組件深入
- component -- 組件傳值父傳子
- component -- 組件傳值子傳父
- component -- 子傳父語法糖拆解
- component -- 父組件操作子組件
- component -- is 動態切換組件
- component -- 用v-if/v-show控制子組件
- component -- 組件切換的動畫效果
- component -- slot 插槽
- component -- 插槽2.6
- component -- 組件的生命周期
- component -- 基礎組件全局注冊
- VueRouter--獲取路由參數
- VueRouter -- 介紹路由
- VueRouter -- 安裝
- VueRouter -- 使用
- VueRouter--router-link簡單參數
- VueRouter--router-link樣式問題
- VueRouter--router-view動畫效果
- VueRouter -- 匹配優先級
- vueRouter -- 動態路由
- VueRouter -- 命名路由
- VueRouter -- 命名視圖
- VueRouter--$router 獲取函數
- VueRouter--$route獲取參數
- VueRouter--路由嵌套
- VueRouter -- 導航守衛
- VueRouter -- 寫在最后
- Vue--模塊化方式結構
- webpack--自定義配置
- webpack -- 自定義Vue操作
- VueCli -- 3.0可視化配置
- VueCli -- 3.0 項目目錄
- Vue -- 組件升級篇
- Vue -- 組件種類與組件組成
- Vue -- 組件prop、event、slot 技巧
- Vue -- 組件通信(一)
- Vue -- 組件通信(二)
- Vue -- 組件通信(三)
- Vue -- 組件通信(四)
- Vue -- 組件通信(五)
- Vue -- 組件通信(六)
- Vue -- bus非父子組件通信
- Vue -- 封裝js插件成vue組件
- vue組件分裝 -- 進階篇
- Vue -- 組件封裝splitpane(分割面板)
- UI -- 正式封裝
- Vue -- iview 可編輯表格案例
- Ui -- iview 可以同時編輯多行
- Vue -- 了解遞歸組件
- UI -- 正式使用遞歸菜單
- Vue -- iview Tree組件
- Vue -- 利用通信仿寫一個form驗證
- Vue -- 使用自己的Form
- Vue -- Checkbox 組件
- Vue -- CheckboxGroup.vue
- Vue -- Alert 組件
- Vue -- 手動掛載組件
- Vue -- Alert開始封裝
- Vue -- 動態表單組件
- Vue -- Vuex組件的狀態管理
- Vuex -- 參數使用理解
- Vuex -- state擴展
- Vuex -- getters擴展
- Vuex--mutations擴展
- Vuex -- Action 異步
- Vuex -- plugins插件
- Vuex -- v-model寫法
- Vuex -- 更多
- VueCli -- 技巧總結篇
- CLI -- 路由基礎
- CLI -- 路由升級篇
- CLI --異步axios
- axios -- 封裝axios
- CLI -- 登錄寫法
- CLI -- 權限
- CLI -- 簡單權限
- CLI -- 動態路由加載
- CLI -- 數據性能優化
- ES6 -- 類的概念
- ES6類 -- 基礎
- ES6 -- 繼承
- ES6 -- 工作實戰用類數據管理
- JS -- 適配器模式
- ES7 -- 裝飾器(Decorator)
- 裝飾器 -- 裝飾器修飾類
- 裝飾器--修飾類方法(知識擴展)
- 裝飾器 -- 裝飾器修飾類中的方法
- 裝飾器 -- 執行順序
- Reflect -- es6 自帶版本
- Reflect -- reflect-metadata 版本
- 實戰 -- 驗證篇章(基礎)
- 驗證篇章 -- 搭建和目錄
- 驗證篇章 -- 創建基本模板
- 驗證篇章 -- 使用
- 實戰 -- 更新模型(為了迎合ui升級)
- 實戰 -- 模型與接口對接
- TypeSprict -- 基礎篇章
- TS-- 搭建(一)webpack版本
- TS -- 搭建(二)直接使用
- TS -- 基礎類型
- TS -- 枚舉類型
- TS -- Symbol
- TS -- interface 接口
- TS -- 函數
- TS -- 泛型
- TS -- 類
- TS -- 類型推論和兼容
- TS -- 高級類型(一)
- TS -- 高級類型(二)
- TS -- 關于模塊解析
- TS -- 聲明合并
- TS -- 混入
- Vue -- TS項目模擬
- TS -- vue和以前代碼對比
- TS -- vue簡單案例上手
- Vue -- 簡單弄懂VueRouter過程
- VueRouter -- 實現簡單Router
- Vue-- 原理2.x源碼簡單理解
- 了解 -- 簡單的響應式工作原理
- 準備工作 -- 了解發布訂閱和觀察者模式
- 了解 -- 響應式工作原理(一)
- 了解 -- 響應式工作原理(二)
- 手寫 -- 簡單的vue數據響應(一)
- 手寫 -- 簡單的vue數據響應(二)
- 模板引擎可以做的
- 了解 -- 虛擬DOM
- 虛擬dom -- 使用Snabbdom
- 閱讀 -- Snabbdom
- 分析snabbdom源碼 -- h函數
- 分析snabbdom -- init 方法
- init 方法 -- patch方法分析(一)
- init 方法 -- patch方法分析(二)
- init方法 -- patch方法分析(三)
- 手寫 -- 簡單的虛擬dom渲染
- 函數表達解析 - h 和 create-element
- dom操作 -- patch.js
- Vue -- 完成一個minVue
- minVue -- 打包入口
- Vue -- new實例做了什么
- Vue -- $mount 模板編譯階段
- 模板編譯 -- 分析入口
- 模板編譯 -- 分析模板轉譯
- Vue -- mountComponent 掛載階段
- 掛載階段 -- vm._render()
- 掛載階段 -- vnode
- 備份章節
- Vue -- Nuxt.js
- Vue3 -- 學習
- Vue3.x --基本功能快速預覽
- Vue3.x -- createApp
- Vue3.x -- 生命周期
- Vue3.x -- 組件
- vue3.x -- 異步組件???
- vue3.x -- Teleport???
- vue3.x -- 動畫章節 ??
- vue3.x -- 自定義指令 ???
- 深入響應性原理 ???
- vue3.x -- Option API VS Composition API
- Vue3.x -- 使用set up
- Vue3.x -- 響應性API
- 其他 Api 使用
- 計算屬性和監聽屬性
- 生命周期
- 小的案例(一)
- 小的案例(二)-- 泛型
- Vue2.x => Vue3.x 導讀
- v-for 中的 Ref 數組 -- 非兼容
- 異步組件
- attribute 強制行為 -- 非兼容
- $attrs 包括 class & style -- 非兼容
- $children -- 移除
- 自定義指令 -- 非兼容
- 自定義元素交互 -- 非兼容
- Data選項 -- 非兼容
- emits Option -- 新增
- 事件 API -- 非兼容
- 過濾器 -- 移除
- 片段 -- 新增
- 函數式組件 -- 非兼容
- 全局 API -- 非兼容
- 全局 API Treeshaking -- 非兼容
- 內聯模板 Attribute -- 非兼容
- key attribute -- 非兼容
- 按鍵修飾符 -- 非兼容
- 移除 $listeners 和 v-on.native -- 非兼容
- 在 prop 的默認函數中訪問 this -- ??
- 組件使用 v-model -- 非兼容
- 渲染函數 API -- ??
- Slot 統一 ??
- 過渡的 class 名更改 ???
- Transition Group 根元素 -- ??
- v-if 與 v-for 的優先級對比 -- 非兼容
- v-bind 合并行為 非兼容
- 監聽數組 -- 非兼容