写在前面的小结
- 每一个 hook 函数都有对应的 hook 对象保存状态信息
useContext
是唯一一个不需要添加到 hook 链表的 hook 函数- 只有 useEffect、useLayoutEffect 以及 useImperativeHandle 这三个 hook 具有副作用,在 render 阶段需要给函数组件 fiber 添加对应的副作用标记。同时这三个 hook 都有对应的 effect 对象保存其状态信息
- 每次渲染都是重新构建 hook 链表以及 收集 effect list(fiber.updateQueue)
- 初次渲染调用 mountWorkInProgressHook 构建 hook 链表。更新渲染调用 updateWorkInProgressHook 构建 hook 链表并复用上一次的 hook 状态信息
Demo
可以用下面的 demo 在本地调试
import React, {
useState,
useEffect,
useContext,
useCallback,
useMemo,
useRef,
useImperativeHandle,
useLayoutEffect,
forwardRef,
} from "react";
import ReactDOM from "react-dom";
const themes = {
foreground: "red",
background: "#eeeeee",
};
const ThemeContext = React.createContext(themes);
const Home = forwardRef((props, ref) => {
debugger;
const [count, setCount] = useState(0);
const myRef = useRef(null);
const theme = useContext(ThemeContext);
useEffect(() => {
console.log("useEffect", count);
}, [count]);
useLayoutEffect(() => {
console.log("useLayoutEffect...", myRef);
});
const res = useMemo(() => {
console.log("useMemo");
return count * count;
}, [count]);
console.log("res...", res);
useImperativeHandle(ref, () => ({
focus: () => {
myRef.current.focus();
},
}));
const onClick = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div style={
{ color: theme.foreground }} ref={myRef} onClick={onClick}>
{count} </div>
);
});
ReactDOM.render(<Home />, document.getElementById("root"));
fiber
React 在初次渲染或者更新过程中,都会在 render 阶段创建新的或者复用旧的 fiber 节点。每一个函数组件,都有对应的 fiber 节点。
fiber 的主要属性如下:
var fiber = {
alternate,
child,
elementType: () => {
},
memoizedProps: null,
memoizedState: null, // 在函数组件中,memoizedState用于保存hook链表
pendingProps: {
},
return,
sibling,
stateNode,
tag, // fiber的节点类型,初次渲染时,函数组件对应的tag为2,后续更新过程中对应的tag为0
type: () => {
}
updateQueue: null,
}
在函数组件的 fiber 中,有两个属性和 hook 有关:memoizedState
和updateQueue
属性。
- memoizedState 属性用于保存 hook 链表,hook 链表是单向链表。
- updateQueue 属性用于收集hook的副作用信息,保存
useEffect
、useLayoutEffect
、useImperativeHandle
这三个 hook 的 effect 信息,是一个环状链表,其中 updateQueue.lastEffect 指向最后一个 effect 对象。effect 描述了 hook 的信息,比如useLayoutEffect
的 effect 对象保存了监听函数,清除函数,依赖等。
hook 链表
React 为我们提供的以use
开头的函数就是 hook,本质上函数在执行完成后,就会被销毁,然后状态丢失。React 能记住这些函数的状态信息的根本原因是,在函数组件执行过程中,React 会为每个 hook 函数创建对应的 hook 对象,然后将状态信息保存在 hook 对象中,在下一次更新渲染时,会从这些 hook 对象中获取上一次的状态信息。
在函数组件执行的过程中,比如上例中,当执行 Home()
函数组件时,React 会为组件内每个 hook 函数创建对应的 hook 对象,这些 hook 对象保存 hook 函数的信息以及状态,然后将这些 hook 对象连成一个链表。上例中,第一个执行的是useState
hook,React 为其创建一个 hook:stateHook。第二个执行的是useRef
hook,同样为其创建一个 hook:refHook,然后将 stateHook.next 指向 refHook:stateHook.next = refHook。同理,refHook.next = effectHook,…
需要注意:
useContext
是唯一一个不会出现在 hook 链表中的 hook。- useState 是 useReducer 的语法糖,因此这里只需要用 useState 举例就好。
useEffect
、useLayoutEffect
、useImperativeHandle
这三个 hook 都是属于 effect 类型的 hook,他们的 effect 对象都需要被添加到函数组件 fiber 的 updateQueue 中,以便在 commit 阶段执行。
上例中,hook 链表如下红色虚线中所示:
hook 对象及其属性介绍
函数组件内部的每一个 hook 函数,都有对应的 hook 对象用来保存 hook 函数的状态信息,hook 对象的属性如下:
var hook = {
memoizedState,,
baseState,
baseQueue,
queue,
next,
};
注意,hook 对象中的memoizedState
属性和 fiber 的memoizedState
属性含义不同。next
指向下一个 hook 对象,函数组件中的 hook 就是通过 next 指针连成链表
同时,不同的 hook 中,memoizedState 的含义不同,下面详细介绍各类型 hook 对象的属性含义
useState Hook 对象
- hook.memoizedState 保存的是 useState 的 state 值。比如
const [count, setCount] = useState(0)
中,memoizedState 保存的就是 state 的值。 - hook.queue 保存的是更新队列,是个环状链表。queue 的属性如下:
hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState,
};
比如我们在 onClick 中多次调用setCount
:
const onClick = useCallback(() => {
debugger;
setCount(count + 1);
setCount(2);
setCount(3);
}, [count]);
每次调用setCount
,都会创建一个新的 update 对象,并添加进 hook.queue 中,update 对象属性如下:
var update = {
lane: lane,