🎉干货满满,React设计原理(二):藏在源码里的两个圈🎉
💡相关阅读
文章首发公众号:萌萌哒草头将军,最近关注有🎁,欢迎关注
💎 第二座大山:链表结构和双缓存机制
上篇文章中讲述了几个容易给源码阅读造成困扰的几个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示意图如下:
实际的Fiber树状链表结构如下:
此时对应的是mounted阶段的初始状态,如果我们点击一次按钮,新的树状链状结构(对应updated阶段)如下:
对比两次的Fiber数据结构,从中我们可以得出结论:
- 🔥 在函数组件对应的链表结构中,
React每次将更新的内容渲染在页面之后,会将组件里的每个useState返回的状态记录在memoizedState下的baseState属性上,返回的dispatch方法有queue属性上,同时使用next属性指向下一个状态。直到最后一个状态时,next为null。这是我们发现的第二条链状结构。
- 🔥 另外我们还发现,
button所在的fiber结构中,memoizedProps、pendingProps属性上存在children、onClick属性
- 🔥 我们还发现,更新之后,每个
fiber结构的alternate都指向了上次的自己。这其实是双缓存机制的实现,下面我们还会讲到。
如果我们将上面的函数组件替换为具有同样功能的类组件时(代码如下)
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>
</>
);
}
}
它的树状链表结构如下:
这里我们发现类组件和函数组件不一样的地方:
- 🔥 类组件的
fiber结构的memoizedState属性仅仅对应this.state的值,没有了想函数组件的第二条链表。
- 🔥 类组件的
fiber结构的updateQueue属性承载了组件的更新信息。这里的更新我们以后会详细讲到的。
总结下,React会为不同类型的Fiber tree节点创建不同的数据结构(略微不同的FiberNode类型),不同的数据结构更新方式也不一样。
除了上面说到的类组件和函数组件,还有Fargement、Suspense内置组件类型和一些别的情况下的特殊组件。
🚗 双缓存机制
上面提到,更新之后每个fiber节点的alternate属性都会指向上次的自己。其实这是React的一种优化策略。
React在运行时解析vnode,更新之后标记出更新前后变动的dom,然后渲染在页面中。如果每次都重新生成新的dom显然十分浪费资源。
所以React一方面会为每个dom绑定上次的状态,当发生变更时,快速比对,找出变动的地方。
另一方面,React还在内存中维护了一棵Fiber tree,变量名为workInProgress,用于快速切换。
源码中,所有带着
workInProgressXxx的变量,都是指运行在内存中的对象。比如workInProgressHook
上篇文章中提到过,每个应用都会有唯一的FiberRootNode实例用来维护整个应用的状态和组件信息。它有个current属性用于指向渲染在页面中的fiber tree,而每个fiber节点alternate指向另一棵树中的自己。
接下来我们从组件开始加载到更新,看看双缓存机制的作用过程。
首先是应用被建立。App组件还未还未加载,此时是FiberRootNode的current属性为null:
在App组件解析成vMNode后,还在内存workInProgress中时:
当将vNode渲染在浏览器时,FiberRootNode的current属性指向workInProgress,workInProgress置空操作:
此时,我们点击button的点击事件,触发更新,内存中又多了个一棵树:
通过alternate属性比对,发现是App组件状态发生改变了,所以从App组件开始替换子树,然后将FiberRootNode的current属性指向workInProgress成为新的curent属性,旧的current替换之后成为workInProgress,并置为空,等待下次的更新:
这里我小小地剧透下,上述整个过程主要是
render阶段地内容。具体而言,render阶段又可以分为三个小阶段:
beginWork阶段:顺着child属性向下遍历,找到变化地地方,打上标记complateWork阶段:顺着return属性向上回归,将有标记的地方更新,此时就是更新workInProgress对应地Fiber treecommitRoot阶段:将workInProgress对应的Fiber tree渲染到页面,同时完成上述指针的切换工作。
🚗 总结
React为不同的节点类型构建了不同的fiber结构和更新机制,但总的来说,它们具有同样的链表结构。
本文重点介绍了类组件和函数组件的一些字段区别。另外通过alternate引出并介绍了双缓存机制:current和workInProgress的循环往替更新。
就是这两个重要的”圈“,给React套上了神秘的面纱。
🎉 最后
如果你发现本文一些错误的地方,请不吝指正,肥肠感谢🙏
这是本系列的第二篇了,真的干货满满,全文近六千五字符。
这个系列的目的通过分析一些理论知识,降低阅读源码的难度,即使不读源码也会对React的设计思想有总体上的理解。
- 🎉干货满满,React设计原理(一):藏在源码里的紧箍咒,几个容易混淆的变量🎉
- 🎉干货满满,React设计原理(二):藏在源码里的两个圈,关键的链表结构和双缓存技术🎉
- 🎉干货满满,React设计原理(三):藏在源码里的排位赛,
Lanu模型和Batched Updates🎉 - 🎉干货满满,React设计原理(四):藏在源码里的传呼机,
Dispatch机制和事件系统🎉 - 🎉干货满满,React设计原理(五):藏在源码里的xx,待定🎉
所以对你有帮助话请给我点下赞,这对我很重要!
