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