热身预备
在正式讲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
是什么时分初始化又是什么时分开端更新的?