Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

浅谈react diff实现 #27

Open
yinguangyao opened this issue Apr 26, 2019 · 0 comments
Open

浅谈react diff实现 #27

yinguangyao opened this issue Apr 26, 2019 · 0 comments

Comments

@yinguangyao
Copy link
Owner

yinguangyao commented Apr 26, 2019

浅谈react diff实现

这是一篇硬核文,因此不会用生动幽默的语言来讲述,这篇文章大概更像是自己心血来潮的总结吧哈哈哈哈。
有很多文章讲过 react 的 diff 算法,但要么是晦涩难懂的源码分析,让人很难读进去,要么就是流于表面的简单讲解,实际上大家看完后还是一头雾水,因此我将 react-lite(基于 react v15) 中的 diff 算法实现稍微整理了一下,希望能够帮助大家解惑。
对于 react diff,我们已知的有两点,一个是会通过 key 来做比较,另一个是 react 默认是同级节点做diff,不会考虑到跨层级节点的 diff(事实是前端开发中很少有DOM节点跨层级移动的)。
image

递归更新

首先,抛给我们一个问题,那就是 react 怎么对那么深层次的 DOM 做的 diff?实际上 react 是对 DOM 进行递归来做的,遍历所有子节点,对子节点再做递归,这一过程类似深度优先遍历。

// 超简单代码实现
const updateVNode = (vnode, node) => {
    updateVChildren(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移除掉,再创建新的E节点。
这样一来,问题就简化了很多,我们只需要收集到需要 create、remove和update 的节点信息就行了。

// 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 的节点后,我们这时就可以开始进行渲染了。
node | 状态 | index
:-: | :-: | :-: | :-: | :-:
A | update | 1
B | update| 0
C | remove
D | update | 3
E | create | 2

首先,我们遍历所有需要 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的一些理解。当然了,还有一些对 DOM 节点属性之类的比较,这里不做讲解。
image
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant