diff.js 列表對比演算法 原始碼分析

龍恩0707發表於2017-09-11

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}];
  8  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;
View Code

該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。

相關文章