有不少文章講過react的diff算法,但要麼是晦澀難懂的源碼分析,讓人很難讀進去,要麼就是流於表面的簡單講解,實際上你們看完後仍是一頭霧水,所以我將react-lite(基於react v15)中的diff算法實現稍微整理了一下,但願可以幫助你們解惑。javascript
在看本文以前,建議先看一下這篇文章,看完後會對react中diff的基本原理有一些理解:blog.csdn.net/sexy_squirr…html
對於react diff,咱們已知的有兩點,一個是會經過key來作比較,另外一個是react默認是同級節點作diff,不會考慮到跨層級節點的diff(事實是前端開發中不多有DOM節點跨層級移動的)。前端
首先,拋給咱們一個問題,那就是react怎麼對那麼深層次的DOM作的diff?實際上react是對DOM進行遞歸來作的,遍歷全部子節點,對子節點再作遞歸。java
// 超簡單代碼實現
const compareTwoVnodes(oldVnode, newVnode, dom) {
let newNode = dom
// 若是新的虛擬DOM是null,那麼就將前一次的真實DOM移除掉
if (newVnode == null) {
destroyVnode(oldVnode, dom)
dom.parentNode.removeChild(dom)
} else if (oldVnode.type !== newVnode.type || oldVnode.key !== newVnode.key) {
// replace
destroyVnode(oldVnode, dom)
newNode = initVnode(newVnode, parentContext, dom.namespaceURI)
dom.parentNode.replaceChild(newNode, dom)
} else if (oldVnode !== newVnode || parentContext) {
// same type and same key -> update
newNode = updateVNode(oldVnode, newVnode, dom, parentContext)
}
}
/** * 更新虛擬DOM * 這裏的type須要注意一下,若是vnode是個html元素,例如h1,那麼type就是'h1' * 若是vnode是一個函數組件,例如const Header = () => <h1>header</h1>,那麼type就是函數Header * 若是vnode是一個class組件,那麼type就是那個class */
const updateVNode = (vnode, node) => {
const { type } = vnode; // type是指虛擬DOM的類型
// 若是是class組件
if (type === VCOMPONENT) {
return updateComponent(vnode, node)
} else (type === VSTATELESS){
return updateStateLess(vnode, node)
}
updateVChildren(vnode, node)
}
// 更新class組件(調用render方法拿到新的虛擬DOM)
const updateComponent = (vnode, node) => {
const { type: Component } = vnode; // type是指虛擬DOM的類型
const newVNode = new Component().render();
compareTwoVnodes(newVNode, vnode, node);
}
// 更新無狀態組件(直接執行函數拿到新的虛擬DOM)
const updateStateLess = (vnode, node) => {
const { type: Component } = vnode; // type是指虛擬DOM的類型
const newVNode = Component();
compareTwoVnodes(newVNode, vnode, node);
}
const updateVChildren = (vnode, node) => {
for (let i = 0; i < node.children.length; i++) {
updateVNode(vnode.children[i], node.children[i])
}
}
複製代碼
所以,咱們這裏以其中一層節點來說解diff是如何作到列表更新的。node
假設咱們的react組件渲染成功後,在瀏覽器中顯示的真實DOM節點是A、B、C、D,咱們更新後的虛擬DOM是B、A、E、D。
那咱們這裏須要作的操做就是,將原來DOM中已經存在的A、B、D進行更新,將原來DOM中存在,而如今不存的C移除掉,再建立新的D節點。
這樣一來,問題就簡化了不少,咱們只須要收集到須要create、remove和update的節點信息就好了。react
// oldDoms是真實DOM,newDoms是最新的虛擬DOM
const oldDoms = [A, B, C, D],
newDoms = [B, A, E, D],
updates = [],
removes = [],
creates = [];
// 進行兩層遍歷,獲取到哪些節點須要更新,哪些節點須要移除。
for (let i = 0; i < oldDoms.length; i++) {
const oldDom = oldDoms[i]
let shouldRemove = true
for (let j = 0; j < newDoms.length; j++) {
const newDom = newDoms[j];
if (
oldDom.key === newDom.key &&
oldDom.type === newDom.type
) {
updates[j] = {
index: j,
node: oldDom,
parentNode: parentNode // 這裏真實DOM的父節點
}
shouldRemove = false
}
}
if (shouldRemove) {
removes.push({
node: oldDom
})
}
}
// 從虛擬DOM節點來取出不要更新的節點,這就是須要新建立的節點。
for (let j = 0; j < newDoms.length; j++) {
if (!updates[j]) {
creates.push({
index: j,
vnode: newDoms[j],
parentNode: parentNode // 這裏真實DOM的父節點
})
}
}
複製代碼
這樣,咱們便拿到了想要的狀態信息。算法
在獲得須要create、update和remove的節點後,咱們這時就能夠開始進行渲染了。
數組
首先,咱們遍歷全部須要remove的節點,將其從真實DOM中remove掉。所以這裏須要remove掉C節點,最後渲染結果是A、B、D。瀏覽器
const remove = (removes) => {
removes.forEach(remove => {
const node = remove.node
node.parentNode.removeChild(node)
})
}
複製代碼
其次,咱們再遍歷須要更新的節點,將其插入到對應的位置中。因此這裏最後渲染結果是B、A、D。bash
const update = (updates) => {
updates.forEach(update => {
const index = update.index,
parentNode = update.parentNode,
node = update.node,
curNode = parentNode.children[index];
if (curNode !== node) {
parentNode.insertBefore(node, curNode)
}
})
}
複製代碼
最後一步,咱們須要建立新的DOM節點,並插入到正確的位置中,最後渲染結果爲B、A、E、D。
const create = (creates) => {
creates.forEach(create => {
const index = create.index,
parentNode = create.parentNode,
vnode = create.vnode,
curNode = parentNode.children[index],
node = createNode(vnode); // 建立DOM節點
parentNode.insertBefore(node, curNode)
})
}
複製代碼
雖然這篇文章寫的比較簡單,可是一個完整的diff流程就是這樣了,能夠加深對react的一些理解。