原文連接:Nealyang PersonalBlog css
因爲源碼中diff算法摻雜了太多別的功能模塊,而且dom diff相對於以前的代碼實現來講仍是有些麻煩的,尤爲是列表對比的算法,因此這裏咱們單獨拿出來講他實現vue
衆所周知,React中最爲人稱讚的就是Virtual DOM和 diff 算法的完美結合,讓咱們能夠不顧性能的「任性」更新界面,前面文章中咱們有介紹道Virtual DOM,其實就是經過js來模擬dom的實現,而後經過對js obj的操做,最後渲染到頁面中,可是,若是當咱們修改了一丟丟東西,就要渲染整個頁面的話,性能消耗仍是很是大的,如何才能準確的修改該修改的地方就是咱們diff算法的功能了。node
其實所謂的diff算法大概就是當狀態發生改變的時候,從新構造一個新的Virtual DOM,而後根據與老的Virtual DOM對比,生成patches補丁,打到對應的須要修改的地方。react
這裏引用司徒正美的介紹git
最開始經典的深度優先遍歷DFS算法,其複雜度爲O(n^3),存在高昂的diff成本,而後是cito.js的橫空出世,它對從此全部虛擬DOM的算法都有重大影響。它採用兩端同時進行比較的算法,將diff速度拉高到幾個層次。緊隨其後的是kivi.js,在cito.js的基出提出兩項優化方案,使用key實現移動追蹤及基於key的編輯長度距離算法應用(算法複雜度 爲O(n^2))。但這樣的diff算法太過複雜了,因而後來者snabbdom將kivi.js進行簡化,去掉編輯長度距離算法,調整兩端比較算法。速度略有損失,但可讀性大大提升。再以後,就是著名的vue2.0 把snabbdom整個庫整合掉了。github
傳統的diff算法經過循環遞歸每個節點,進行對比,這樣的操做效率很是的低,複雜程度O(n^3),其中n標識樹的節點總數。若是React僅僅是引入傳統的diff算法的話,其實性能也是很是差的。然而FB經過大膽的策略,知足了大多數的性能最大化,將O(n^3)複雜度的問題成功的轉換成了O(n),而且後面對於同級節點移動,犧牲必定的DOM操做,算法的複雜度也纔打到O(max(M,N))。算法
這裏借用下網上的一張圖,感受畫的很是贊~bash
大概解釋下:dom
額。。。其實上面也已近解釋了,當Virtual DOM發生變化的時,如上圖的第二個和第三個 p 的sonx被刪除了,這時候,咱們就經過diff算法,計算出先後Virtual DOM的差別->補丁對象patches,而後根據這個patches對象中的信息來遍歷以前的老Virtual DOM樹,對其須要更新的地方進行更新,使其變成新VIrtual DOM。ide
Web UI中節點跨級操做特別少,能夠忽略不計
擁有相同類的兩個組件將會生成類似的樹形結構,擁有不一樣類的兩個組件將會生成不一樣的樹形結構。(哪怕同樣的而我也認爲不同 -> 大機率優化)
對於同一層級的一組子節點,他們能夠經過惟一的key來區分,以方便後續的列表對比算法
基於如上,React分別對tree diff、Component diff 、element diff 進行了算法優化。
基於策略一,React的diff很是簡單明瞭:只會對同一層次的節點進行比較。這種非傳統的按深度遍歷搜索,這種經過大膽假設獲得的改進方案,不只符合實際場景的須要,並且大幅下降了算法實現複雜度,從O(n^3)提高至O(n)。
基於此,React官方並不推薦進行DOM節點的跨層級操做 ,假若真的出現了,那就是很是消耗性能的remove和create的操做了。
我是真的不會畫圖
因爲React是基於組件開發的,因此組件的dom diff其實也很是簡單,若是組件是同一類型,則進行tree diff比較。若是不是,則直接放入到patches中。即便是子組件結構類型都相同,只要父組件類型不一樣,都會被從新渲染。這也說明了爲何咱們推薦使用shouldComponentUpdate來提升React性能。
大概的感受是醬紫的
對於節點的比較,其實只有三種操做,插入、移動和刪除。(這裏最麻煩的是移動,後面會介紹實現)。當被diff節點處於同一層級時,經過三種節點操做新舊節點進行更新:插入,移動和刪除,同時提供給用戶設置key屬性的方式調整diff更新中默認的排序方式,在沒有key值的列表diff中,只能經過按順序進行每一個元素的對比,更新,插入與刪除,在數據量較大的狀況下,diff效率低下,若是可以基於設置key標識盡心diff,就可以快速識別新舊列表之間的變化內容,提高diff效率。
對於這三種理論知識能夠參照知乎上難以想象的 react diff的介紹。
前方高清多碼預警
這裏引入代碼處理咱們先撇開list diff中的移動操做,先一步一步去實現
根據節點變動類型,咱們定義以下幾種變化
const ATTRS = 'ATTRS';//屬性改變
const TEXT = 'TEXT';//文本改變
const REMOVE = 'REMOVE';//移除操做
const REPLACE = 'REPLACE';//替換操做
let Index = 0;
複製代碼
解釋下index,爲了方便演示diff,咱們暫時沒有想react源碼中給每個Element添加惟一標識
var ReactElement = function(type, key, ref, self, source, owner, props) {
var element = {
// This tag allow us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,//重點在這裏
// Built-in properties that belong on the element
type: type,
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner,
};
return element;
};
...
'use strict';
// The Symbol used to tag the ReactElement type. If there is no native Symbol
// nor polyfill, then a plain number is used for performance.
var REACT_ELEMENT_TYPE =
(typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||
0xeac7;
module.exports = REACT_ELEMENT_TYPE;
複製代碼
咱們遍歷每個VDom,以index爲索引。注意這裏咱們使用全局變量index,由於遍歷整個VDom,以index做爲區分,因此必須用全局變量,固然,GitHub上有大神的實現方式爲{index:0}
,哈~引用類型傳遞,換湯不換藥~
開始遍歷
export default function diff(oldTree, newTree) {
let patches = {};
// 遞歸樹, 比較後的結果放到補丁包中
walk(oldTree, newTree, Index, patches)
return patches;
}
複製代碼
function walk(oldNode, newNode, index, patches) {
let currentPatch = [];
if(!newNode){
currentPatch.push({
type:REMOVE,
index
});
}else if(isString(oldNode) && isString(newNode)){
if(oldNode !== newNode){// 判斷是否爲文本
currentPatch.push({
type:TEXT,
text:newNode
});
}
}else if (oldNode.type === newNOde.type) {
// 比較屬性是否有更改
let attrs = diffAttr(oldNode.porps, newNode.props);
if (Object.keys(attrs).length > 0) {
currentPatch.push({
type: ATTRS,
attrs
});
}
// 比較兒子們
diffChildren(oldNode.children,newNode.children,patches);
}else{
// 說明節點被替換
currentPatch.push({
type: REPLACE,
newNode
});
}
currentPatch.length ? patches[index] = currentPatch : null;
}
function diffChildren(oldChildren,newChildren,patches) {
oldChildren.forEach((child,ids)=>{
// index 每次傳遞給walk時, index應該是遞增的.全部的都基於同一個Index
walk(child,newChildren[idx],++Index,patches);
})
}
function diffAttr(oldAttrs, newAttrs) {
let patch = {};
// 判斷老屬性和新屬性的關係
for (let key in oldAttrs) {
if (oldAttrs[key] !== newAttrs[key]) {
patch[key] = newAttrs[key]; //有多是undefined => 新節點中刪了該屬性
}
}
// 新節點新增了不少屬性
for (let key in newAttrs) {
if (!oldAttrs.hasOwnProperty(key)) {
patch[key] = newAttrs[key];
}
}
return patch;
}
複製代碼
在diff過程當中,咱們須要去判斷文本標籤,須要在util中寫一個工具函數
function isString(node) {
return Object.prototype.toString.call(node)==='[object String]';
}
複製代碼
實現思路很是簡單,手工流程圖瞭解下
經過diff後,最終咱們會拿到新舊VDom的patches補丁,補丁的內容大體以下:
patches = {
1:{
type:'REMOVE',
index:1
},
3:{
type:'TEXT',
newText:'hello Nealyang~',
},
6:{
type:'REPLACE',
newNode:newNode
}
}
複製代碼
大體是這麼個感受,兩秒鐘體會下~
這裏應該會有點詫異的是1 3 6...
是什麼鬼?
由於以前咱們說過,diff採用的依舊是深度優先遍歷,及時你是改良後的升級產品,可是遍歷流程依舊是:
既然patches補丁已經拿到了,該如何使用呢,對,咱們依舊是遍歷!
Element 調用render後,咱們已經能夠拿到一個經過VDom(代碼)解析後的真是Dom了,因此咱們只須要將遍歷真實DOM,而後在指定位置修改對應的補丁上指定位置的更改就好了。
代碼以下:(本身實現的簡易版)
let allPaches = {};
let index = 0; //默認哪一個須要補丁
export default function patch(dom, patches) {
allPaches = patches;
walk(dom);
}
function walk(dom) {
let currentPatche = allPaches[index];
let childNodes = dom.childNodes;
childNodes.forEach(element => walk(element));
if (currentPatche > 0) {
doPatch(dom, currentPatche);
}
}
function doPatch(node, patches) {
patches.forEach(patch => {
switch (patch.type) {
case 'ATTRS':
setAttrs(patch.attrs)//別的文件方法
break;
case 'TEXT':
node.textContent = patch.text;
break;
case 'REPLACE':
let newNode = patch.newNode instanceof Element ? render(patch.newNode) : document.createTextNode(patch.newNode);
node.parentNode.replaceChild(newNode, node)
break;
case 'REMOVE':
node.parentNode.removeChild(node);
break;
}
})
}
複製代碼
關於setAttrs其實功能都加都明白,這裏給個簡單實例代碼,你們YY下
function setAttrs(dom, props) {
const ALL_KEYS = Object.keys(props);
ALL_KEYS.forEach(k =>{
const v = props[k];
// className
if(k === 'className'){
dom.setAttribute('class',v);
return;
}
if(k == "style") {
if(typeof v == "string") {
dom.style.cssText = v
}
if(typeof v == "object") {
for (let i in v) {
dom.style[i] = v[i]
}
}
return
}
if(k[0] == "o" && k[1] == "n") {
const capture = (k.indexOf("Capture") != -1)
dom.addEventListener(k.substring(2).toLowerCase(),v,capture)
return
}
dom.setAttribute(k, v)
})
}
複製代碼
如上,其實咱們已經實現了DOM diff了,可是存在一個問題.
以下圖,老集合中包含節點:A、B、C、D,更新後的新集合中包含節點:B、A、D、C,此時新老集合進行 diff 差別化對比,發現 B != A,則建立並插入 B 至新集合,刪除老集合 A;以此類推,建立並插入 A、D 和 C,刪除 B、C 和 D。
針對這一現象,React 提出優化策略:容許開發者對同一層級的同組子節點,添加惟一 key 進行區分,雖然只是小小的改動,性能上卻發生了翻天覆地的變化!
具體介紹能夠參照 zhuanlan.zhihu.com/p/20346379
這裏咱們放到代碼實現上:
/**
* Diff two list in O(N).
* @param {Array} oldList - Original List
* @param {Array} newList - List After certain insertions, removes, or moves
* @return {Object} - {moves: <Array>}
* - moves is a list of actions that telling how to remove and insert
*/
function diff (oldList, newList, key) {
var oldMap = makeKeyIndexAndFree(oldList, key)
var newMap = makeKeyIndexAndFree(newList, key)
var newFree = newMap.free
var oldKeyIndex = oldMap.keyIndex
var newKeyIndex = newMap.keyIndex
var moves = []
// a simulate list to manipulate
var children = []
var i = 0
var item
var itemKey
var freeIndex = 0
// first pass to check item in old list: if it's removed or not // 遍歷舊的集合 while (i < oldList.length) { item = oldList[i] itemKey = getItemKey(item, key)//itemKey a // 是否能夠取到 if (itemKey) { // 判斷新集合中是否有這個屬性,若是沒有則push null 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++ } // children [{id:"a"},{id:"b"},{id:"c"},null,{id:"e"}] var simulateList = children.slice(0)//[{id:"a"},{id:"b"},{id:"c"},null,{id:"e"}] // remove items no longer exist i = 0 while (i < simulateList.length) { if (simulateList[i] === null) { remove(i) removeSimulate(i) } else { i++ } } // i is cursor pointing to a item in new list // j is cursor pointing to a item in simulateList var j = i = 0 while (i < newList.length) { item = newList[i] itemKey = getItemKey(item, key)//c var simulateItem = simulateList[j] //{id:"a"} var simulateItemKey = getItemKey(simulateItem, key)//a if (simulateItem) { if (itemKey === simulateItemKey) { j++ } else { // 新增項,直接插入 if (!oldKeyIndex.hasOwnProperty(itemKey)) { insert(i, item) } else { // if remove current simulateItem make item in right place // then just remove it var nextItemKey = getItemKey(simulateList[j + 1], key) if (nextItemKey === itemKey) { remove(i) removeSimulate(j) j++ // after removing, current j is right, just jump to next one } else { // else insert item insert(i, item) } } } } else { insert(i, item) } i++ } //if j is not remove to the end, remove all the rest item var k = simulateList.length - j while (j++ < simulateList.length) { k-- remove(k + i) } // 記錄舊的列表中移除項 {index:3,type:0} function remove (index) { var move = {index: index, type: 0} moves.push(move) } function insert (index, item) { var move = {index: index, item: item, type: 1} moves.push(move) } // 刪除simulateList中null function removeSimulate (index) { simulateList.splice(index, 1) } return { moves: moves, children: children } } /** * Convert list to key-item keyIndex object. * 將列表轉換爲 key-item 的鍵值對象 * [{id: "a"}, {id: "b"}, {id: "c"}, {id: "d"}, {id: "e"}] -> [a:0,b:1,c:2...] * @param {Array} list * @param {String|Function} key */ 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 } } // 獲取置頂key的value function getItemKey (item, key) { if (!item || !key) return void 666 return typeof key === 'string' ? item[key] : key(item) } exports.makeKeyIndexAndFree = makeKeyIndexAndFree exports.diffList = diff 複製代碼
代碼參照:list-diff 具體的註釋都已經加上。 使用以下:
import {diffList as diff} from './lib/diffList';
var oldList = [{id: "a"}, {id: "b"}, {id: "c"}, {id: "d"}, {id: "e"}]
var newList = [{id: "c"}, {id: "a"}, {id: "b"}, {id: "e"}, {id: "f"}]
var moves = diff(oldList, newList, "id")
// type 0 表示移除, type 1 表示插入
// moves: [
// {index: 3, type: 0},
// {index: 0, type: 1, item: {id: "c"}},
// {index: 3, type: 0},
// {index: 4, type: 1, item: {id: "f"}}
// ]
console.log(moves)
moves.moves.forEach(function(move) {
if (move.type === 0) {
oldList.splice(move.index, 1) // type 0 is removing
} else {
oldList.splice(move.index, 0, move.item) // type 1 is inserting
}
})
// now `oldList` is equal to `newList`
// [{id: "c"}, {id: "a"}, {id: "b"}, {id: "e"}, {id: "f"}]
console.log(oldList)
複製代碼
這裏我最困惑的地方時,實現diff都是index爲索引,深度優先遍歷,若是存在這種移動操做的話,那麼以前我補丁patches裏記錄的index不就沒有意義了麼??
在 後來在開源的simple-virtual-dom中找到了index做爲索引和標識去實現diff的答案。
function Element(tagName, props, children) {
if (!(this instanceof Element)) {
if (!_.isArray(children) && children != null) {
children = _.slice(arguments, 2).filter(_.truthy)
}
return new Element(tagName, props, children)
}
if (_.isArray(props)) {
children = props
props = {}
}
this.tagName = tagName
this.props = props || {}
this.children = children || []
this.key = props ?
props.key :
void 666
var count = 0
_.each(this.children, function (child, i) {
if (child instanceof Element) {
count += child.count
} else {
children[i] = '' + child
}
count++
})
this.count = count
}
複製代碼
(leftNode && leftNode.count) ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1
。完整代碼以下:function diffChildren(oldChildren, newChildren, index, patches, currentPatch) {
var diffs = diffList(oldChildren, newChildren, 'key')
newChildren = diffs.children
if (diffs.moves.length) {
var reorderPatch = {
type: patch.REORDER,
moves: diffs.moves
}
currentPatch.push(reorderPatch)
}
var leftNode = null
var currentNodeIndex = index
_.each(oldChildren, function (child, i) {
var newChild = newChildren[i]
currentNodeIndex = (leftNode && leftNode.count) ?
currentNodeIndex + leftNode.count + 1 :
currentNodeIndex + 1
dfsWalk(child, newChild, currentNodeIndex, patches)
leftNode = child
})
}
複製代碼
話說,這裏困擾了我很久很久。。。。
回到開頭
var REACT_ELEMENT_TYPE =
(typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||
0xeac7;
複製代碼
也就說明了這段代碼的必要性。
最後咱們在看下0.3中diff的實現:
updateMultiChild: function(nextChildren, transaction) {
if (!nextChildren && !this._renderedChildren) {
return;
} else if (nextChildren && !this._renderedChildren) {
this._renderedChildren = {}; // lazily allocate backing store with nothing
} else if (!nextChildren && this._renderedChildren) {
nextChildren = {};
}
var rootDomIdDot = this._rootNodeID + '.';
var markupBuffer = null; // Accumulate adjacent new children markup.
var numPendingInsert = 0; // How many root nodes are waiting in markupBuffer
var loopDomIndex = 0; // Index of loop through new children.
var curChildrenDOMIndex = 0; // See (Comment 1)
for (var name in nextChildren) {
if (!nextChildren.hasOwnProperty(name)) {continue;}
// 獲取當前節點與要渲染的節點
var curChild = this._renderedChildren[name];
var nextChild = nextChildren[name];
// 是否兩個節點都存在,且類型相同
if (shouldManageExisting(curChild, nextChild)) {
// 若是有插入標示,以後又循環到了不須要插入的節點,則直接插入,並把插入標示制空
if (markupBuffer) {
this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert);
markupBuffer = null;
}
numPendingInsert = 0;
// 若是找到當前要渲染的節點序號比最大序號小,則移動節點
/*
* 在0.3中,沒有根據key作diff,而是經過Object中的key做爲索引
* 好比{a,b,c}替換成{c,b,c}
* b._domIndex = 1挪到loopDomIndex = 1的位置,就是原地不動
a._domIndex = 0挪到loopDomIndex = 2的位置,也就是和c換位
*/
if (curChild._domIndex < curChildrenDOMIndex) { // (Comment 2)
this.enqueueMove(curChild._domIndex, loopDomIndex);
}
curChildrenDOMIndex = Math.max(curChild._domIndex, curChildrenDOMIndex);
// 遞歸更新子節點Props,調用子節點dom-diff...
!nextChild.props.isStatic &&
curChild.receiveProps(nextChild.props, transaction);
curChild._domIndex = loopDomIndex;
} else {
// 當前存在,執行刪除
if (curChild) { // !shouldUpdate && curChild => delete
this.enqueueUnmountChildByName(name, curChild);
curChildrenDOMIndex =
Math.max(curChild._domIndex, curChildrenDOMIndex);
}
// 當前不存在,下個節點存在, 執行插入,渲染下個節點
if (nextChild) { // !shouldUpdate && nextChild => insert
this._renderedChildren[name] = nextChild;
// 渲染下個節點
var nextMarkup =
nextChild.mountComponent(rootDomIdDot + name, transaction);
markupBuffer = markupBuffer ? markupBuffer + nextMarkup : nextMarkup;
numPendingInsert++;
nextChild._domIndex = loopDomIndex;
}
}
loopDomIndex = nextChild ? loopDomIndex + 1 : loopDomIndex;
}
// 執行插入操做,插入位置計算方式以下:
// 要渲染的節點位置-要插入的節點個數:好比當前要渲染的節點index=3,當前節點只有一個,也就是index=1。
// 如<div>1</div>渲染成<div>1</div><div>2</div><div>3</div>
// 那麼從<div>2</div>開始就開始加入buffer,最終buffer內容爲<div>2</div><div>3</div>
// 那麼要插入的位置爲 3 - 1 = 2。咱們以<div>1</div>爲1,就是把buffer插入2的位置,也就是<div>1</div>後面
if (markupBuffer) {
this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert);
}
// 循環老節點
for (var childName in this._renderedChildren) {
if (!this._renderedChildren.hasOwnProperty(childName)) { continue; }
var child = this._renderedChildren[childName];
// 當前節點存在,下個節點不存在,刪除
if (child && !nextChildren[childName]) {
this.enqueueUnmountChildByName(childName, child);
}
}
// 一次提交全部操做
this.processChildDOMOperationsQueue();
}
複製代碼