这几个关键的数据结构都不会,你也配学react源码???
不知道大家在学习react源码的时候有没有这样的感觉:fiber对象的结构太复杂了,不仅是属性繁多,而且有些属性还是个巨复杂的对象。我在学习hooks的时候,这种感觉尤为强烈。那么,这篇文章就以fiber.memoizedState和fiber.updateQueue这两个重要属性为例,梳理一下他们的结构和作用,希望对大家阅读源码有帮助。
先来看类组件
首先来看类组件。类组件的fiber.memoizedState和fiber.updateQueue比较简单,memoizedState就是我们定义的state对象,updateQueue的结构如下:
fiber.updateQueue {
baseState,
firstBaseUpdate,
lastBaseUpdate,
shared: {
pending,
},
effects
}
首先,前三个属性baseState、firstBaseUpdate和lastBaseUpdate是和可中断更新相关的。在concurrent mode下,当前的更新可能被更高优先级的更新打断,而baseState就记录了被跳过的update节点之前计算出的state,firstBaseState和lastBaseUpdate构成一条链表,记录因为优先级不足而被跳过的所有更新。shared.pending才是真正记录了所有update对象的链表,这是一条环形链表,shared.pending指向该链表的最后一个update对象,effects则是一个数组,存储了setState的回调函数。关于类组件的state计算原理,可以看我的这篇文章,这里面有更加详细的讲解,本文还是主要介绍数据结构。
看函数组件
了解hooks原理的同学应该都知道,react使用链表来存储函数组件中用到的所有hooks,而这个链表就保存在fiber.memoizedState。这个hooks链表的每一项都是hook对象,hook对象的结构如下
hook {
memoizedState,
baseState,
baseQueue,
queue,
next
}
没错,
fiber.memoizedState指向了hook链表,而每个hook对象还有一个memoizedState对象!!!
不论是哪种hook,都会使用这一种数据结构,而有些hook可能只会用到其中的几个属性。下面,就由简入繁,介绍不同的hook都用到了哪些属性。
useCallback, useMemo, useRef
这三个hook都只使用到了hook.memoizedState这一个属性。
useCallback接受一个需要缓存的函数和依赖数组,而hook.memoizedState则保存了一个数组,数组内放了缓存函数和依赖数组
function mountCallback(callback, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
useMemo和useCallback基本一致,只不过useMemo保存了函数的返回值
function mountMemo(nextCreate, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
var nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
至于useRef就能简单了,保存一个对象即可
function mountRef(initialValue) {
var hook = mountWorkInProgressHook();
var ref = {
current: initialValue
};
{
Object.seal(ref);
}
hook.memoizedState = ref;
return ref;
}
这里使用Object.seal封闭了ref对象,Object.seal就是在不可扩展(Object.preventExtensions)的基础上不可配置(configurable变为false)。
useState
看完了三个简单的hook,现在加大一点难度,看看useState。
useState就比较复杂了,用到了hook对象身上的所有属性
hook {
memoizedState: 保存的state,
baseState:和类组件的updateQueue.baseState作用相同,
baseQueue: 和类组件的updateQueue.firstBaseUpdate和lastBaseUpdate作用相同,
queue: {
pending: 保存update对象的环状链表,
dispatch: dispatchAction,useState的第二个返回值,
lastRenderedReducer: 上次更新时用到的reducer,
lastRenderedState: 上次更新的state
}
}
这里需要注意一点,useState中用到了hook.queue这个属性,而hook.queue.pending和类组件的updateQueue.shared.pending比较类似,都是用来保存update对象的。而使用useState时,fiber.updateQueue是null,也就是说和类组件不同,useState并没有用到updateQueue,这一点容易让人困惑。
useEffect
接下来看看我个人认为最复杂的hook:useEffect。说他复杂,是因为useEffect的数据结构很绕,下面一起来看一下
首先,关于hook对象身上的几个属性,useEffect只用到了一个,就是memoizedState,而在memoizedState上保存的又是一个effect对象
hook {
memoized: {
tag: 用于区分是useEffect还是useLayoutEffect,
create: 就是useEffect的回调函数,
destory: useEffect回调的返回值,
deps: 依赖数组,
next: 下一个effect对象,构成环装链表
},
baseState: null,
baseQueue: null,
queue: null
}
此外,useEffect还用到了fiber.updateQueue,这个updateQueue的结构还和类组件的不一样
// 类组件的updateQueue
{
baseState,
firstBaseUpdate,
lastBaseUpdate,
shared: {
pending
},
effects
}
// useEffect的updateQueue
{
lastEffect
}
useEffect不仅将effect对象放在了hook.memoized上,还放在了fiber.updateQueue上,并且都是环形链表
举个🌰
前面说了这么多,可能大家还是比较困惑,下面看个实际的例子
const App = () => {
const [num, setNum] = useState(1)
const handleClick1 = useCallback(() => {
setNum((pre) => pre + 1)
}, [])
const [count, setCount] = useState(1)
const handleClick2 = useCallback(() => {
setCount((pre) => pre + 1)
}, [])
useEffect(() => {
console.log('1st effect', num)
}, [num])
useEffect(() => {
console.log('2nd effect', num)
}, [num])
useLayoutEffect(() => {
console.log('use layout', count)
}, [count])
return (
<div>
<p>{num}</p>
<p>{count}</p>
<button onClick={handleClick1}>click me</button>
<button onClick={handleClick2}>click me</button>
</div>
)
}
上面的例子中,我们使用了两个useState,并且每个事件处理函数都使用useCallback进行了包装,此外还有useEffect和useLayoutEffect,下面看一下
- 首先,执行
useState,因此在fiber.memoizedState上挂载了一个useState的hook对象
- 接下来执行到
useCallback,在hooks链表中再添加一项
- 接下来又是
useState和useCallback,因此和前两步一样
- 之后来到比较复杂的
useEffect了,useEffect不仅会添加hook对象到hooks链表中,还会修改updateQueue
- 后面还是一个
useEffect,这一步就比较复杂了
可以看到,使用useEffect时,不仅构成了hooks链表,effect对象也构成了一条环形链表
- 之后执行了
useLayoutEffect
最后来总结一下这个例子
链接:https://juejin.cn/post/6993150359317250085