diff.js 列表對比演算法 原始碼分析
diff.js列表對比演算法 原始碼分析
npm上的程式碼可以檢視 (https://www.npmjs.com/package/list-diff2) 原始碼如下:
1 /** 2 * 3 * @param {Array} oldList 原始列表 4 * @param {Array} newList 新列表 5 * @param {String} key 鍵名稱 6 * @return {Object} {children: [], moves: [] } 7 * children 是源列表 根據 新列表返回 移動的新資料,比如 oldList = [{id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 5}, {id: 6}];View Code8 newList = [{id: 2}, {id: 3}, {id: 1}]; 最後返回的children = [ 9 {id: 1}, 10 {id: 2}, 11 {id: 3}, 12 null, 13 null, 14 null 15 ] 16 moves 是源列表oldList 根據新列表newList 返回的操作,children為null的話,依次刪除掉掉,因此返回的是 17 moves = [ 18 {type: 0, index:3}, 19 {type: 0, index: 3}, 20 {type: 0, index: 3},21 {type: 0, index: 0}, 22 {type: 1, index: 2, item: {id: 1}} 23 ] 24 注意:type = 0 是刪除操作, type = 1 是新增操作 25 */ 26 function diff(oldList, newList, key) { 27 var oldMap = makeKeyIndexAndFree(oldList, key); 28 var newMap = makeKeyIndexAndFree(newList, key); 29 var newFree = newMap.free;30 31 var oldKeyIndex = oldMap.keyIndex; 32 var newKeyIndex = newMap.keyIndex; 33 34 var moves = []; 35 var children = []; 36 var i = 0; 37 var freeIndex = 0; 38 var item; 39 var itemKey; 40 41 while(i < oldList.length) { 42 item = oldList[i]; 43 itemKey = getItemKey(item, key); 44 if(itemKey) { 45 if(!newKeyIndex.hasOwnProperty(itemKey)) { 46 children.push(null); 47 } else { 48 var newItemIndex = newKeyIndex[itemKey]; 49 children.push(newList[newItemIndex]); 50 } 51 } else { 52 var freeItem = newFree[freeIndex++]; 53 children.push(freeItem || null); 54 } 55 i++; 56 } 57 // 刪除不存在的項 58 var simulateList = children.slice(0); 59 i = 0; 60 while (i < simulateList.length) { 61 if (simulateList[i] === null) { 62 remove(i); 63 // 呼叫該方法執行刪除 64 removeSimulate(i); 65 } else { 66 i++; 67 } 68 } 69 70 // 71 var j = i = 0; 72 while (i < newList.length) { 73 item = newList[i]; 74 itemKey = getItemKey(item, key); 75 76 var simulateItem = simulateList[j]; 77 var simulateItemKey = getItemKey(simulateItem, key); 78 if (simulateItem) { 79 if (itemKey === simulateItemKey) { 80 j++; 81 } else { 82 // 新的一項,插入 83 if (!oldKeyIndex.hasOwnProperty(itemKey)) { 84 insert(i, item); 85 } else { 86 var nextItemKey = getItemKey(simulateList[j + 1], key); 87 if (nextItemKey === itemKey) { 88 remove(i); 89 removeSimulate(j); 90 j++; 91 } else { 92 insert(i, item); 93 } 94 } 95 } 96 } else { 97 insert(i, item); 98 } 99 i++; 100 } 101 102 function remove(index) { 103 var move = {index: index, type: 0}; 104 moves.push(move); 105 } 106 107 function insert(index, item) { 108 var move = {index: index, item: item, type: 1}; 109 moves.push(move); 110 } 111 112 function removeSimulate(index) { 113 simulateList.splice(index, 1); 114 } 115 return { 116 moves: moves, 117 children: children 118 } 119 } 120 /* 121 * 列表轉化為 keyIndex 物件 122 * 比如如下程式碼: 123 var list = [{key: 'id1'}, {key: 'id2'}, {key: 'id3'}, {key: 'id4'}] 124 var map = diff.makeKeyIndexAndFree(list, 'key'); 125 console.log(map); 126 // { 127 keyIndex: {id1: 0, id2: 1, id3: 2, id4: 3}, 128 free: [] 129 } 130 * @param {Array} list 131 * @param {String|Function} key 132 */ 133 function makeKeyIndexAndFree(list, key) { 134 var keyIndex = {}; 135 var free = []; 136 for (var i = 0, len = list.length; i < len; i++) { 137 var item = list[i]; 138 var itemKey = getItemKey(item, key); 139 if (itemKey) { 140 keyIndex[itemKey] = i; 141 } else { 142 free.push(item); 143 } 144 } 145 return { 146 keyIndex: keyIndex, 147 free: free 148 } 149 } 150 151 function getItemKey(item, key) { 152 if (!item || !key) { 153 return; 154 } 155 return typeof key === 'string' ? item[key] : key[item] 156 } 157 exports.makeKeyIndexAndFree = makeKeyIndexAndFree; 158 exports.diff = diff;
該js的作用是:深度遍歷兩個列表資料,每層的節點進行對比,記錄下每個節點的差異。並返回該物件的差異。
@return {Object} {children: [], moves: [] }
children 是源列表 根據 新列表返回 移動或新增的資料。
比如
oldList = [{id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 5}, {id: 6}];
newList = [{id: 2}, {id: 3}, {id: 1}];
最後返回的
children = [ {id: 1}, {id: 2}, {id: 3}, null, null, null ]
moves 是源列表oldList 根據新列表newList 返回的操作,children為null的話,依次刪除掉掉,因此返回的是
moves = [ {type: 0, index:3}, {type: 0, index: 3}, {type: 0, index: 3}, {type: 0, index: 0}, {type: 1, index: 2, item: {id: 1}} ]
注意:type = 0 是刪除操作, type = 1 是新增操作
因為
oldList = [{id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 5}, {id: 6}];
newList = [{id: 2}, {id: 3}, {id: 1}];
所以oldList根據newList來對比,{id: 4} 和 {id: 5} 和 {id: 6} 在新節點 newList沒有找到,因此在moves設定為 {type:0, index:3},
所以oldList資料依次變為 [{id: 1}, {id: 2}, {id: 3}, {id: 5}, {id: 6}] 和 [{id: 1}, {id: 2}, {id: 3}, {id: 6}] 和 [{id: 1}, {id: 2}, {id: 3}]
每次在moves儲存了一次的話,原陣列會刪掉當前的一項,因此oldList 變為 [{id: 1}, {id: 2}, {id: 3}], newList 為 [{id: 2}, {id: 3}, {id: 1}],
然後各自取出該值進行比較,也就是 oldList變為 [1, 2, 3], newList變為 [2, 3, 1]; 因此oldList相對於 newList來講的話,第一項不相同就刪掉該項 所以moves新增一項{type: 0, index:0}, index從0開始的,表示第一項被刪除,然後第二項1被新增,因此moves再加一項 {type: 1, index:2, item: {id: 1}};
程式碼理解如下:
該方法需要傳入三個引數 oldLsit, newList, key;
oldList 和 newList 是原始陣列 和 新陣列, key是根據鍵名進行匹配。
現在分別對oldList 和 newList 傳值如下資料:
var oldLsit = [{id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 5}, {id: 6}];
var newList = [{id: 2}, {id: 3}, {id: 1}];
因此 var oldMap = makeKeyIndexAndFree(oldList, key);
makeKeyIndexAndFree程式碼如下:
function makeKeyIndexAndFree(list, key) { var keyIndex = {}; var free = []; for (var i = 0, len = list.length; i < len; i++) { var item = list[i]; var itemKey = getItemKey(item, key); if (itemKey) { keyIndex[itemKey] = i; } else { free.push(item); } } return { keyIndex: keyIndex, free: free } }
getItemKey 程式碼如下:
function getItemKey(item, key) { if (!item || !key) { return; } return typeof key === 'string' ? item[key] : key[item] }
執行程式碼變成如下:
var oldMap = { keyIndex: { 1: 0, 2: 1, 3: 2, 4: 3, 5: 4, 6: 5 }, free: [] } var newMap = makeKeyIndexAndFree(newList, key); 輸出如下: var newMap = { free: [], keyIndex: { 1: 2, 2: 0, 3: 1 } }
注意:上面的是把{id: xx} 中的xx當做鍵, 但是當xx是數字的話,他會把數字當做索引位置來儲存。
var newFree = newMap.free = []; var oldKeyIndex = oldMap.keyIndex; var newKeyIndex = newMap.keyIndex; var moves = []; var children = []; var i = 0; var freeIndex = 0; var item; var itemKey; while(i < oldList.length) { item = oldList[i]; itemKey = getItemKey(item, key); if(itemKey) { if(!newKeyIndex.hasOwnProperty(itemKey)) { children.push(null); } else { var newItemIndex = newKeyIndex[itemKey]; children.push(newList[newItemIndex]); } } else { var freeItem = newFree[freeIndex++]; children.push(freeItem || null); } i++; }
while迴圈舊節點oldList,獲取其某一項,比如 {id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 5}, {id: 6}, 然後根據鍵名獲取某一項的值,分別為:1,2,3,4,5,6。
然後判斷 新節點中的 newKeyIndex 是否有該屬性鍵名,newKeyIndex = {1: 2, 2: 0, 3: 1}, 判斷newKeyIndex 是否有屬性 1, 2, 3, 4, 5, 6, 如果沒有的話,把null放到children數組裡面去,如果有的話,存入children數組裡面去,因此children的值變為如下:
children = [ {id: 1}, {id: 2}, {id: 3}, null, null, null ]; // 刪除不存在的項 var simulateList = children.slice(0); i = 0; while (i < simulateList.length) { if (simulateList[i] === null) { remove(i); // 呼叫該方法執行刪除 removeSimulate(i); } else { i++; } }
把children陣列的值賦值到 simulateList列表中,如果某一項等於null的話,呼叫 remove(i)方法,把null值以物件的形式儲存到moves數組裡面去,
同時刪除simulateList列表中的null資料。
程式碼如下:
function remove(index) { var move = {index: index, type: 0}; moves.push(move); } function removeSimulate(index) { simulateList.splice(index, 1); } simulateList 資料變成如下: simulateList = [ {id: 1}, {id: 2}, {id: 3} ];
因此 moves 變成如下資料:
var moves = [ {index: 3, type: 0}, {index: 3, type: 0}, {index: 3, type: 0} ];
再執行如下程式碼:
var j = i = 0; while (i < newList.length) { item = newList[i]; itemKey = getItemKey(item, key); var simulateItem = simulateList[j]; var simulateItemKey = getItemKey(simulateItem, key); if (simulateItem) { if (itemKey === simulateItemKey) { j++; } else { // 新的一項,插入 if (!oldKeyIndex.hasOwnProperty(itemKey)) { insert(i, item); } else { var nextItemKey = getItemKey(simulateList[j + 1], key); if (nextItemKey === itemKey) { remove(i); removeSimulate(j); j++; } else { insert(i, item); } } } } else { insert(i, item); } i++; }
遍歷新節點資料newList var newList = [{id: 2}, {id: 3}, {id: 1}]; 然後 itemKey = getItemKey(item, key); 那麼itemKey=2, 3, 1
var simulateItem = simulateList[j];
simulateList的值如下:
simulateList = [ {id: 1}, {id: 2}, {id: 3} ];
獲取simulateList陣列中的某一項,然後
var simulateItemKey = getItemKey(simulateItem, key);
因此 simulateItemKey值依次變為1, 2, 3; 先迴圈最外層的 新資料 2, 3,1,然後在迴圈內層 舊資料 1, 2 ,3,
判斷 itemKey === simulateItemKey 是否相等,相等的話 什麼都不做, 執行下一次迴圈,j++; 否則的話,先判斷是否在舊節點oldKeyIndex
能否找到新節點的值;oldKeyIndex 資料如下:
{ 1: 0, 2: 1, 3: 2, 4: 3, 5: 4, 6: 5 }
如果沒有找到該鍵名的話,說明該新節點資料項就是新增的,那就新增一項,新增的程式碼如下:
function insert(index, item) { var move = {index: index, item: item, type: 1}; moves.push(move); }
因此moves程式碼繼續新增一項,type為1就是新增的。否則的話,獲取simulateList中的下一個資料值,進行對比,如果能找到的話,執行remove(i)方法,因此moves再新加一項
{type:0, index: i}; 此時 j = 0; 刪除原陣列的第一項,然後繼續迴圈上面一樣的操作。
整個思路重新整理一遍:
var before = [{id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 5}, {id: 6}];
var after = [{id: 4}, {id: 3}, {id: 2},{id: 1}];
var diffs = diff.diff(before, after, 'id');
上面的程式碼初始化,原資料 before, 新資料 after,key鍵為id,
oldMap 值為:
oldMap = { keyIndex: { 1: 0, 2: 1, 3: 2, 4: 3, 5: 4, 6: 5 } }
newMap的值為
newMap = { keyIndex: { 1: 3, 2: 2, 3: 1, 4: 0 } }
oldKeyIndex = oldMap.keyIndex = { 1: 0, 2: 1, 3: 2, 4: 3, 5: 4, 6: 5 } var newKeyIndex = newMap.keyIndex = { 1: 3, 2: 2, 3: 1, 4: 0 };
遍歷 before,獲取某一項的值,因此分別為1,2,3,4,5,6;判斷newKeyIndex是否有該值,如果沒有的話,該它置為null,儲存到 children數組裡面去;
因此
children = [ {id: 1}, {id: 2}, {id: 3}, {id: 4}, null, null ]
把children賦值到 simulateList 數組裡面去,然後對simulateList陣列去掉null值,因此simulateList值變為如下:
simulateList = [ {id: 1}, {id: 2}, {id: 3}, {id: 4} ] moves = [ { type: 0, index: 4 }, { type: 0, index: 4 } ]
最後遍歷新節點 newList = [{id: 4}, {id: 3}, {id: 2},{id: 1}]; 獲取該鍵值分別為:4, 3, 2, 1;
獲取源陣列simulateList裡面的鍵值為 1, 2 , 3, 4;
所以 4, 3, 2, 1 遍歷 和 1, 2, 3, 4 遍歷判斷是否相等思路如下:
1. 遍歷newList鍵值 為 4, 先和 1比較,如果相等的話,j++,跳到下一個內部迴圈,否則的話,先判斷該鍵是否在oldKeyIndex裡面,如果不存在的話,說明是新增的,否則的話就進入else語句,判斷simulateList下一個值2 是否和 4 相等,不相等的話,直接插入值到陣列的第一個位置上去,因此 moves的值變為如下:
moves = [ { type: 0, index: 4 }, { type: 0, index: 4 }, { type: 1, index: 0, item: {id: 4} } ]
2. 同樣的道理 ,把 遍歷newList的第二項 3, 和第一步一樣的操作,最後3也是新增的,如下moves的值變為如下:
moves = [ { type: 0, index: 4 }, { type: 0, index: 4 }, { type: 1, index: 0, item: {id: 4} }, { type: 1, index: 1, item: {id: 3} } ]
3. 同樣,遍歷newList的第三項值為2, 和第一步操作,進入else語句,第一個值不符合,接著遍歷第二個值,相等,就做刪除操作,因此moves變為如下值:
moves = [ { type: 0, index: 4 }, { type: 0, index: 4 }, { type: 1, index: 0, item: {id: 4} }, { type: 1, index: 1, item: {id: 3} }, { type: 0, index: 2 } ]
且 oldList被刪除一項,此時j = 0, 所以被刪除掉第一項 因此 oldList = [2, 3, 4];
4. 同樣,遍歷 newList的第四項值為 1, 和第一步操作一樣,值都不相等,因此做插入操作,因此moves值變為
moves = [ { type: 0, index: 4 }, { type: 0, index: 4 }, { type: 1, index: 0, item: {id: 4} }, { type: 1, index: 1, item: {id: 3} }, { type: 0, index: 2 }, { type: 1, index: 3, item: {id: 1} } ]
最後以物件的方式 返回 moves 和 children。