💎 第二座大山:链表结构和双缓存机制
上篇文章中讲述了几个容易给源码阅读造成困扰的几个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 tree
commitRoot
阶段:将workInProgress
对应的Fiber tree
渲染到页面,同时完成上述指针的切换工作。
🚗 总结
React
为不同的节点类型构建了不同的fiber
结构和更新机制,但总的来说,它们具有同样的链表结构。
本文重点介绍了类组件和函数组件的一些字段区别。另外通过alternate
引出并介绍了双缓存
机制:current
和workInProgress
的循环往替更新。
就是这两个重要的”圈“,给React
套上了神秘的面纱。
🎉 最后
如果你发现本文一些错误的地方,请不吝指正,肥肠感谢🙏
这是本系列的第二篇了,真的干货满满,全文近六千五字符。
这个系列的目的通过分析一些理论知识,降低阅读源码的难度,即使不读源码也会对React
的设计思想有总体上的理解。
- 🎉干货满满,React设计原理(一):藏在源码里的紧箍咒,几个容易混淆的变量🎉
- 🎉干货满满,React设计原理(二):藏在源码里的两个圈,关键的链表结构和双缓存技术🎉
- 🎉干货满满,React设计原理(三):藏在源码里的排位赛,
Lanu模型
和Batched Updates
🎉写作中... - 🎉干货满满,React设计原理(四):藏在源码里的传呼机,
Dispatch
机制和事件系统🎉写作中... - 🎉干货满满,React设计原理(五):藏在源码里的xx,待定🎉
所以对你有帮助话请给我点下赞,这对我很重要!