浅谈react diff实现

栏目: 服务器 · 发布时间: 4年前

内容简介:有很多文章讲过react的diff算法,但要么是晦涩难懂的源码分析,让人很难读进去,要么就是流于表面的简单讲解,实际上大家看完后还是一头雾水,因此我将react-lite(基于react v15)中的diff算法实现稍微整理了一下,希望能够帮助大家解惑。在看本文之前,建议先看一下这篇文章,看完后会对react中diff的基本原理有一些理解:对于react diff,我们已知的有两点,一个是会通过key来做比较,另一个是react默认是同级节点做diff,不会考虑到跨层级节点的diff(事实是前端开发中很少有

前言

有很多文章讲过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的一些理解。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Head First HTML and CSS

Head First HTML and CSS

Elisabeth Robson、Eric Freeman / O'Reilly Media / 2012-9-8 / USD 39.99

Tired of reading HTML books that only make sense after you're an expert? Then it's about time you picked up Head First HTML and really learned HTML. You want to learn HTML so you can finally create th......一起来看看 《Head First HTML and CSS》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器