如何實現一個 Virtual DOM 及原始碼分析

龍恩0707發表於2017-09-13

如何實現一個 Virtual DOM 及原始碼分析

Virtual DOM演算法

    web頁面有一個對應的DOM樹,在傳統開發頁面時,每次頁面需要被更新時,都需要手動操作DOM來進行更新,但是我們知道DOM操作對效能來說是非常不友好的,會影響頁面的重排,從而影響頁面的效能。因此在React和VUE2.0+引入了虛擬DOM的概念,他們的原理是:把真實的DOM樹轉換成javascript物件樹,也就是虛擬DOM,每次資料需要被更新的時候,它會生成一個新的虛擬DOM,並且和上次生成的虛擬DOM進行對比,對發生變化的資料做批量更新。---(因為操作JS物件會更快,更簡單,比操作DOM來說)。
我們知道web頁面是由一個個HTML元素巢狀組合而成的,當我們使用javascript來描述這些元素的時候,這些元素可以簡單的被表示成純粹的JSON物件。

比如如下HTML程式碼:

<div id="container" class="container">
   <ul id="list">
     <li class="item">111</li>
     <li class="item">222</li>
     <li class="item">333</li>
   </ul>
   <button class="btn btn-blue"><em>提交</em></button>
</div>

上面是真實的DOM樹結構,我們可以使用javascript中的json物件來表示的話,變成如下:

var element = {
      tagName: 'div',
      props: {   // DOM的屬性
        id: 'container',
        class: 'container'
      },
      children: [
        {
          tagName: 'ul',
          props: {
            id: 'list'
          },
          children: [
            {tagName: 'li', props: {class: 'item'}, children: ['111']},
            {tagName: 'li', props: {class: 'item'}, children: ['222']},
            {tagName: 'li', props: {class: 'item'}, children: ['333']}
          ]
        },
        {
          tagName: 'button',
          props: {
            class: 'btn btn-blue'
          },
          children: [
            {
              tagName: 'em',
              children: ['提交']
            }
          ]
        }
      ]
   };

因此我們可以使用javascript物件表示DOM的資訊和結構,當狀態變更的時候,重新渲染這個javascript物件的結構,然後可以使用新渲染的物件樹去和舊的樹去對比,記錄兩顆樹的差異,兩顆樹的差異就是我們需要對頁面真正的DOM操作,然後把他們應用到真正的DOM樹上,頁面就得到更新。檢視的整個結構確實全渲染了,但是最後操作DOM的時候,只變更不同的地方。
因此我們可以總結一下 Virtual DOM演算法:
1. 用javascript物件結構來表示DOM樹的結構,然後用這個樹構建一個真正的DOM樹,插入到文件中。
2. 當狀態變更的時候,重新構造一顆新的物件樹,然後使用新的物件樹與舊的物件樹進行對比,記錄兩顆樹的差異。
3. 把記錄下來的差異用到步驟1所構建的真正的DOM樹上。檢視就更新了。

演算法實現:
2-1 使用javascript物件模擬DOM樹。
使用javascript來表示一個DOM節點,有如上JSON的資料,我們只需要記錄它的節點型別,屬性和子節點即可。

element.js 程式碼如下:

function Element(tagName, props, children) {
  this.tagName = tagName;
  this.props = props;
  this.children = children;
}
Element.prototype.render = function() {
  var el = document.createElement(this.tagName);
  var props = this.props;
  // 遍歷子節點,依次設定子節點的屬性
  for (var propName in props) {
    var propValue = props[propName];
    el.setAttribute(propName, propValue);
  }
  // 儲存子節點
  var childrens = this.children || [];
  // 遍歷子節點,使用遞迴的方式 渲染
  childrens.forEach(function(child) {
    var childEl = (child instanceof Element) ? child.render() // 如果子節點也是虛擬DOM,遞迴構建DOM節點
      : document.createTextNode(child);    // 如果是字串的話,只構建文字節點
    el.appendChild(childEl);
  });
  return el;
};
module.exports = function(tagName, props, children) {
  return new Element(tagName, props, children);
}

入口index.js程式碼如下:

var el = require('./element');

var element = el('div', {id: 'container', class: 'container'}, [
  el('ul', {id: 'list'},[
    el('li', {class: 'item'}, ['111']),
    el('li', {class: 'item'}, ['222']),
    el('li', {class: 'item'}, ['333']),
  ]),
  el('button', {class: 'btn btn-blue'}, [
    el('em', {class: ''}, ['提交'])
  ])
]);

var elemRoot = element.render();
document.body.appendChild(elemRoot);

開啟頁面即可看到效果。

2-2 比較兩顆虛擬DOM樹的差異及差異的地方進行dom操作

上面的div只會和同一層級的div對比,第二層級的只會和第二層級的對比,這樣的演算法的複雜度可以達到O(n).
但是在實際程式碼中,會對新舊兩顆樹進行一個深度優先的遍歷,因此每個節點都會有一個標記。如下圖所示:

在遍歷的過程中,每次遍歷到一個節點就把該節點和新的樹進行對比,如果有差異的話就記錄到一個物件裡面。

現在我們來看下我的目錄下 有哪些檔案;然後分別對每個檔案程式碼進行解讀,看看做了哪些事情,舊的虛擬dom和新的虛擬dom是如何比較的,且是如何更新頁面的 如下目錄:
目錄結構如下:

vdom  ---- 工程名
|   | ---- index.html  html頁面
|   | ---- element.js  例項化元素組成json資料 且 提供render方法 渲染頁面
|   | ---- util.js     提供一些公用的方法
|   | ---- diff.js     比較新舊節點資料 如果有差異儲存到一個物件裡面去
|   | ---- patch.js    對當前差異的節點資料 進行DOM操作
|   | ---- index.js    頁面程式碼初始化呼叫

首先是 index.js檔案 頁面渲染完成後 變成如下html結構 

<div id="container">
  <h1 style="color: red;">simple virtal dom</h1>
  <p>the count is :1</p>
  <ul>
    <li>Item #0</li>
  </ul>
</div>

假如發生改變後,變成如下結構 

<div id="container">
  <h1 style="color: blue;">simple virtal dom</h1>
  <p>the count is :2</p>
  <ul>
    <li>Item #0</li>
    <li>Item #1</li>
  </ul>
</div>

可以看到 新舊節點頁面資料的改變,h1標籤從屬性 顏色從紅色 變為藍色,p標籤的文字發生改變,ul新增了一項元素li。
基本的原理是:先渲染出頁面資料出來,生成第一個模板頁面,然後使用定時器會生成一個新的頁面資料出來,對新舊兩顆樹進行一個深度優先的遍歷,因此每個節點都會有一個標記。
然後呼叫diff方法對比物件新舊節點遍歷進行對比,找出兩者的不同的地方存入到一個物件裡面去,最後通過patch.js找出物件不同的地方,分別進行dom操作。

index.js程式碼如下:

var el = require('./element');
var diff = require('./diff');
var patch = require('./patch');

var count = 0;
function renderTree() {
  count++;
  var items = [];
  var color = (count % 2 === 0) ? 'blue' : 'red';
  for (var i = 0; i < count; i++) {
    items.push(el('li', ['Item #' + i]));
  }
  return el('div', {'id': 'container'}, [
    el('h1', {style: 'color: ' + color}, ['simple virtal dom']),
    el('p', ['the count is :' + count]),
    el('ul', items)
  ]);
}

var tree = renderTree()
var root = tree.render()
document.body.appendChild(root)
setInterval(function () {
  var newTree = renderTree()
  var patches = diff(tree, newTree)
  console.log(patches)
  patch(root, patches)
  tree = newTree
}, 1000);

執行 var tree = renderTree()方法後,會呼叫element.js,
1. 依次遍歷子節點(從內到外呼叫)依次為 li, h1, p, ul, li和h1和p有一個文字子節點,因此遍歷完成後,count就等於1,
但是遍歷ul的時候,因為有一個子節點li,因此 count += 1; 所以呼叫完成後,ul的count等於2. 因此會對每個element屬性新增count屬性。對於最外層的container容器就是對每個子節點的依次增加,h1子節點預設為1,迴圈完成後 +1;因此變為2, p節點預設為1,迴圈完成後 +1,因此也變為2,ul為2,迴圈完成後 +1,因此變為3,因此container節點的count=2+2+3 = 7;

element.js部分程式碼如下:

function Element(tagName, props, children) {
  if (!(this instanceof Element)) {
    // 判斷子節點 children 是否為 undefined
    if (!utils.isArray(children) && children !== null) {
      children = utils.slice(arguments, 2).filter(utils.truthy);
    }
    return new Element(tagName, props, children);
  }
  // 如果沒有屬性的話,第二個引數是一個陣列,說明第二個引數傳的是子節點
  if (utils.isArray(props)) {
    children = props;
    props = {};
  }
  this.tagName = tagName;
  this.props = props || {};
  this.children = children || [];
  // 儲存key鍵 如果有屬性 儲存key,否則返回undefined
  this.key = props ? props.key : void 0;
  var count = 0;
  
  utils.each(this.children, function(child, i) {
    // 如果是元素的實列的話
    if (child instanceof Element) {
      count += child.count;
    } else {
      // 如果是文字節點的話,直接賦值
      children[i] = '' + child;
    }
    count++;
  });
  this.count = count;
}

oldTree資料最終變成如下:

var oldTree = {
  tagName: 'div',
  key: undefined,
  count: 7,
  props: {id: 'container'},
  children: [
    {
      tagName: 'h1',
      key: undefined
      count: 1
      props: {style: 'colod: red'},
      children: ['simple virtal dom']
    },
    {
      tagName: 'p',
      key: undefined
      count: 1
      props: {},
      children: ['the count is :1']
    },
    {
      tagName: 'ul',
      key: undefined
      count: 2
      props: {},
      children: [
        {
          tagName: 'li',
          key: undefined,
          count: 1,
          props: {},
          children: ['Item #0']
        }
      ]
    },
  ]
};

定時器 執行 var newTree = renderTree()後,呼叫方法步驟還是和第一步一樣:
2. 依次遍歷子節點(從內到外呼叫)依次為 li, h1, p, ul, li和h1和p有一個文字子節點,因此遍歷完成後,count就等於1,因為有2個子元素li,count都為1,因此ul每次遍歷依次在原來的基礎上加1,因此遍歷完成第一個li時候,ul中的count為2,當遍歷完成第二個li的時候,ul的count就為4了。因此ul中的count為4. 對於最外層的container容器就是對每個子元素依次增加。
所以 container節點的count = 2 + 2 + 5 = 9;

newTree資料最終變成如下資料:

var newTree = {
  tagName: 'div',
  key: undefined,
  count: 9,
  props: {id: 'container'},
  children: [
    {
      tagName: 'h1',
      key: undefined
      count: 1
      props: {style: 'colod: red'},
      children: ['simple virtal dom']
    },
    {
      tagName: 'p',
      key: undefined
      count: 1
      props: {},
      children: ['the count is :1']
    },
    {
      tagName: 'ul',
      key: undefined
      count: 4
      props: {},
      children: [
        {
          tagName: 'li',
          key: undefined,
          count: 1,
          props: {},
          children: ['Item #0']
        },
        {
          tagName: 'li',
          key: undefined,
          count: 1,
          props: {},
          children: ['Item #1']
        }
      ]
    },
  ]
}

var patches = diff(oldTree, newTree);

呼叫diff方法可以比較新舊兩棵樹節點的資料,把兩顆樹的不同節點找出來。(注意,檢視diff對比資料的方法,找到不同的節點,可以檢視這篇文章diff演算法)如下呼叫程式碼:

function diff (oldTree, newTree) {
  var index = 0;
  var patches = {};
  deepWalk(oldTree, newTree, index, patches);
  return patches;
}

執行deepWalk如下程式碼:

function deepWalk(oldNode, newNode, index, patches) {
  var currentPatch = [];
  // 節點被刪除掉
  if (newNode === null) {
    // 真正的DOM節點時,將刪除執行重新排序,所以不需要做任何事
  } else if(utils.isString(oldNode) && utils.isString(newNode)) {
    // 替換文字節點
    if (newNode !== oldNode) {
      currentPatch.push({type: patch.TEXT, content: newNode});
    }
  } else if(oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
    // 相同的節點,但是新舊節點的屬性不同的情況下 比較屬性
    // diff props
    var propsPatches = diffProps(oldNode, newNode);
    if (propsPatches) {
      currentPatch.push({type: patch.PROPS, props: propsPatches});
    }
    // 不同的子節點 
    if (!isIgnoreChildren(newNode)) {
      diffChildren(
        oldNode.children,
        newNode.children,
        index,
        patches,
        currentPatch
      )
    }
  } else {
    // 不同的節點,那麼新節點替換舊節點
    currentPatch.push({type: patch.REPLACE, node: newNode});
  }
  if (currentPatch.length) {
    patches[index] = currentPatch;
  }
}

1. 判斷新節點是否為null,如果為null,說明節點被刪除掉。
2. 判斷新舊節點是否為字串,如果為字串說明是文字節點,並且新舊兩個文字節點不同的話,存入陣列裡面去,如下程式碼:

   currentPatch.push({type: patch.TEXT, content: newNode});
   patch.TEXT 為 patch.js裡面的 TEXT = 3;content屬性為新節點。

3. 如果新舊tagName相同的話,並且新舊節點的key相同的話,繼續比較新舊節點的屬性,如下程式碼:

var propsPatches = diffProps(oldNode, newNode);

diffProps方法的程式碼如下:

function diffProps(oldNode, newNode) {
      var count = 0;
      var oldProps = oldNode.props;
      var newProps = newNode.props;
      var key,
        value;
      var propsPatches = {};
      // 找出不同的屬性值
      for (key in oldProps) {
        value = oldProps[key];
        if (newProps[key] !== value) {
          count++;
          propsPatches[key] = newProps[key];
        }
      }
      // 找出新增屬性
      for (key in newProps) {
        value = newProps[key];
        if (!oldProps.hasOwnProperty(key)) {
          count++;
          propsPatches[key] = newProps[key];
        }
      }
      // 如果所有的屬性都是相同的話
      if (count === 0) {
        return null;
      }
      return propsPatches;
   }

diffProps程式碼解析如下:

for (key in oldProps) {
   value = oldProps[key];
   if (newProps[key] !== value) {
      count++;
      propsPatches[key] = newProps[key];
   }
}

如上程式碼是 判斷舊節點的屬性值是否在新節點中找到,如果找不到的話,count++; 把新節點的屬性值賦值給 propsPatches 儲存起來。

for (key in newProps) {
   value = newProps[key];
   if (!oldProps.hasOwnProperty(key)) {
      count++;
      propsPatches[key] = newProps[key];
   }
}

如上程式碼是 判斷新節點的屬性是否能在舊節點中找到,如果找不到的話,count++; 把新節點的屬性值賦值給 propsPatches 儲存起來。

if (count === 0) {
   return null;
}
return propsPatches;

最後如果count 等於0的話,說明所有屬性都是相同的話,所以不需要做任何變化。否則的話,返回新增的屬性。

如果有 propsPatches 的話,執行如下程式碼:

if (propsPatches) {
   currentPatch.push({type: patch.PROPS, props: propsPatches});
}

因此currentPatch陣列裡面也有對應的更新的屬性,props就是需要更新的屬性物件。

繼續程式碼:

// 不同的子節點 
if (!isIgnoreChildren(newNode)) {
   diffChildren(
     oldNode.children,
     newNode.children,
     index,
     patches,
     currentPatch
   )
}
function isIgnoreChildren(node) {
  return (node.props && node.props.hasOwnProperty('ignore'));
}

如上程式碼判斷子節點是否相同,diffChildren程式碼如下:

function diffChildren(oldChildren, newChildren, index, patches, currentPatch) {
    var diffs = listDiff(oldChildren, newChildren, 'key');
    newChildren = diffs.children;

    if (diffs.moves.length) {
      var recorderPatch = {type: patch.REORDER, moves: diffs.moves};
      currentPatch.push(recorderPatch);
    }

    var leftNode = null;
    var currentNodeIndex = index;
    utils.each(oldChildren, function(child, i) {
      var newChild = newChildren[i];
      currentNodeIndex = (leftNode && leftNode.count) ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1;
      // 遞迴
      deepWalk(child, newChild, currentNodeIndex, patches);
      leftNode = child;
    });
  }

如上程式碼:var diffs = listDiff(oldChildren, newChildren, 'key'); 新舊節點按照key來比較,目前key為undefined,所以diffs 為如下:

diffs = {
    moves: [],
    children: [
      {
        tagName: 'h1',
        key: undefined
        count: 1
        props: {style: 'colod: blue'},
        children: ['simple virtal dom']
      },
      {
        tagName: 'p',
        key: undefined
        count: 1
        props: {},
        children: ['the count is :2']
      },
      {
        tagName: 'ul',
        key: undefined
        count: 4
        props: {},
        children: [
          {
            tagName: 'li',
            key: undefined,
            count: 1,
            props: {},
            children: ['Item #0']
          },
          {
            tagName: 'li',
            key: undefined,
            count: 1,
            props: {},
            children: ['Item #1']
          }
        ]
      }
    ]
  };

newChildren = diffs.children;
oldChildren資料如下:

oldChildren = [
    {
      tagName: 'h1',
      key: undefined
      count: 1
      props: {style: 'colod: red'},
      children: ['simple virtal dom']
    },
    {
      tagName: 'p',
      key: undefined
      count: 1
      props: {},
      children: ['the count is :1']
    },
    {
      tagName: 'ul',
      key: undefined
      count: 2
      props: {},
      children: [
        {
          tagName: 'li',
          key: undefined,
          count: 1,
          props: {},
          children: ['Item #0']
        }
      ]
    }
  ];

接著就是遍歷 oldChildren, 第一次遍歷時 leftNode 為null,因此 currentNodeIndex = currentNodeIndex + 1 = 0 + 1 = 1; 不是第一次遍歷,那麼leftNode都為上一次遍歷的子節點,因此不是第一次遍歷的話,那麼 currentNodeIndex = currentNodeIndex + leftNode.count + 1; 
然後遞迴呼叫 deepWalk(child, newChild, currentNodeIndex, patches); 方法,接著把child賦值給leftNode,leftNode = child;

所以一直遞迴遍歷,最終把不相同的節點 會儲存到 currentPatch 陣列內。最後執行

if (currentPatch.length) {
   patches[index] = currentPatch;
}

把對應的currentPatch 儲存到 patches物件內中的對應項,最後就返回 patches物件。

4. 返回到index.js 程式碼內,把兩顆不相同的樹節點的提取出來後,需要呼叫patch.js方法傳進;把不相同的節點應用到真正的DOM上.
不相同的節點 patches資料如下:

patches = {
    1: [{type: 2, props: {style: 'color: blue'}}],
    4: [{type: 3, content: 'the count is :2'}],
    5: [
          { 
              type: 1, 
              moves: [
                { index: 1, 
                   item: {
                      tagName: 'li',
                      props: {},
                      count: 1,
                      key: undefined,
                      children: ['Item #1']
                    }
                }
              ]
           }
        ]
    }

如下程式碼呼叫:
patch(root, patches);
執行patch方法,程式碼如下:

function patch(node, patches) {
  var walker = {index: 0};
  deepWalk(node, walker, patches);
}

deepWalk 程式碼如下:

function deepWalk(node, walker, patches) {
   var currentPatches = patches[walker.index];
      // node.childNodes 返回指定元素的子元素集合,包括HTML節點,所有屬性,文字節點。
   var len = node.childNodes ? node.childNodes.length : 0;
   for (var i = 0; i < len; i++) {
      var child = node.childNodes[i];
      walker.index++;
      // 深度複製 遞迴遍歷
      deepWalk(child, walker, patches);
   }
   if (currentPatches) {
      applyPatches(node, currentPatches);
   }
}

1. 首次呼叫patch的方法,root就是container的節點,因此呼叫deepWalk方法,因此 var currentPatches = patches[0] = undefined,
var len = node.childNodes ? node.childNodes.length : 0; 因此 len = 3; 很明顯該子節點的長度為3,因為子節點有 h1, p, 和ul元素;


2. 然後進行for迴圈,獲取該父節點的子節點,因此第一個子節點為 h1 元素,walker.index++; 因此walker.index = 1; 再進行遞迴 deepWalk(child, walker, patches); 此時子節點為h1, walker.index為1, 因此獲取 currentPatches = patches[1]; 獲取值,再獲取 h1的子節點的長度,len = 1; 然後再for迴圈,獲取child為文字節點,此時 walker.index++; 所以此時walker.index 為2, 在呼叫deepwalk方法遞迴,因此再繼續獲取 currentPatches = patches[2]; 值為undefined,再獲取len = 0; 因為文字節點麼有子節點,所以for迴圈跳出,所以判斷currentPatches是否有值,因為此時 currentPatches 為undefined,所以遞迴結束,再返回到 h1元素上來,所以currentPatches = patches[1]; 所以有值,所以呼叫 applyPatches()方法來更新dom元素。


3. 繼續迴圈 i, 此時i = 1; 獲取子節點 child = p元素,walker.index++,此時walker.index = 3, 繼續呼叫 deepWalk方法,獲取 var currentPatches = patches[walker.index] = patches[3]的值,var len = 1; 因為p元素下有一個子節點(文字節點),再進for迴圈,此時 walker.index++; 因此walker.index = 4; child此時為文字節點,在呼叫 deepwalk方法的時候,再獲取var currentPatches = patches[walker.index] = patches[4]; 再執行len 程式碼的時候 len = 0;因此跳出for迴圈,判斷 currentPatches是否有值,有值的話,更新對應的DOM元素。

4. 繼續迴圈i = 2; 獲取子節點 child = ul元素,walker.index++; 此時walker.index = 5; 在呼叫deepWalk方法遞迴,因此再獲取 var currentPatches = patches[walker.index] = patches[5]; 然後len = 1, 因為ul元素下有一個li元素,在繼續for迴圈遍歷,獲取子節點li,此時walker.index++; walker.index = 6; 再遞迴呼叫deepwalk方法,再獲取var currentPatches = patches[walker.index] = patches[6]; len = 1; 因為li的元素下有一個文字節點,再進行for迴圈,此時child為文字節點,walker.index++;此時walker.index = 7; 再執行 deepwalk方法,再獲取 var currentPatches = patches[walker.index] = patches[7]; 這時候 len = 0了,因此跳出for迴圈,判斷 當前的currentPatches是否有值,沒有,就跳出,然後再返回ul元素,獲取該自己li的時候,walker.index 等於5,因此var currentPatches = patches[walker.index] = patches[5]; 然後判斷 currentPatches是否有值,有值就進行更新DOM元素。

最後就是 applyPatches 方法更新dom元素了,如下程式碼:

function applyPatches(node, currentPatches) {
  utils.each(currentPatches, function(currentPatch) {
    switch (currentPatch.type) {
      case REPLACE:
        var newNode = (typeof currentPatch.node === 'string') 
          ? document.createTextNode(currentPatch.node)
          : currentPatch.node.render();
        node.parentNode.replaceChild(newNode, node);
        break;
      case REORDER:
        reorderChildren(node, currentPatch.moves);
        break;
      case PROPS: 
        setProps(node, currentPatch.props);
        break;
      case TEXT:
        if(node.textContent) {
          node.textContent = currentPatch.content;
        } else {
          // ie bug
          node.nodeValue = currentPatch.content;
        }
        break;
      default:
        throw new Error('Unknow patch type' + currentPatch.type);
    }
  });
}

判斷型別,替換對應的屬性和節點。
最後就是對子節點進行排序的操作,程式碼如下:

// 對子節點進行排序
function reorderChildren(node, moves) {
  var staticNodeList = utils.toArray(node.childNodes);
  var maps = {};
  utils.each(staticNodeList, function(node) {
    // 如果是元素節點
    if (node.nodeType === 1) {
      var key = node.getAttribute('key');
      if (key) {
        maps[key] = node;
      }
    }
  })
  utils.each(moves, function(move) {
    var index = move.index;
    if (move.type === 0) {
      // remove Item
      if (staticNodeList[index] === node.childNodes[index]) {
        node.removeChild(node.childNodes[index]);
      }
      staticNodeList.splice(index, 1);
    } else if(move.type === 1) {
      // insert item
      var insertNode = maps[move.item.key] 
        ? maps[move.item.key].cloneNode(true)
        : (typeof move.item === 'object') ? move.item.render() : document.createTextNode(move.item);
      staticNodeList.splice(index, 0, insertNode);
      node.insertBefore(insertNode, node.childNodes[index] || null);
    }
  });
}

遍歷moves,判斷moves.type 是等於0還是等於1,等於0的話是刪除操作,等於1的話是新增操作。比如現在moves值變成如下:

moves = {
  index: 1,
  type: 1,
  item: {
    tagName: 'li',
    key: undefined,
    props: {},
    count: 1,
    children: ['#Item 1']
  }
};

node節點 就是 'ul'元素,var staticNodeList = utils.toArray(node.childNodes); 把ul的舊子節點li轉成Array形式,由於沒有屬性key,所以直接跳到下面遍歷程式碼來,遍歷moves,獲取某一項的索引index,判斷move.type 等於0 還是等於1, 目前等於1,是新增一項,但是沒有key,因此呼叫move.item.render(); 渲染完後,對staticNodeList陣列裡面的舊節點的li項從第二項開始插入節點li,然後執行node.insertBefore(insertNode, node.childNodes[index] || null); node就是ul父節點,insertNode節點插入到 node.childNodes[1]的前面。因此把在第二項的前面插入第一項。
檢視github上原始碼

相關文章