热身准备
在正式讲 useState ,我们先热热身,了解下必备知识。
为什么会有hooks
大家都知道 hooks 是在函数组件的产物。之前 class 组件为什么没有出现 hooks 这种东西呢?
答案很简单,不需要。
因为在 class 组件中,在运行时,只会生成一个实例,而在这个实例中会保存组件的 state 等信息。在后续的更新操作中,也只是调用其中的 render 方法,实例中的信息不会丢失。而在函数组件中,每次渲染,更新都会去执行这个函数组件,所以在函数组件中是没办法保存 state 等信息的。为了保存 state 等信息,于是有了 hooks ,用来记录函数组件的状态,执行副作用。
hooks执行时机
上面提到,在函数组件中,每次渲染,更新都会去执行这个函数组件。所以我们在函数组件内部声明的 hooks 也会在每次执行函数组件时执行。
在这个时候,可能有的同学听了我上面的说法( hooks 用来记录函数组件的状态,执行副作用),又有疑惑了,既然每次函数组件执行都会执行 hooks 方法,那 hooks 是怎么记录函数组件的状态的呢?
答案是,记录在函数组件对应的 fiber 节点中。
两套hooks
在我们刚开始学习使用 hooks 时,可能会有疑惑, 为什么 hooks 要在函数组件的顶部声明,而不能在条件语句或内部函数中声明?
答案是, React 维护了两套 hooks ,一套用来在项目初始化 mount 时,初始化 hooks 。而在后续的更新操作中会基于初始化的 hooks 执行更新操作。如果我们在条件语句或函数中声明 hooks ,有可能在项目初始化时不会声明,这样就会导致在后面的更新操作中出问题。
hooks存储
提前讲一下hooks存储方式,避免看晕了~~~
每个初始化的 hook 都会创建一个 hook 结构,多个 hook 是通过声明顺序用链表的结构相关联,最终这个链表会存放在 fiber.memoizedState 中:
var hook = { memoizedState: null, // 存储hook操作,不要和fiber.memoizedState搞混了 baseState: null, baseQueue: null, queue: null, // 存储该hook本次更新阶段的所有更新操作 next: null // 链接下一个hook };
而在每个 hook.queue 中存放的么个 update 也是一个链表结构存储的,千万不要和 hook 的链表搞混了。
接下来,让我们带着下面几个问题看文章:
为什么 setState 后不能马上拿到最新的 state 的值? 多个 setState 是如何合并的? setState 到底是同步还是异步的? 为什么 setState 的值相同时,函数组件不更新?假如我们有下面这样一段代码:
function App(){ const [count, setCount] = useState(0) const handleClick = () => { setCount(count => count + 1) } return ( <div> 勇敢牛牛, <span>不怕困难</span> <span onClick={handleClick}>{count}</span> </div> ) }
初始化 mount
useState
我们先来看下 useState() 函数:
function useState(initialState) { var dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); }
上面的 dispatcher 就会涉及到开始提到的两套 hooks 的变换使用, initialState 是我们传入 useState 的参数,可以是基础数据类型,也可以是函数,我们主要看 dispatcher.useState(initialState) 方法,因为我们这里是初始化,它会调用 mountState 方法:相关参考视频: 传送门
function mountState(initialState) { var hook = mountWorkInProgressHook(); // workInProgressHook if (typeof initialState === 'function') { // 在这里,如果我们传入的参数是函数,会执行拿到return作为initialState initialState = initialState(); } hook.memoizedState = hook.baseState = initialState; var queue = hook.queue = { pending: null, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: initialState }; var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue); return [hook.memoizedState, dispatch]; }
上面的代码还是比较简单,主要就是根据 useState() 的入参生成一个 queue 并保存在 hook 中,然后将入参和绑定了两个参数的 dispatchAction 作为返回值暴露到函数组件中去使用。
这两个返回值,第一个 hook.memoizedState 比较好理解,就是初始值,第二个 dispatch ,也就是 dispatchAction.bind(null, currentlyRenderingFiber$1, queue) 这是个什么东西呢?
我们知道使用 useState() 方法会返回两个值 state, setState ,这个 setState 就对应上面的 dispatchAction ,这个函数是怎么做到帮我们设置 state 的值的呢?
我们先保留这个疑问,往下看,在后面会慢慢揭晓答案。
接下来我们主要看看 mountWorkInProgressHook 都做了些什么。
mountWorkInProgressHook
function mountWorkInProgressHook() { var hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null }; // 这里的if/else主要用来区分是否是第一个hook if (workInProgressHook === null) { currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook; } else { // 把hook加到hooks链表的最后一条, 并且指针指向这条hook workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }
从上面的 currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook; 这一行代码,我们可以发现,hook是存放在对应 fiber.memoizedState 上的。
workInProgressHook = workInProgressHook.next = hook; ,从这一行代码,我们能知道,如果是有多个 hook ,他们是以链表的形式进行的存放。
不仅仅是 useState() 这个 hook 会在初始化时走 mountWorkInProgressHook 方法,其他的 hook ,例如: useEffect, useRef, useCallback 等在初始化时都是调用的这个方法。
到这里我们能搞明白两件事:
hooks 的状态数据是存放在对应的函数组件的 fiber.memoizedState ; 一个函数组件上如果有多个 hook ,他们会通过声明的顺序以链表的结构存储;到这里,我们的 useState() 已经完成了它初始化时的所有工作了,简单概括下, useState() 在初始化时会将我们传入的初始值以 hook 的结构存放到对应的 fiber.memoizedState ,以数组形式返回 [state, dispatchAction] 。
更新update
当我们以某种形式触发 setState() 时, React 也会根据 setState() 的值来决定如何更新视图。
在上面讲到, useState 在初始化时会返回 [state, dispatchAction] ,那我们调用 setState() 方法,实际上就是调用 dispatchAction ,而且这个函数在初始化时还通过 bind 绑定了两个参数, 一个是 useState 初始化时函数组件对应的 fiber ,另一个是 hook 结构的 queue 。
来看下我精简后的 dispatchAction (去除了和 setState 无关的代码)
function dispatchAction(fiber, queue, action) { // 创建一个update,用于后续的更新,这里的action就是我们setState的入参 var update = { lane: lane, action: action, eagerReducer: null, eagerState: null, next: null }; // 这段闭环链表插入update的操作有没有很熟悉? var pending = queue.pending; if (pending === null) { update.next = update; } else { update.next = pending.next; pending.next = update; } queue.pending = update; var alternate = fiber.alternate; // 判断当前是否是渲染阶段 if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) { var lastRenderedReducer = queue.lastRenderedReducer; // 这个if语句里的一大段就是用来判断我们这次更新是否和上次一样,如果一样就不会在进行调度更新 if (lastRenderedReducer !== null) { var prevDispatcher; { prevDispatcher = ReactCurrentDispatcher$1.current; ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV; } try { var currentState = queue.lastRenderedState; var eagerState = lastRenderedReducer(currentState, action); update.eagerReducer = lastRenderedReducer; update.eagerState = eagerState; if (objectIs(eagerState, currentState)) { return; } } finally { { ReactCurrentDispatcher$1.current = prevDispatcher; } } } } // 将携带有update的fiber进行调度更新 scheduleUpdateOnFiber(fiber, lane, eventTime); } }
上面的代码已经是我尽力精简的结果了。。。代码上有注释,各位看官凑合看下。
不愿细看的我来总结下 dispatchAction 做的事情:
创建一个 update 并加入到 fiber.hook.queue 链表中,并且链表指针指向这个 update ; 判断当前是否是渲染阶段决定要不要马上调度更新; 判断这次的操作和上次的操作是否相同, 如果相同则不进行调度更新; 满足上述条件则将带有 update 的 fiber 进行调度更新;到这里我们又搞明白了一个问题:
为什么 setState 的值相同时,函数组件不更新?
updateState
我们这里不详细讲解调度更新的过程, 后面文章安排, 这里我们只需要知道,在接下来更新过程中,会再次执行我们的函数组件,这时又会调用 useState 方法了。前面讲过,React维护了两套 hooks ,一套用于初始化, 一套用于更新。 这个在调度更新时就已经完成了切换。所以我们这次调用 useState 方法会和之前初始化有所不同。
这次我们进入 useState ,会看到其实是调用的 updateState 方法
function updateState(initialState) { return updateReducer(basicStateReducer); }
看到这几行代码,看官们应该就明白为什么网上有人说 useState 和 useReducer 相似。原来在 useState 的更新中调用的就是 updateReducer 啊。
updateReducer
本来很长,想让各位看官忍一忍。于心不忍,忍痛减了很多
function updateReducer(reducer, initialArg, init) { // 创建一个新的hook,带有dispatchAction创建的update var hook = updateWorkInProgressHook(); var queue = hook.queue; queue.lastRenderedReducer = reducer; var current = currentHook; var baseQueue = current.baseQueue; var pendingQueue = queue.pending; current.baseQueue = baseQueue = pendingQueue; if (baseQueue !== null) { // 从这里能看到之前讲的创建闭环链表插入update的好处了吧?直接next就能找到第一个update var first = baseQueue.next; var newState = current.baseState; var update = first; // 开始遍历update链表执行所有setState do { var updateLane = update.lane; // 假如我们这个update上有多个setState,在循环过程中,最终都会做合并操作 var action = update.action; // 这里的reducer会判断action类型,下面讲 newState = reducer(newState, action); update = update.next; } while (update !== null && update !== first); hook.memoizedState = newState; hook.baseState = newBaseState; hook.baseQueue = newBaseQueueLast; queue.lastRenderedState = newState; } var dispatch = queue.dispatch; return [hook.memoizedState, dispatch]; }
上面的更新中,会循环遍历 update 进行一个合并操作,只取最后一个 setState 的值,这时候可能有人会问那直接取最后一个 setState 的值不是更方便吗?
这样做是不行的,因为 setState 入参可以是基础类型也可以是函数, 如果传入的是函数,它会依赖上一个 setState 的值来完成更新操作,下面的代码就是上面的循环中的 reducer
function basicStateReducer(state, action) { return typeof action === 'function' ? action(state) : action; }
到这里我们搞明白了一个问题,多个 setState 是如何合并的?
updateWorkInProgressHook
下面是伪代码,我把很多的逻辑判断给删除了,免了太长又让各位看官难受,原来的代码里会判断当前的 hook 是不是第一个调度更新的 hook ,我这里为了简单就按第一个来解析
function updateWorkInProgressHook() { var nextCurrentHook; nextCurrentHook = current.memoizedState; var newHook = { memoizedState: currentHook.memoizedState, baseState: currentHook.baseState, baseQueue: currentHook.baseQueue, queue: currentHook.queue, next: null } currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook; return workInProgressHook; }
从上面代码能看出来, updateWorkInProgressHook 抛去那些判断, 其实做的事情也很简单,就是基于 fiber.memoizedState 创建一个新的 hook 结构覆盖之前的 hook 。前面 dispatchAction 讲到会把 update 加入到 hook.queue 中,在这里的 newHook.queue 上就有这个 update 。
总结
总结下 useState 初始化和 setState 更新:
useState 会在第一次执行函数组件时进行初始化,返回 [state, dispatchAction] 。 当我们通过 setState 也就是 dispatchAction 进行调度更新时,会创建一个 update 加入到 hook.queue 中。 当更新过程中再次执行函数组件,也会调用 useState 方法,此时的 useState 内部会使用更新时的 hooks 。 通过 updateWorkInProgressHook 获取到 dispatchAction 创建的 update 。 在 updateReducer 通过遍历 update 链表完成 setState 合并。 返回 update 后的 [newState, dispatchAction] .还有两个问题
为什么 setState 后不能马上拿到最新的 state 的值? React 其实可以这么做,为什么没有这么做,因为每个 setState 都会触发更新, React 出于性能考虑,会做一个合并操作。所以 setState 只是触发了 dispatchAction 生成了一个 update 的动作,新的 state 会存储在 update 中,等到下一次 render , 触发这个 useState 所在的函数组件执行,才会赋值新的 state 。
setState 到底是同步还是异步的?
同步的,假如我们有这样一段代码:
const handleClick = () => { setCount(2) setCount(count => count + 1) console.log('after setCount') }
你会惊奇的发现页面还没有更新 count ,但是控制台已经打印了 after setCount 。
之所以表现上像是异步,是因为内部使用了 try{...}finally{...} 。当调用 setState 触发调度更新时,更新操作会放在 finally 中,返回去继续执行 handlelick 的逻辑。于是会出现上面的情况。
看完这篇文章, 我们可以弄明白下面这几个问题:
为什么 setState 后不能马上拿到最新的 state 的值? 多个 setState 是如何合并的? setState 到底是同步还是异步的? 为什么 setState 的值相同时,函数组件不更新? setState 是怎么完成更新的? useState 是什么时候初始化又是什么时候开始更新的?到此这篇关于React超详细分析useState与useReducer源码的文章就介绍到这了,更多相关React useState与useReducer内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!
查看更多关于React超详细分析useState与useReducer源码的详细内容...