🎉干货满满,React设计原理(二):藏在源码里的两个圈🎉

1,213 阅读6分钟

💡相关阅读

文章首发公众号:萌萌哒草头将军,最近关注有🎁,欢迎关注

💎 第二座大山:链表结构和双缓存机制

上篇文章中讲述了几个容易给源码阅读造成困扰的几个fiber相关的变量名称,这篇我将介绍下Fiber架构的链表结构和双缓存机制。

上文提到,FiberNode扮演多种角色时,保存着不同的数据,所以FiberNode保存的数据比较复杂。

本文重点,讲解作为Fiber架构的一环时,保存的链状数据结构(同时也会捎带的讲解其他的一些属性),以及双缓存机制,

🚗 链表结构

Fiber tree由多个FiberNode节点组成的树状链表结构的数据。每个FiberNode 的节点都有以下几个和Fiber架构相关的重要属性:

// 指向父节点
this.return = null;
// 指向第一个子节点
this.child = null;
// 指向右边兄弟节点
this.sibling = null;

虽然根据不同的节点类型(比如函数组件、类组件、普通元素等)数据结构会有所不同,但是它们都会使用这三个属性描述它与它们相邻节点的关系。

比如,有如下的代码:

function App() {
  const [name, setName] = useState("mmdctjj");
  const [count, setCount] = useState(0);
  return (
    <>
      <button
        onClick={() => {
          setName(name => name + 'l')
          setCount(count => count + 1)
        }}
      >
        {count}--{name}
      </button>
    </>
  );
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

它们的Fiber tree示意图如下:

image.png

实际的Fiber树状链表结构如下:

image.png

此时对应的是mounted阶段的初始状态,如果我们点击一次按钮,新的树状链状结构(对应updated阶段)如下:

image.png

对比两次的Fiber数据结构,从中我们可以得出结论:

  • 🔥 在函数组件对应的链表结构中,React每次将更新的内容渲染在页面之后,会将组件里的每个useState返回的状态记录在memoizedState下的baseState属性上,返回的dispatch方法有queue属性上,同时使用next属性指向下一个状态。直到最后一个状态时,nextnull。这是我们发现的第二条链状结构。

image.png

  • 🔥 另外我们还发现,button所在的fiber结构中,memoizedPropspendingProps属性上存在childrenonClick属性

image.png

  • 🔥 我们还发现,更新之后,每个fiber结构的alternate都指向了上次的自己。这其实是双缓存机制的实现,下面我们还会讲到。

image.png

如果我们将上面的函数组件替换为具有同样功能的类组件时(代码如下)

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      count: 0,
      name: "mmdctjj",
    };
  }

  render() {
    return (
      <>
        <button
          onClick={() =>
            this.setState({
              count: this.state.count + 1,
              name: this.state.name + "l",
            })
          }
        >
          {this.state.count}--{this.state.name}
        </button>
      </>
    );
  }
}

它的树状链表结构如下:

image.png

这里我们发现类组件和函数组件不一样的地方:

  • 🔥 类组件的fiber结构的memoizedState属性仅仅对应this.state的值,没有了想函数组件的第二条链表。

image.png

  • 🔥 类组件的fiber结构的updateQueue属性承载了组件的更新信息。这里的更新我们以后会详细讲到的。

image.png

总结下,React会为不同类型的Fiber tree节点创建不同的数据结构(略微不同的FiberNode类型),不同的数据结构更新方式也不一样。

除了上面说到的类组件和函数组件,还有FargementSuspense内置组件类型和一些别的情况下的特殊组件。

🚗 双缓存机制

上面提到,更新之后每个fiber节点的alternate属性都会指向上次的自己。其实这是React的一种优化策略。

React在运行时解析vnode,更新之后标记出更新前后变动的dom,然后渲染在页面中。如果每次都重新生成新的dom显然十分浪费资源。

所以React一方面会为每个dom绑定上次的状态,当发生变更时,快速比对,找出变动的地方。

另一方面,React还在内存中维护了一棵Fiber tree,变量名为workInProgress,用于快速切换。

源码中,所有带着workInProgressXxx的变量,都是指运行在内存中的对象。比如workInProgressHook

上篇文章中提到过,每个应用都会有唯一的FiberRootNode实例用来维护整个应用的状态和组件信息。它有个current属性用于指向渲染在页面中的fiber tree,而每个fiber节点alternate指向另一棵树中的自己。

接下来我们从组件开始加载到更新,看看双缓存机制的作用过程。

首先是应用被建立。App组件还未还未加载,此时是FiberRootNodecurrent属性为null

image.png

App组件解析成vMNode后,还在内存workInProgress中时:

image.png

当将vNode渲染在浏览器时,FiberRootNodecurrent属性指向workInProgressworkInProgress置空操作:

image.png

此时,我们点击button的点击事件,触发更新,内存中又多了个一棵树:

image.png

通过alternate属性比对,发现是App组件状态发生改变了,所以从App组件开始替换子树,然后将FiberRootNodecurrent属性指向workInProgress成为新的curent属性,旧的current替换之后成为workInProgress,并置为空,等待下次的更新:

image.png

这里我小小地剧透下,上述整个过程主要是render阶段地内容。具体而言,render阶段又可以分为三个小阶段:

  • beginWork阶段:顺着child属性向下遍历,找到变化地地方,打上标记
  • complateWork阶段:顺着return属性向上回归,将有标记的地方更新,此时就是更新workInProgress对应地Fiber tree
  • commitRoot阶段:将workInProgress对应的Fiber tree渲染到页面,同时完成上述指针的切换工作。

🚗 总结

React为不同的节点类型构建了不同的fiber结构和更新机制,但总的来说,它们具有同样的链表结构。

本文重点介绍了类组件和函数组件的一些字段区别。另外通过alternate引出并介绍了双缓存机制:currentworkInProgress的循环往替更新。

就是这两个重要的”圈“,给React套上了神秘的面纱。

🎉 最后

如果你发现本文一些错误的地方,请不吝指正,肥肠感谢🙏

这是本系列的第二篇了,真的干货满满,全文近六千五字符。

这个系列的目的通过分析一些理论知识,降低阅读源码的难度,即使不读源码也会对React的设计思想有总体上的理解。

所以对你有帮助话请给我点下赞,这对我很重要!

image.png