一文帶你徹底搞定Diff算法
一、基礎(chǔ)
Diff算法實(shí)現(xiàn)的是最小量更新虛擬DOM。這句話雖然簡短,但是涉及到了兩個(gè)核心要素:虛擬DOM、最小量更新。
1.虛擬DOM
虛擬DOM指的就是將真實(shí)的DOM樹構(gòu)造為js對(duì)象的形式,從而解決瀏覽器操作真實(shí)DOM的性能問題。
例如:如下DOM與虛擬DOM之間的映射關(guān)系
2.最小量更新
Diff的用途就是在新老虛擬DOM之間找到最小更新的部分,從而將該部分對(duì)應(yīng)的DOM進(jìn)行更新。
二、整個(gè)流程
Diff算法真的很美,整個(gè)流程如下圖所示:
- 首先比較一下新舊節(jié)點(diǎn)是不是同一個(gè)節(jié)點(diǎn)(可通過比較sel(選擇器)和key(唯一標(biāo)識(shí))值是不是相同),不是同一個(gè)節(jié)點(diǎn)則進(jìn)行暴力刪除(注:先以舊節(jié)點(diǎn)為基準(zhǔn)插入新節(jié)點(diǎn),然后再刪除舊節(jié)點(diǎn))。
- 若是同一個(gè)節(jié)點(diǎn)則需要進(jìn)一步比較
完全相同,不做處理
新節(jié)點(diǎn)內(nèi)容為文本,直接替換完事
新節(jié)點(diǎn)有子節(jié)點(diǎn),這個(gè)時(shí)候就要仔細(xì)考慮一下了:若老節(jié)點(diǎn)沒有子元素,則直接清空老節(jié)點(diǎn),將新節(jié)點(diǎn)的子元素插入即可;若老節(jié)點(diǎn)有子元素則就需要按照上述的更新策略老搞定了(記住更新策略,又可以吹好幾年了,666666)。
三、實(shí)戰(zhàn)
光說不練假把式,下面直接輸出diff算法的核心內(nèi)容。
3.1 patch函數(shù)
Diff算法的入口函數(shù),主要判斷新舊節(jié)點(diǎn)是不是同一個(gè)節(jié)點(diǎn),然后交由不同的邏輯進(jìn)行處理。
- export default function patch(oldVnode, newVnode) {
- // 判斷傳入的第一個(gè)參數(shù),是DOM節(jié)點(diǎn)還是虛擬節(jié)點(diǎn)
- if (oldVnode.sel === '' || oldVnode.sel === undefined) {
- // 傳入的第一個(gè)參數(shù)是DOM節(jié)點(diǎn),此時(shí)要包裝成虛擬節(jié)點(diǎn)
- oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
- }
- // 判斷oldVnode和newVnode是不是同一個(gè)節(jié)點(diǎn)
- if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
- //是同一個(gè)節(jié)點(diǎn),則進(jìn)行精細(xì)化比較
- patchVnode(oldVnode, newVnode);
- }
- else {
- // 不是同一個(gè)節(jié)點(diǎn),暴力插入新的,刪除舊的
- let newVnodeElm = createElement(newVnode);
- // 將新節(jié)點(diǎn)插入到老節(jié)點(diǎn)之前
- if (oldVnode.elm.parentNode && newVnodeElm) {
- oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
- }
- // 刪除老節(jié)點(diǎn)
- oldVnode.elm.parentNode.removeChild(oldVnode.elm);
- }
- }
3.2 patchVnode函數(shù)
該函數(shù)主要負(fù)責(zé)精細(xì)化比較,通過按照上述流程圖中的邏輯依次判斷屬于哪一個(gè)分支,從而采取不同的處理邏輯。(思路清晰,算法太牛了)
- export default function patchVnode(oldVnode, newVnode) {
- // 判斷新舊vnode是否是同一個(gè)對(duì)象
- if (oldVnode === newVnode) {
- return;
- }
- // 判斷vnode有沒有text屬性
- if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {
- console.log('新vnode有text屬性');
- if (newVnode.text !== oldVnode.text) {
- oldVnode.elm.innerText = newVnode.text;
- }
- }
- else {
- // 新vnode沒有text屬性,有children
- console.log('新vnode沒有text屬性');
- // 判斷老的有沒有children
- if (oldVnode.children !== undefined && oldVnode.children.length > 0) {
- // 老的有children,新的也有children
- updateChildren(oldVnode.elm, oldVnode.children, newVnode.children);
- }
- else {
- // 老的沒有children,新的有children
- // 清空老的節(jié)點(diǎn)的內(nèi)容
- oldVnode.elm.innerHTML = '';
- // 遍歷新的vnode的子節(jié)點(diǎn),創(chuàng)建DOM,上樹
- for (let i = 0; i < newVnode.children.length; i++) {
- let dom = createElement(newVnode.children[i]);
- oldVnode.elm.appendChild(dom);
- }
- }
- }
- }
3.3 updateChildren函數(shù)
核心函數(shù),主要負(fù)責(zé)舊虛擬節(jié)點(diǎn)和新虛擬節(jié)點(diǎn)均存在子元素的情況,按照比較策略依次進(jìn)行比較,最終找出子元素中變化的部分,實(shí)現(xiàn)最小更新。對(duì)于該部分,涉及到一些指針,如下所示:
- 舊前指的就是更新前虛擬DOM中的頭部指針
- 舊后指的就是更新前虛擬DOM中的尾部指針
- 新前指的就是更新后虛擬DOM中的頭部指針
- 新后指的就是更新后虛擬DOM中的尾部指針
按照上述的更新策略,上述舊虛擬DOM更新為新虛擬DOM的流程為:
- 命中“新后舊前”策略,然后將信后對(duì)應(yīng)的DOM節(jié)點(diǎn)(也就是節(jié)點(diǎn)1)移動(dòng)到舊后節(jié)點(diǎn)(節(jié)點(diǎn)3)后面,然后舊前節(jié)點(diǎn)指針下移,新后節(jié)點(diǎn)指針上移。
- 仍然命中“新后舊前”策略,做相同的操作,將節(jié)點(diǎn)2移動(dòng)到舊后節(jié)點(diǎn)(節(jié)點(diǎn)3)后面,下移舊前節(jié)點(diǎn),上移新后節(jié)點(diǎn)。
- 命中“新前舊前”策略,DOM節(jié)點(diǎn)不變,舊前和新前節(jié)點(diǎn)均下移。
- 跳出循環(huán),移動(dòng)結(jié)束。
- export default function updateChildren(parentElm, oldCh, newCh) {
- // 舊前
- let oldStartIdx = 0;
- // 新前
- let newStartIdx = 0;
- // 舊后
- let oldEndIdx = oldCh.length - 1;
- // 新后
- let newEndIdx = newCh.length - 1;
- // 舊前節(jié)點(diǎn)
- let oldStartVnode = oldCh[0];
- // 舊后節(jié)點(diǎn)
- let oldEndVnode = oldCh[oldEndIdx];
- // 新前節(jié)點(diǎn)
- let newStartVnode = newCh[0];
- // 新后節(jié)點(diǎn)
- let newEndVnode = newCh[newEndIdx];
- let keyMap = null;
- while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
- // 略過已經(jīng)加undefined標(biāo)記的內(nèi)容
- if (oldStartVnode == null || oldCh[oldStartIdx] === undefined) {
- oldStartVnode = oldCh[++oldStartIdx];
- }
- else if (oldEndVnode == null || oldCh[oldEndIdx] === undefined) {
- oldEndVnode = oldCh[--oldEndIdx];
- }
- else if (newStartVnode == null || newCh[newStartIdx] === undefined) {
- newStartVnode = newCh[++newStartIdx];
- }
- else if (newEndVnode == null || newCh[newEndIdx] === undefined) {
- newEndVnode = newCh[--newEndIdx];
- }
- else if (checkSameVnode(oldStartVnode, newStartVnode)) {
- // 新前與舊前
- console.log('新前與舊前命中');
- patchVnode(oldStartVnode, newStartVnode);
- oldStartVnode = oldCh[++oldStartIdx];
- newStartVnode = newCh[++newStartIdx];
- }
- else if (checkSameVnode(oldEndVnode, newEndVnode)) {
- // 新后和舊后
- console.log('新后和舊后命中');
- patchVnode(oldEndVnode, newEndVnode);
- oldEndVnode = oldCh[--oldEndIdx];
- newEndVnode = newCh[--newEndVnode];
- }
- else if (checkSameVnode(oldStartVnode, newEndVnode)) {
- console.log('新后和舊前命中');
- patchVnode(oldStartVnode, newEndVnode);
- // 當(dāng)新后與舊前命中的時(shí)候,此時(shí)要移動(dòng)節(jié)點(diǎn),移動(dòng)新后指向的這個(gè)節(jié)點(diǎn)到老節(jié)點(diǎn)舊后的后面
- parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
- oldStartVnode = oldCh[++oldStartIdx];
- newEndVnode = newCh[--newEndIdx];
- }
- else if (checkSameVnode(oldEndVnode, newStartVnode)) {
- // 新前和舊后
- console.log('新前和舊后命中');
- patchVnode(oldEndVnode, newStartVnode);
- // 當(dāng)新前和舊后命中的時(shí)候,此時(shí)要移動(dòng)節(jié)點(diǎn),移動(dòng)新前指向的這個(gè)節(jié)點(diǎn)到老節(jié)點(diǎn)舊前的前面
- parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
- oldEndVnode = oldCh[--oldEndIdx];
- newStartVnode = newCh[++newStartIdx];
- }
- else {
- // 四種都沒有命中
- // 制作keyMap一個(gè)映射對(duì)象,這樣就不用每次都遍歷老對(duì)象了
- if (!keyMap) {
- keyMap = {};
- for (let i = oldStartIdx; i <= oldEndIdx; i++) {
- const key = oldCh[i].key;
- if (key !== undefined) {
- keyMap[key] = i;
- }
- }
- }
- // 尋找當(dāng)前這項(xiàng)(newStartIdx)在keyMap中的映射的位置序號(hào)
- const idxInOld = keyMap[newStartVnode.key];
- if (idxInOld === undefined) {
- // 如果idxInOld是undefined表示踏實(shí)全新的項(xiàng),此時(shí)會(huì)將該項(xiàng)創(chuàng)建為DOM節(jié)點(diǎn)并插入到舊前之前
- parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
- }
- else {
- // 如果不是undefined,則不是全新的項(xiàng),則需要移動(dòng)
- const elmToMove = oldCh[idxInOld];
- patchVnode(elmToMove, newStartVnode);
- // 把這項(xiàng)設(shè)置為undefined,表示已經(jīng)處理完這項(xiàng)了
- oldCh[idxInOld] = undefined;
- // 移動(dòng)
- parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
- }
- // 指針下移,只移動(dòng)新的頭
- newStartVnode = newCh[++newStartIdx];
- }
- }
- // 循環(huán)結(jié)束后,處理未處理的項(xiàng)
- if (newStartIdx <= newEndIdx) {
- console.log('new還有剩余節(jié)點(diǎn)沒有處理,要加項(xiàng),把所有剩余的節(jié)點(diǎn)插入到oldStartIdx之前');
- // 遍歷新的newCh,添加到老的沒有處理的之前
- for (let i = newStartIdx; i <= newEndIdx; i++) {
- // insertBefore方法可以自動(dòng)識(shí)別null,如果是null就會(huì)自動(dòng)排到隊(duì)尾去
- // newCh[i]現(xiàn)在還沒有真正的DOM,所以要調(diào)用createElement函數(shù)變?yōu)镈OM
- parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm);
- }
- }
- else if (oldStartIdx <= oldEndIdx) {
- console.log('old還有剩余節(jié)點(diǎn)沒有處理,要?jiǎng)h除項(xiàng)');
- // 批量刪除oldStart和oldEnd指針之間的項(xiàng)
- for (let i = oldStartIdx; i <= oldEndIdx; i++) {
- if (oldCh[i]) {
- parentElm.removeChild(oldCh[i].elm);
- }
- }
- }
- }
【編輯推薦】