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