在日常利用React开发需求的过程当中,useState或许是绝大部分同学用的最频繁且最熟悉的Hook。但是对于这个Hook本身的实现原理,以及在它在React Runtime中的运行机制又是否能做到一清二楚呢?本文将从实现一个简易版的useState为切入点,深度剖析useState的整体运行进制,争取让大家能够彻底明白useState背后的那些事。
基本使用
useState提供了一个在Function Component中声明状态的方法,他接收一个初始值或者一个返回初始值的方法,同时以数组的形式,返回一个状态和改变状态的方法。在执行改变状态的方法后,组件会进行Rerender,并更新到最新的状态
js
// 传入初始值
const [name, setName] = useState('Edward');
// 传入初始化函数(初始化函数只会在组件初始化的时候执行一次)
const createInitialTodos = () => {
const initialTodos = [];
for (let i = 0; i < 50; i++) {
initialTodos.push({
id: i,
text: 'Item ' + (i + 1)
});
}
return initialTodos;
}
const [todos, setTodos] = useState(createInitialTodos);
尝试实现一个useState
根据useState的基本使用,我们可以根据他的特性来实现一个简易版的useCustomState。
前置知识
在React16.8之后,针对Dom tree过大导致组件更新过程过长的问题,React引进了全新的reconcile结构,其核心就是新的Virtual Dom形式,Fiber架构。Fiber架构以链表的形式,利用child,sibling,return等属性将每个节点的父子和兄弟节点通通连接起来。
当组件树过于庞大,更新过程较长时,利用Fiber架构,可以将整个组件的ReRnder变成一个可中断可恢复的过程,不断地利用浏览器的空闲时间来完成Diff和Update的工作,从而在不阻塞页面正常刷新的前提下,完成组件的更新。
而我们的useState所缓存的组件状态,则是挂载在每个组件对应的Fiber节点里的memoizeState属性。所以在实现useState之前,我们需要先简单的构造一个Fiber节点。另外,在同一个组件中,我们可能会同时使用多个useState方法维护多个状态,所以我们还需要一个变量去维护当前组件所使用到的所有的hook。
js
const fiber = {
type: "FunctionComponent",
memoizedState: null
}
const workInProgressHook = null;
初始化阶段
然后,才开始正式实现我们的useCustomState方法。这个方法可以接受一个初始值或者返回初始值的函数,并且返回一个状态和改变状态的方法,同时,这个方法的调用会更新状态和重新绘制组件。
js
// 表示组件是初次加载还是更新状态
let firstMount = true;
const _renderApp = () => {}
const useCustomState = (initState) => {
let state = typeof initState === 'function' ? initState() : initState;
let hook;
// 组件首次加载时,构造一个hook对象挂载在fiber上
if(firstMount) {
hook = {
memoizedState: state,
next: null
}
// 创建更新队列,这个队列是更新状态值的时候用的,会保存所有的更新行为
let queue = {
pending: null,
lanes: null,
next: null
}
hook.queue = queue
if(!fiber.memoizedState) {
fiber.memoizedState = workInProgressHook = hook;
}
// 利用next属性将多个hook对象串连起来,既保存了hook的状态,也保存了hooks的顺序
workInProgressHook = workInProgressHook.next = hook;
}
// 返回当前hook的状态值,触发更新状态和组件rerender的函数
return [hook.memoizedState, dispatchAction.bind(null, hook)]
}
const dispatchAction(hook, action) = () => {}
通过以上代码,我们实现了,在初始化组件和hook时,我们对组件内使用到的所有hook,都通过workInProgressHook,以链表的形式,保存了所有的hook状态以及hook的调用顺序,这对我们后面的工作至关重要。
实现setState方法
下一步我们来实现,useState中返回的更改状态的方法(setState),该方法可以接收一个新的状态,或者是一个以前一个状态为参数并返回最新状态的函数。通过调用setState,我们可以触发组件的Rerender,并将状态更新到我们传入的值。
js
const dispatchAction(hook, action) = () => {
// 每次触发setState时,我们都新建一个update对象
let update = {
action,
next: null
}
// 将setState的入参保存在hook.queue.pending中
// 同时,当同一个hook的setState操作使用了多次时,我们需要通过hook.queue.pending.next属性串连起每一个setState操作。
const pending = hook.queue.pending;
if(pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = udpate;
}
queue.pending = update;
// 虚构一个Render组件方法
_renderApp()
}
通过上述代码,我们可以很清晰的看到,在触发setState之后,我们做了两件事,
- 每次触发更新时,我们都会新建一个update对象,这个对象保存了当前更新的相关新,包含了最新的状态值action。
- 使用环形链表,将所有的update对象串联起来,并保存在hook.queue.pending上。
使用环形链表的意义:
整个环形链表变量我们叫它
update
,使得queue.pending = update
那么此时queue.pending
的最近一次更新,就是update
,最早的一次更新是update.next
这样就快速定位到最早的一次更新了
如果是单链表,想找到最早的 一次更新,需要一层一层往下找
环形链表一次就找到了
更新阶段
当用户触发了setState的操作时,我们的组件会进行到更新阶段,整个函数组件会重新调用useState方法,但是此时的处理就不能跟初始化那样了,我们必须返回setState之后最新的数据状态。
js
const useCustomState = (initState) => {
let state = typeof initState === 'function' ? initState() : initState;
let hook;
// 组件首次加载时,构造一个hook对象挂载在fiber上
if(firstMount) {
hook = {
memoizedState: state,
next: null
}
// 创建更新队列,这个队列是更新状态值的时候用的,会保存所有的更新行为
let queue = {
pending: null,
lanes: null,
next: null
}
hook.queue = queue
if(!fiber.memoizedState) {
fiber.memoizedState = workInProgressHook = hook;
}
// 利用next属性将多个hook对象串连起来,既保存了hook的状态,也保存了hooks的顺序
workInProgressHook = workInProgressHook.next = hook;
} else {
// 更新阶段时,先取出当前操作的hook
hook = workInProgressHook
let baseState = hook.memoizedState
// 如果存在更新操作,则遍历所有的更新操作,将其遍历完
if(hook.queue.pending) {
let firstAction = hook.queue.pending.next;
do{
baseState = typeof firstAction.action === 'function' ? firstAction.action(baseState) : firstAction.action
firstAction = firstAction.next
} while(firstAction !== hook.queue.pending)
// 遍历之后,取最终值更新到hook对象里的memoizedState中
hook.memoizedState = baseState;
hook.queue.pending = null;
}
// 进行下一个hook的操作
workInProgressHook = workInProgressHook.next
}
// 返回当前hook的状态值,触发更新状态和组件rerender的函数
return [hook.memoizedState, dispatchAction.bind(null, hook)]
}
在上面的代码中,其实我们的做的事情非常简单,在更新流程里,判断当前hook是否存在对应的更新操作,如果有的话,可遍历所有的更新操作,取最终值,并更新到hook中。
从代码
workInProgressHook = workInProgressHook.next
中可以得知,在更新每个hook的状态时,我们是严格按照初始化时维护的hook链表指向顺序,依次进行的。这也直接回答了,为什么hook要定义在顶部,而不可以放在诸如条件判断的代码里了。倘若hook放在了一个条件判断的代码里,初始化组件和更新的时候,hook的数量发生了变化,那么指针的指向就会发生错乱,更新机制就会出现问题。
通过上述的代码,我们已经基本完成了一个简易版的useState了,通过这些代码,我们就可以更好的去理解React中对useState的实现了。
源码解析
在React的里,useState的处理是发生在Reconcile的过程中的renderWithHooks
方法中。 这个方法将useState的执行区分成了两个阶段,对应了两个方法。分别为mountState 和updateState。
mountState
在mountState中,react做的事情其实跟我们之前写的代码大同小异,创建了一个hook对象,并挂载一个记录更新操作的queue属性,同时创建一个更新状态的方法dispatch,并最终返回。
在dispatch方法中,react同样创建了一个update对象,并在创建后,对当前节点以及当前节点的祖先节点进行更新标记,最终利用scheduleUpdateOnFiber
,去调度下一次的渲染。
updateState
当触发setState时,会触发组件的重渲染,此时useState会走到另外一个阶段,也就是updateState方法。
在updateState方法里,调用了updateReducer,updateReducer里会调用updateReducerImpl
方法,这个方法主要是根据更新的优先级,去取出最终要更新的state,并更新到hook.memoizedState中。