我终于捋清fiber树是如何创建的了

2024年1月12日,星期五,我觉得在React源码方面,经历了很多次的挣扎、理解、痛苦后,我终于入门了。本次给大家带来的东西是fiber树的构建。拆解的会比较细,所以,请大家系好安全带,我要发车了。

本次讲解的React源码是17版本(当然,后续肯定会更新React18版本的讲解),请看如下代码:

react 复制代码
import React from 'react';
import ReactDOM from 'react-dom';

class Component1 extends React.Component {
    constructor(props) {
        super(props)
    }
    render(){
        return <div className = 'component-father-1'>
            <div className = 'component-child-1'>
                <div className = 'c1'>qw1</div>
                <div className = 'c2'>qw2</div>
                <div className = 'c3'>qw3</div>
            </div>
        </div>;
    }
}

ReactDOM.render(
    <Component1 />,
    document.getElementById('app')
);

从上图我们可以看到,ReactDOM.render实际上是调用了LegacyRenderSubtreeIntoContainer方法,而我们的<Component1 />组件在传入到render方法里的时候,就已经被转换为了虚拟DOM。那它是如何被转为虚拟DOM的?一句话:babel碰到<Component />这种写法,会把它转译为React.createElement方法,这个方法会返回一个对象,这个对象就叫做虚拟DOM,可以用来描述组件信息

等等,我们好像跑题啦,接着看一下LegacyRenderSubtreeIntoContainer方法吧:

jsx 复制代码
ReactDOM.render( <Component1 />, document.getElementById('root') );

// 等价于下面的写法

legacyRenderSubtreeIntoContainer(
    null, 
    {
        $$typeof: Symbol(react.element),
        props: {},
        type: 名为Component1的构造函数
    }, 
    container, 
    false, 
    undefined
);

上图就是LegacyRenderSubtreeIntoContainer方法的源码,从这里开始,我希望大家每次读完一段讲解,都要去回顾一下之前讲到的内容。

虽然代码有点多,但是我们会发现,上述代码可以精简如下:

jsx 复制代码
function legacyRenderSubtreeIntoContainer(null, children, container, false, undefined) {
    var root = container._reactRootContainer;
    if (!root) {
          // Initial mount
          root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, false);
          fiberRoot = root._internalRoot;
          unbatchedUpdates(function () {
            updateContainer(children, fiberRoot, null, undefined);
          });
     }
}

这个方法首先创建了一个对象,并将这个对象赋值给了 root、container._reactRootContainer。这个对象的样子如下:

仔细观察上图与代码,这个时候出现了一个隐藏的关系:

到目前为止,我相信很多小伙伴应该已经疑惑,你给我弄那么多意思相近的名词干什么。请注意,这些名词不是我定义,是代码里给到的信息而已,我只是先将它图形化了。

紧接着,我们就执行了updateContainer方法。调用这个方法的具体信息如下:

jsx 复制代码
updateContainer(
    {
        $$typeof: Symbol(react.element),
        props: {},
        type: 名为Component1的构造函数
    },
    FiberRoot,
    null,
    undefined
)

updateContainer方法里,先是计算fiber的优先级,然后再根据优先级创建更新对象,随后又将update存了起来,最后是调度当前的fiber节点。

jsx 复制代码
function updateContainer(element, container, null, undefined) {
    var current$1 = container.current;
    // 其余代码在本次内容里不必关注
    scheduleUpdateOnFiber(current$1, lane, eventTime);
}

我们现在来看看这个current$1 是个什么意思。current$1就是FiberRoot.current,也就是fiberNode,这个fiberNode是谁的描述对象呢?大家可以看一下当前fiberNode的信息,它其实就是id为app的dom节点对应的fiber节点。

我们接着来看一下scheduleUpdateOnFiber方法,我们来精简一下代码,只关注跟本次内容相关的东西:

jsx 复制代码
function scheduleUpdateOnFiber(fiber, lane, eventTime) {
    // root就是fiberRoot
    var root = markUpdateLaneFromFiberToRoot(fiber, lane);
    // 将整个fiberRoot传给下面的方法
    performSyncWorkOnRoot(root);
}

注意,注意啊,markUpdateLaneFromFiberToRoot返回的是fiberRoot,还记得下面这张图吗:

接下来我们再看一下performSyncWorkOnRoot方法。这个方法精简后的代码如下:

jsx 复制代码
function performSyncWorkOnRoot(root){
    // 忽略其余代码.........
    renderRootSync(root, 1);
    // 忽略其余代码.........
}

紧接着我们来看一下renderRootSync代码,我们重点关注下面的几行代码:

jsx 复制代码
function renderRootSync(root, lanes) {
    // 忽略其余代码.....
    
    prepareFreshStack(root, lanes);
    do {
      try {
        workLoopSync();
        break;
      } catch (thrownValue) {
        handleError(root, thrownValue);
      }
    } while (true);
    
    // 忽略其余代码.....
}

function workLoopSync(){
    while (workInProgress !== null) {
      performUnitOfWork(workInProgress);
    }
}

从上面的代码来看,prepareFreshStack方法的目的就是创建全局变量workInProgress,好能开启工作循环,而在工作循环中,fiber树一点一点的被创建出来。

我们先来看看prepareFreshStack是如何创建workInProgress的:

jsx 复制代码
function prepareFreshStack(root, lanes) {
    workInProgressRoot = root;
    workInProgress = createWorkInProgress(root.current, null);
}

上面的方法其实就是根据当前id为app的dom对应的fiber(下面都以rootFiber代替)再创建一个fiber,这个新的fiber就是workInProgress,具体关系如下:

现在我们已经成功的创建了workInProgress,接下来就该进入工作循环了:

jsx 复制代码
while (workInProgress !== null) {
  performUnitOfWork(workInProgress);
}

function performUnitOfWork(unitOfWork) {
    var current = unitOfWork.alternate;
    setCurrentFiber(unitOfWork);
    var next;

    next = beginWork$1(current, unitOfWork, subtreeRenderLanes);

    resetCurrentFiber();
    
    unitOfWork.memoizedProps = unitOfWork.pendingProps;

    if (next === null) {
      // 这块暂时不用管
      completeUnitOfWork(unitOfWork);
    } else {
      workInProgress = next;
    }

    ReactCurrentOwner$2.current = null;
}

React源码有一点好处,即使你没读过performUnitOfWork方法,你也能猜出来他要干啥。从根元素向下,不断的通过 beginWork方法 查找自己的子元素,并将他们连接起来

接下来我们就看看,React是如何通过beginWork方法将 当前的rootFiber(workInProgress) 与 我们的<Component1 /> 组件关联起来的:

jsx 复制代码
function beginWork(current, workInProgress, renderLanes){
    // 省略其他代码...
    
    switch (workInProgress.tag) {
        case HostRoot:
            return updateHostRoot(current, workInProgress, renderLanes);
        case ClassComponent:
            return updateClassComponent(current, workInProgress, _Component2, _resolvedProps, renderLanes);
    }
    
    // 省略其他代码....
}

beginWork其实就是根据当前fiber类型不同,做不同的处理,但中心思想都是创建子元素的fiber对象并返回,下面列举了几个fiber类型。

HostRoot:根元素对应的fiber(注意,它并不是fiberRoot,它是rootFiber,忘记的小伙伴回顾一下之前的图片)。

ClassComponent: 类组件对应的fiber。

HostComponent: dom节点对应的fiber。

FunctionComponent: 函数式组件对应的fiber。

因为workInProgress当前指向的是 rootFiber,所以此时会调用 updateHostRoot方法,这个方法主要干了以下几件事:

  • 调用reconcileChildren方法创建子fiber对象。
  • 并将子fiber对象赋值给 workInProgress.child方法。
  • 返回第一步新创建的子fiber对象。

其中,reconcileChildren方法就是React Diff算法,这个算法的主要目的就是创建新的rootFiber。经过上面的步骤后,我们可以得到下面的状态:

拿到上面的状态后,意味着beginWork方法也结束了。此时回退到performUnitOfWork方法里,将next指针指向最新的fiber节点,我们又可以得到下一个状态:

得到这个状态后,意味着本轮的workloop结束了,开启下一轮workloop。

开启下一个workloop,class组件的fiber是如何拿到它的子元素呢?我们别忘了,当前Component1组件可是用class写的,所以beginWork会调用updateClassComponent方法,而这个方法最终会通过instance.render()来拿到当前class组件里的子元素。我们此时会得到下一个状态:

再开启下一个workloop,对于一个dom节点来说,它如何构建下一个子fiber呢?因为dom原声标签对应的tag是HostComponent,所以调用beginWork时,其实是调用的updateHostComponent方法。当我们给一个原生标签创建fiber时,我们是一定能够知道当前标签的children是谁(为什么?因为React在工作前,一定会经过babel处理,我们是可以拿到组件对应的虚拟dom的),然后把children属性放到pendingProps里,这样再构建子fiber时,直接从pendingProps里获取子元素就可以了。

拿到子元素后,构建fiber的流程跟上面一样,我们又得到了一个状态:

再开启一轮工作循环,当前workInProgress对应的子节点有3个,分别是c1、c2、c3。因为子元素有多个,所以再进行diff算法的时候,会调用reconcileChildrenArray方法,然后每个子fiber之间通过sibling连接,形成下面的最终态:

总结

下面来用一句话总结一下fiber树的构建:

如果React版本是16、17,那么React会首先为app建立rootFiber,随后开启工作循环,工作循环的目的就是根据当前fiber,去创建子fiber,并将子fiber通过return、child属性跟当前fiber关联起来,最终形成一颗fiber树

最后

这篇文章终于写完了,写React源码方面的文章真是太累了,因为React关联太密切了,你又很想讲细,但又怕文章字数太多,所以很容易导致烂尾~~

精选好文

Fiber树的构建

相关推荐
小镇程序员1 小时前
vue2 src自定义事件
前端·javascript·vue.js
炒毛豆2 小时前
vue3+echarts+ant design vue实现进度环形图
javascript·vue.js·echarts
nameofworld3 小时前
前端面试笔试(六)
前端·javascript·面试·学习方法·递归回溯
前端fighter4 小时前
js基本数据新增的Symbol到底是啥呢?
前端·javascript·面试
流着口水看上帝4 小时前
JavaScript完整原型链
开发语言·javascript·原型模式
guokanglun4 小时前
JavaScript数据类型判断之Object.prototype.toString.call() 的详解
开发语言·javascript·原型模式
GISer_Jing4 小时前
从0开始分享一个React项目:React-ant-admin
前端·react.js·前端框架
Embrace9244 小时前
为什么 Vue2会出现数据更新视图不更新 Vue3不会出现
javascript·vue.js·ecmascript
qq_415628174 小时前
bpmn.js显示流程图
javascript·vue.js·流程图
豆子熊.5 小时前
外包干了3年,技术退步明显...
软件测试·selenium·测试工具·面试·职场和发展