React Hooks useState 使用详解+实现原理+源码分析
1. 简介
useState
=> 让函数组件具有维持状态的能力
useState
这个 Hook
是用来管理 state
的,它可以让函数组件具有维持状态的能力。即在一个函数组件的多次渲染之间,这个 state
是共享的。
2. 使用方法
函数签名如下:
// 使用方法一:初始值为基础数据类型或Object
const [state, setState] = useState(initialState);
// 使用方法二:初始值为函数
const [state, setState] = useState(() => initialState);
@param useState
接受一个参数,作为 state
的初始值,这个参数可以是任何数据类型,也可以是函数
@return useState
返回一个数组,数组中包括 someState
数据源和更新这个 state
的方法 setSomeState
2.1 useState 使用示例
const [name, setName] = useState('React'); // 参数是String
const [age, setAge] = useState(9); // 参数是Number
const [features, setFeatures] = useState([{ text: 'JSX' }]); // 参数是Object
const [count, setCount] = useState(() => {
// 先执行一定的逻辑,后再返回初始值
const initialState = computedBaseCount(props);
return initialState;
}); // 参数是Function
2.2 setState 使用示例:计数器函数组件
setSomeState
接收一个新的state
值(这个参数可以是任何数据类型,也可以是函数),并将组件的一次重新渲染加入队列 普通更新: 直接赋值 函数式更新: 如果new state
需要通过old state
计算得出,可以给setState
传入函数 __ 参数即为old state
__ 返回值为new state
function Counter({initialCount}) {
// 声明一个叫做 “count” 的 state 变量
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
);
}
从这个例子中可以看到,useState
可以让我们非常方便的去设置一个 state(count)
,并提供一个特定的方法(setCount
)来专门设置更新这个状态。
2.3 总结特点
- 惰性初始
state
:initialState
参数只会在组件的初始渲染中起作用,后续渲染时会被忽略; - 函数式更新
state
:可以为setState
传入函数,来获取前一次state
值; - 语义化:每一个数据,都有一个专门设置状态的方法(即,一个函数有多个
setState
)。
与类组件
setState
的区别: ○ 类组件中setState
只能有一个,所以我们把一个对象作为一个state
,通过不同的属性来标识不同状态; ○ 函数组件中setState
可有多个,更加语义化,更加方便使用。
什么样的值应该保存在
state
中呢? 这是日常开发中需要经常思考的问题。通常来说,我们要遵循的一个原则就是:state
中永远不要保存可以通过计算得到的值。比如说:
- 从
props
传递过来的值。有时候props
传递过来的值无法直接使用,而是要通过一定的计算后再在UI
上展示,比如说排序。那么我们要做的就是每次用的时候,都重新排序一下,或者利用某些cache
机制,而不是将结果直接放到state
里。- 从
URL
中读到的值。比如有时需要读取URL
中的参数,把它作为组件的一部分状态。那么我们可以在每次需要用的时候从URL
中读取,而不是读出来直接放到state
里。- 从
cookie
、localStorage
中读取的值。通常来说,也是每次要用的时候直接去读取,而不是读出来后放到state
里。
3. 源码分析👇👇👇
贴上的源码中,有些省略了异常处理、调试工具处理等非核心逻辑。(采用截图的方式,代码颜色利于识别)
3.1 Hook 公共的执行环境判断
React Fiber
会从 packages/react-reconciler/src/ReactFiberBeginWork.js 中的 beginWork()
开始执行
对于函数组件 FunctionComponent
,其走以下逻辑加载或更新组件:
在 updateFunctionComponent
中,可看到 React Hooks
的渲染核心入口是 renderWithHooks
renderWithHooks
函数处理 Hooks 逻辑(省略异常处理代码和__DEV__处理逻辑)
renderWithHooks
主要做了两件事:
- 用变量
currentlyRenderingFiber
记录当前的fiber node
。使得useState
能拿到当前node
的状态。 - 判断
hook api
挂载在那个对象上。首次渲染和后期的更新,挂载的对象是不同的 => 解耦。
可以看到,在组件首次渲染的时候,useState = mountState
;非首次渲染,useState = upStateState
3.2 再来看mountState函数
第一步: 创建 hook 对象,并将该 hook 对象加到 hook 链的末尾
workInProgressHook
:hook
链表中的一个重要指针 => 它通过记录当前生成(更新)的 hook
对象,可以间接反映在组件中当前调用到哪个 hook
函数了。每调用一次 hook
函数,就将这个指针的指向移到该 hook
函数产生的 hook
对象上。
baseQueue 每次更新完会赋值上一个
update
,方便React
在渲染错误的边缘,数据回溯。 Update 称作一个更新,在调度一次React
更新时会用到。 UpdateQueue 是 Update 的队列,同时还带有更新的dispatch
。
第二步: 初始化 hook 对象的状态值,也就是我们传进来的 initState 的值。
第三步: 创建更新队列,这个队列是更新状态值的时候用的,会保存所有的更新行为。
第四步: 绑定 dispatchSetState 函数。
我们可以看到最后一行返回的就是这个函数。
也就是说这个函数,其实就是我们改变状态用的函数,就相当于是 setState
函数。这里它先做了一个绑定当前 quene
和 fiber node
的动作,就是为了在调用 setState
的时候,知道该更改的是哪个组件的哪一个状态的值。
3.3 改变状态的函数 dispatchSetState
useState
执行 setState
后会调用 dispatchSetState
下面来看看 dispatchSetState
都做了什么
我们可以看到实际上,dispatchSetState
这个函数主要做了两件事情。
第一件事:创建了一个 update
对象,这个对象上面保存了本次更新的相关信息,包括新的状态值 action
。
第二件事:将所有的 update
对象串成了一个环形链表,保存在我们 hook
对象的 queue.pending
属性上面。
使用环形链表的意义: 整个环形链表变量我们叫它
update
,使得queue.pending = update
那么此时queue.pending
的最近一次更新,就是update
,最早的一次更新是update.next
这样就快速定位到最早的一次更新了 如果是单链表,想找到最早的 一次更新,需要一层一层往下找 环形链表一次就找到了
3.4 updateState
updateState
是我们组件更新阶段,调用 useState
真正走的逻辑。
可以看到其实
updateState
的内部逻辑,其实是就是 updateReducer
,所以其实 useState
和 useReducer
两个 hooks
其实做的是同一件事 => 更新 state
updateReducer 相关源码解析,请看本篇文章
useState
和useReduer
的关系:useState
是useReduer
的一个特殊情况,其传入的reducer
是固定的basicStateReducer
,负责改变state
useReducer
可以传入自定义的reducer
updateState
做的事情,实际上就是拿到更新队列,循环队列,并根据每一个 update
对象对当前 hook
进行状态更新。最后返回最终的结果。
3.5 流程总结
转载自:https://juejin.cn/post/7076456859611168776