初探 React - 双缓存 fiber tree

2,097 阅读5分钟

前言

虽然知道从 16 版本以后,React 采取了双缓存 fiber tree,但一直对双缓存 fiber tree 存在的意义不是很理解。琢磨了一段时间,感觉也没有必要采用双缓存 fiber tree,单 fiber tree 同样可以满足需要,要知道 vue 采用的就是单 virtual tree 结构。

最近学习了 React Concurrent 模式,恍然大悟,才发现是自己浅薄了,原来双缓存 fiber tree 的作用不仅仅是作为 MVVM 框架中所谓的 virtual node tree 来实现响应式更新的,它更主要的是为了服务 Concurrent 模式。

接下来,我们就通过本文来具体说明一下。

双缓存 fiber tree

首先我们先来简单了解一下 React 的双缓存 fiber tree

React 做更新处理时,会同时存在两颗 fiber tree。一颗是已经存在的 old fiber tree,对应当前屏幕显示的内容,通过根节点 fiberRootNodecurrrent 指针可以访问,称为 current fiber tree;另外一颗是更新过程中构建的 new fiber tree,称为 workInProgress fiber tree

diff 比较,就是在构建 workInProgress fiber tree 的过程中,判断 current fiber tree 中的 fiber node 是否可以被 workInProgress fiber tree 复用。能被复用,意味在本次更新中,需要做组件的 update 以及 dom 节点的 move、update 等操作;不可复用,则意味着需要做组件的 mount、unmount 以及 dom 节点的 insert、delete 等操作。

当更新完成以后,fiberRootNode 的 current 指针会指向 workInProgress fiber tree,作为下一次更新的 current fiber tree。

关于双缓存技术以及双缓存 fiber tree,卡颂大佬在 React技术揭秘 - Fiber 架构的工作原来 一文中已做了详细讲解,如果大家感兴趣,可以去看一下。

React Concurrent

关于 Concurrent,官网是这样介绍的:

Concurrent 模式是一组 React 的新功能,可帮助应用保持响应,并根据用户的设备性能和网速进行适当的调整。

Concurrent 是 React 提供的新的特性,最关键的一点就是将原来的同步不可中断的更新变为可中断的异步更新

同步不可中断更新,意味着在更新过程中,即使产生了更高优先级的更新,原来的更新也会继续处理,等处理完毕渲染到屏幕上以后才会开始处理更高优先级的更新。

异步可中断更新,意味着同样的情况,原来的更新可先中断,优先处理更高优先级的更新,处理完毕之后才处理原来被中断的更新。

关于可中断/不可中断更新,我们通过一个 demo 来说明一下。


function Parent() {
    const [number, setNumber] = useState(1);
    const buttonRef = useRef(null);
    const add = () => { setNumber(number + 1) }
    const click = () => { buttonRef.current.click() }
    return (
        <div>
            <button ref={buttonRef} onClick={add}>修改 Parent</button>
            <span>{number}</span>
            <Child callback={click} />
        </div>
    )
 }

const Child = (props) => {
    const [number, setNumber] = useState(1);
    const click = () => {
        setTimeout(() => {
            // setTimeout 内部产生的更新,优先级为普通优先级
            setNumber(number + 1);
        }, 10)
        setTimeout(() => {
            // click 触发的更新,优先级为用户 block 优先级,要更高一些
            props.callback && props.callback();
        }, 10);
     }

    return (
        <div>
            <button onClick={click}>修改 Child + Parent</button>
            <div  className="box">
                {Array(50000).fill(number).map(item => (<span>{item}</span>))}
            </div>
        </div>
                
    )
}

在上面的 demo 中,我们点击 Child 的 button 按钮,同时给 Child 和 Parent 的 number 加 1。其中 Child 的加 1 操作先开始,并且 Parent 的加 1 操作优先级更高。

不可中断更新:

ReactDOM.render(<Parent />, document.geElementById('app'));

Jul-29-2021 10-18-07.gif

观察示例,可以很明显的看到 Child 的加 1 操作先于 Parent 的加 1 操作完成。尽管 Parent 的加 1 操作优先级更高,但仍需要等待 Child 的加 1 操作完成以后才会进行。

内部工作过程如下:

image.png

image.png

可中断更新:

ReactDOM.createRoot(document.getElementById('app')).render(<Parent />);

Jul-29-2021 10-19-15.gif

示例中可以很明显的看到 Parent 的加 1 操作先完成。在 Concurrent 模式下,尽管 Child 的加 1 操作先开始,但会被 Parent 的加 1 操作中断,等 Parent 的加 1 操作完成以后才会继续。

内部工作过程如下:

image.png

image.png

image.png

异步可中断更新,体现在 fiber tree 上,就是 workInProgress fiber tree 的构建整个过程是可中断的。在构建 workInProgress 的过程中,如果有更高优先级的更新产生, React 会停止 workInProgress fiber tree 的构建,然后开始处理更高优先级的更新,重新构建 workInProgress fiber tree。等更高优先级的更新处理完毕之后,才会处理原来被中断的更新。

对应的源码如下:

// 可中断渲染过程
function renderRootConcurrent(root, lanes) {
  ...
  // workInProgressRootRenderLanes !== lanes,意味着有更高优先级的更新需要处理
  if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
    ...
    // 原来的更新中断,从头开始构建 workInProgress tree
    prepareFreshStack(root, lanes);
    ...
  }
  ...
}
function prepareFreshStack(root, lanes) {
  ...
  workInProgressRoot = root;
  // 从头开始构建 workInProgress tree
  workInProgress = createWorkInProgress(root.current, null);
  ...
}

我们在源码中添加如下 console.log

function renderRootConcurrent(root, lanes) {
  ...
  // workInProgressRootRenderLanes !== lanes,意味着有更高优先级的更新需要处理
  if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
    ...
    console.log('重置 workInProgress tree', workInProgressRootRenderLanes, lanes)
    // 原来的更新中断,从头开始构建 workInProgress tree
    prepareFreshStack(root, lanes);
    ...
  }
  ...
}

再运行上面的示例,我们就可以清楚的看到优先级变更导致 workInProgress fiber tree 重置的过程。

Jul-29-2021 13-48-57.gif

到这里,我们已经可以理解为什么 React 要采取双缓存 fiber tree 结构了。current fiber tree,记录上一次更新结束的状态;workInProgree fiber tree,更新过程中创建的 new tree,可随着更新优先级的变化随时重置。

试想一下,如果采取 single fiber tree,那么异步可中断更新将会发生下面这样的情况:

image.png

想必这种情况,也不是我们所期望的。

最后

当然,React 采取双缓存 fiber tree,不仅仅只是为了更急迫的更新可以中断已经开始的渲染,还服务于 Suspense、useTransition、useDeferedValue 等新特性。由于文章篇幅和自己还未完全理解的情况,在这里就不做说明了,后面等梳理明白以后再开新的文章来介绍。

由于本人水平有限,文中内容可能存在理解不到位或者错误的情况,希望大家能留言指出,😊 。共同学习,一起进步。

参考文档