React16:Fiber架构学习总结

您好,我是沧沧凉凉,是一名前端开发者,目前在掘金知乎以及个人博客上同步发表一些学习前端时遇到的趣事和知识,欢迎关注。


本篇文章紧跟着前一篇文章,前文中谈到了Fiber节点,那么由Fiber节点构成的树就叫做Fiber树,同时它也对应着DOM树,而如果要用Fiber树更新DOM树,那么这个时候就需要用到一个“双缓存”技术。

试想一下,如果仅仅一颗Fiber树,那么每次需要更新界面上的DOM树的时候,都需要通过diff算法对Fiber树进行增删改,一旦所需要更改的节点过多,可能会造成界面闪烁的情况,就像电视一样,如果每一个画面都是在清除电视上显示的图像才进行绘制的话,那就会不停的闪烁。

所以在React中最多会存在两颗Fiber树。

  • 当前屏幕上显示的DOM对应的Fiber树叫做:current Fiber树,树中节点叫做current fiber。
  • 正在内存中进行构建的Fiber树叫做workInProgress Fiber树,树中的节点叫做workInProgress fiber。

这两个树之间的节点通过alternate属性连接。

注意:上面的概念非常重要!后面在讲解diff算法的时候,会反复提到,所以一定要有一个印象!可能现在看起来比较抽象,不过没关系,下面会具体讲解如何变化。

下面这两个概念也非常重要,基本贯穿了整个React深入学习,分别为:

  • mount时:首次执行ReactDOM.render时。
  • update时:触发状态更新时。

这里提前剧透一下:在react-hooks的原理部分,mount时和update时,react调用的hook的方法是不同的,比如说useState,在mount时会调用对应的mountState方法,而在update时会调用updateState方法。

mount

mount代表的是应用初始化的时候,这点

在调用ReactDOM.render方法后,会调用createFiberRoot方法,创建FiberRoot节点,然后调用createHostRootFiber方法,创建HostRootFiber节点。

在createFiberRoot源码中是这么写的:

function createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks) {
  var root = new FiberRootNode(containerInfo, tag, hydrate);
  
  // 这里执行了 createHostRootFiber 生成了 HostRootFiber
  var uninitializedFiber = createHostRootFiber(tag);
  // 将 FiberRoot 的 current 指向 HostRootFiber
  root.current = uninitializedFiber;
  // 将 HostRootFiber 的 stateNode 指向 FiberRoot
  uninitializedFiber.stateNode = root;
  // 下面是更新队列相关的,暂时不用管
  initializeUpdateQueue(uninitializedFiber);
  return root;
}

为什么上面的源码看起来不一样?因为他们是来自react-dom打包后的文件react-dom.development.js中。

从上面的源码中,可以很清晰的看到FiberRoot节点指向HostRootFiber节点的过程。

这个FiberRoot节点不同于普通的Fiber对象,它单独实例化了一个对象FiberRootNode,而createHostRootFiber方法就是实例化的一个普通的Fiber对象,即FiberNode

至于FiberRootNode对象我们暂时就不深入研究,我们先来看一下FiberNode对象中有些什么东西:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // 作为静态数据结构的属性
  // 在总结JSX的时候会提到
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // 用于连接其他Fiber节点形成Fiber树
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  // 作为动态的工作单元的属性,也就是和Hooks相关
  // 在后面总结Hooks的时候会提到
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  // Effects
  this.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.deletions = null;

  // 调度优先级相关
  // 在后面总结lane模型的时候会提到
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  // 指向该Fiber在另一次更新时对应的Fiber
  this.alternate = null;
}

这里多说一点,React的源码看着有点像TypeScript,其实它是使用的自家的静态类型检查器Flow,所以语法上可能和TypeScript还是有点区别,不过这不妨碍我们对源码的阅读。

从Fiber对象中可以看出,它具有非常多的属性,在后面的总结文章中,我会提到大部分属性究竟是用来做什么的,所以现在看看就好,千万不要一头扎进去,就像一瞬间搞清楚这些属性的作用,随着往后深入,这些属性的作用也会逐步在你的脑海中形成一个较为清晰的印象。

其中FiberRoot是整个应用的根节点,HostRootFiber是<App />所在组件树的根节点。

在应用中,可以多次调用ReactDOM.render方法,生成不同的组件树,它们拥有不同的HostRootFiber,但是整个应用的根节点只有一个,那就是FiberRoot节点。

这个时候FiberRoot的current会指向页面上已渲染内容对应Fiber树,即current Fiber树

image-20220323234613614

update

我们看一下下面的代码:

function App() {
  const [count, setCount] = useState(0);
  return <p onClick={() => setCount(count + 1)}>{count}</p>;
}

即点击p标签,

update

可以看到,当页面的状态更新的时候,在控制台的元素中只有p标签发现了变化,而决定workInProgress fiber树中的创建是否可以复用current Fiber树中的节点,这个过程就是靠diff算法来进行实现,

点击p标签后,整体流程如下:

updateFiber

正如上面所说的,在Fiber中最多有两棵树,一颗叫做current Fiber树,一颗叫做workInProgress Fiber树,当workInProgress Fiber树构建完成后交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,那么此时的workInProgress Fiber树就会变成current Fiber树

也就是通过这两棵树之间的替换,来完成DOM的更新。

最后

本文的内容不多,但是起到了一个承上启下的作用,主要是为了引出后面的内容。

同样,本文参考了React技术揭秘,省略了一些我感觉在现阶段理解比较困难的内容,如果有兴趣还是推荐去阅读一下原文。

可能看到这里你对于Fiber架构已经大致有一个概念了,但是你可能还有一些疑问,其实包括我现在在整理学习到的知识的时候都还有非常多的疑问,但是我相信随着我逐步整理,我的疑问会变得越来越少。