淺談react diff實現

尹光耀發表於2019-04-30

前言

有很多文章講過react的diff演算法,但要麼是晦澀難懂的原始碼分析,讓人很難讀進去,要麼就是流於表面的簡單講解,實際上大家看完後還是一頭霧水,因此我將react-lite(基於react v15)中的diff演算法實現稍微整理了一下,希望能夠幫助大家解惑。


在看本文之前,建議先看一下這篇文章,看完後會對react中diff的基本原理有一些理解:blog.csdn.net/sexy_squirr…


對於react diff,我們已知的有兩點,一個是會通過key來做比較,另一個是react預設是同級節點做diff,不會考慮到跨層級節點的diff(事實是前端開發中很少有DOM節點跨層級移動的)。

淺談react diff實現

遞迴更新

首先,拋給我們一個問題,那就是react怎麼對那麼深層次的DOM做的diff?實際上react是對DOM進行遞迴來做的,遍歷所有子節點,對子節點再做遞迴。

// 超簡單程式碼實現
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是如何做到列表更新的。

狀態收集

假設我們的react元件渲染成功後,在瀏覽器中顯示的真實DOM節點是A、B、C、D,我們更新後的虛擬DOM是B、A、E、D。
那我們這裡需要做的操作就是,將原來DOM中已經存在的A、B、D進行更新,將原來DOM中存在,而現在不存的C移除掉,再建立新的D節點。
這樣一來,問題就簡化了很多,我們只需要收集到需要create、remove和update的節點資訊就行了。

淺談react diff實現
// 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的父節點
        })
    }
}
複製程式碼

這樣,我們便拿到了想要的狀態資訊。

diff

在得到需要create、update和remove的節點後,我們這時就可以開始進行渲染了。

淺談react diff實現

首先,我們遍歷所有需要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。

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的一些理解。


相關文章