翼度科技»论坛 编程开发 JavaScript 查看内容

React超详细分析useState与useReducer源码

3

主题

3

帖子

9

积分

新手上路

Rank: 1

积分
9
热身准备

在正式讲
  1. useState
复制代码
,我们先热热身,了解下必备知识。

为什么会有hooks

大家都知道
  1. hooks
复制代码
是在函数组件的产物。之前
  1. class
复制代码
组件为什么没有出现
  1. hooks
复制代码
这种东西呢?
答案很简单,不需要。
因为在
  1. class
复制代码
组件中,在运行时,只会生成一个实例,而在这个实例中会保存组件的
  1. state
复制代码
等信息。在后续的更新操作中,也只是调用其中的
  1. render
复制代码
方法,实例中的信息不会丢失。而在函数组件中,每次渲染,更新都会去执行这个函数组件,所以在函数组件中是没办法保存
  1. state
复制代码
等信息的。为了保存
  1. state
复制代码
等信息,于是有了
  1. hooks
复制代码
,用来记录函数组件的状态,执行副作用。

hooks执行时机

上面提到,在函数组件中,每次渲染,更新都会去执行这个函数组件。所以我们在函数组件内部声明的
  1. hooks
复制代码
也会在每次执行函数组件时执行。
在这个时候,可能有的同学听了我上面的说法(
  1. hooks
复制代码
用来记录函数组件的状态,执行副作用),又有疑惑了,既然每次函数组件执行都会执行
  1. hooks
复制代码
方法,那
  1. hooks
复制代码
是怎么记录函数组件的状态的呢?
答案是,记录在函数组件对应的
  1. fiber
复制代码
节点中。

两套hooks

在我们刚开始学习使用
  1. hooks
复制代码
时,可能会有疑惑, 为什么
  1. hooks
复制代码
要在函数组件的顶部声明,而不能在条件语句或内部函数中声明?
答案是,
  1. React
复制代码
维护了两套
  1. hooks
复制代码
,一套用来在项目初始化
  1. mount
复制代码
时,初始化
  1. hooks
复制代码
。而在后续的更新操作中会基于初始化的
  1. hooks
复制代码
执行更新操作。如果我们在条件语句或函数中声明
  1. hooks
复制代码
,有可能在项目初始化时不会声明,这样就会导致在后面的更新操作中出问题。

hooks存储

提前讲一下hooks存储方式,避免看晕了~~~
每个初始化的
  1. hook
复制代码
都会创建一个
  1. hook
复制代码
结构,多个
  1. hook
复制代码
是通过声明顺序用链表的结构相关联,最终这个链表会存放在
  1. fiber.memoizedState
复制代码
中:
  1. var hook = {
  2.     memoizedState: null,   // 存储hook操作,不要和fiber.memoizedState搞混了
  3.     baseState: null,
  4.     baseQueue: null,
  5.     queue: null,    // 存储该hook本次更新阶段的所有更新操作
  6.     next: null      // 链接下一个hook
  7. };
复制代码
而在每个
  1. hook.queue
复制代码
中存放的么个
  1. update
复制代码
也是一个链表结构存储的,千万不要和
  1. hook
复制代码
的链表搞混了。
接下来,让我们带着下面几个问题看文章:

  • 为什么
    1. setState
    复制代码
    后不能马上拿到最新的
    1. state
    复制代码
    的值?
  • 多个
    1. setState
    复制代码
    是如何合并的?
    1. setState
    复制代码
    到底是同步还是异步的?
  • 为什么
    1. setState
    复制代码
    的值相同时,函数组件不更新?
假如我们有下面这样一段代码:
  1. function App(){
  2.   const [count, setCount] = useState(0)
  3.   const handleClick = () => {
  4.     setCount(count => count + 1)
  5.   }
  6.   return (
  7.     <div>
  8.         勇敢牛牛,        <span>不怕困难</span>
  9.         <span onClick={handleClick}>{count}</span>
  10.     </div>
  11.   )
  12. }
复制代码
初始化 mount


useState

我们先来看下
  1. useState()
复制代码
函数:
  1. function useState(initialState) {
  2.   var dispatcher = resolveDispatcher();
  3.   return dispatcher.useState(initialState);
  4. }
复制代码
上面的
  1. dispatcher
复制代码
就会涉及到开始提到的两套
  1. hooks
复制代码
的变换使用,
  1. initialState
复制代码
是我们传入
  1. useState
复制代码
的参数,可以是基础数据类型,也可以是函数,我们主要看
  1. dispatcher.useState(initialState)
复制代码
方法,因为我们这里是初始化,它会调用
  1. mountState
复制代码
方法:相关参考视频:传送门
  1. function mountState(initialState) {
  2.   var hook = mountWorkInProgressHook();   // workInProgressHook
  3.   if (typeof initialState === 'function') {
  4.     // 在这里,如果我们传入的参数是函数,会执行拿到return作为initialState
  5.     initialState = initialState();
  6.   }
  7.   hook.memoizedState = hook.baseState = initialState;
  8.   var queue = hook.queue = {
  9.     pending: null,
  10.     dispatch: null,
  11.     lastRenderedReducer: basicStateReducer,
  12.     lastRenderedState: initialState
  13.   };
  14.   var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
  15.   return [hook.memoizedState, dispatch];
  16. }
复制代码
上面的代码还是比较简单,主要就是根据
  1. useState()
复制代码
的入参生成一个
  1. queue
复制代码
并保存在
  1. hook
复制代码
中,然后将入参和绑定了两个参数的
  1. dispatchAction
复制代码
作为返回值暴露到函数组件中去使用。
这两个返回值,第一个
  1. hook.memoizedState
复制代码
比较好理解,就是初始值,第二个
  1. dispatch
复制代码
,也就是
  1. dispatchAction.bind(null, currentlyRenderingFiber$1, queue)
复制代码
这是个什么东西呢?
我们知道使用
  1. useState()
复制代码
方法会返回两个值
  1. state, setState
复制代码
,这个
  1. setState
复制代码
就对应上面的
  1. dispatchAction
复制代码
,这个函数是怎么做到帮我们设置
  1. state
复制代码
的值的呢?
我们先保留这个疑问,往下看,在后面会慢慢揭晓答案。
接下来我们主要看看
  1. mountWorkInProgressHook
复制代码
都做了些什么。

mountWorkInProgressHook
  1. function mountWorkInProgressHook() {
  2.   var hook = {
  3.     memoizedState: null,
  4.     baseState: null,
  5.     baseQueue: null,
  6.     queue: null,
  7.     next: null
  8.   };
  9.   // 这里的if/else主要用来区分是否是第一个hook
  10.   if (workInProgressHook === null) {
  11.     currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
  12.   } else {
  13.   //  把hook加到hooks链表的最后一条, 并且指针指向这条hook
  14.     workInProgressHook = workInProgressHook.next = hook;  
  15.   }
  16.   return workInProgressHook;
  17. }
复制代码
从上面的
  1. currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
复制代码
这一行代码,我们可以发现,hook是存放在对应
  1. fiber.memoizedState
复制代码
上的。
  1. workInProgressHook = workInProgressHook.next = hook;
复制代码
,从这一行代码,我们能知道,如果是有多个
  1. hook
复制代码
,他们是以链表的形式进行的存放。
不仅仅是
  1. useState()
复制代码
这个
  1. hook
复制代码
会在初始化时走
  1. mountWorkInProgressHook
复制代码
方法,其他的
  1. hook
复制代码
,例如:
  1. useEffect, useRef, useCallback
复制代码
等在初始化时都是调用的这个方法。
到这里我们能搞明白两件事:

    1. hooks
    复制代码
    的状态数据是存放在对应的函数组件的
    1. fiber.memoizedState
    复制代码

  • 一个函数组件上如果有多个
    1. hook
    复制代码
    ,他们会通过声明的顺序以链表的结构存储;
到这里,我们的
  1. useState()
复制代码
已经完成了它初始化时的所有工作了,简单概括下,
  1. useState()
复制代码
在初始化时会将我们传入的初始值以
  1. hook
复制代码
的结构存放到对应的
  1. fiber.memoizedState
复制代码
,以数组形式返回
  1. [state, dispatchAction]
复制代码


更新update

当我们以某种形式触发
  1. setState()
复制代码
时,
  1. React
复制代码
也会根据
  1. setState()
复制代码
的值来决定如何更新视图。
在上面讲到,
  1. useState
复制代码
在初始化时会返回
  1. [state, dispatchAction]
复制代码
,那我们调用
  1. setState()
复制代码
方法,实际上就是调用
  1. dispatchAction
复制代码
,而且这个函数在初始化时还通过
  1. bind
复制代码
绑定了两个参数, 一个是
  1. useState
复制代码
初始化时函数组件对应的
  1. fiber
复制代码
,另一个是
  1. hook
复制代码
结构的
  1. queue
复制代码

来看下我精简后的
  1. dispatchAction
复制代码
(去除了和
  1. setState
复制代码
无关的代码)
  1. function dispatchAction(fiber, queue, action) {
  2.   // 创建一个update,用于后续的更新,这里的action就是我们setState的入参
  3.   var update = {
  4.     lane: lane,
  5.     action: action,
  6.     eagerReducer: null,
  7.     eagerState: null,
  8.     next: null
  9.   };
  10.   // 这段闭环链表插入update的操作有没有很熟悉?
  11.   var pending = queue.pending;
  12.   if (pending === null) {
  13.     update.next = update;
  14.   } else {
  15.     update.next = pending.next;
  16.     pending.next = update;
  17.   }
  18.   queue.pending = update;
  19.   var alternate = fiber.alternate;
  20.     // 判断当前是否是渲染阶段
  21.     if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
  22.       var lastRenderedReducer = queue.lastRenderedReducer;
  23.        // 这个if语句里的一大段就是用来判断我们这次更新是否和上次一样,如果一样就不会在进行调度更新
  24.       if (lastRenderedReducer !== null) {
  25.         var prevDispatcher;
  26.         {
  27.           prevDispatcher = ReactCurrentDispatcher$1.current;
  28.           ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
  29.         }
  30.         try {
  31.           var currentState = queue.lastRenderedState;
  32.           var eagerState = lastRenderedReducer(currentState, action);
  33.           update.eagerReducer = lastRenderedReducer;
  34.           update.eagerState = eagerState;
  35.           if (objectIs(eagerState, currentState)) {
  36.             return;
  37.           }
  38.         } finally {
  39.           {
  40.             ReactCurrentDispatcher$1.current = prevDispatcher;
  41.           }
  42.         }
  43.       }
  44.     }
  45.     // 将携带有update的fiber进行调度更新
  46.     scheduleUpdateOnFiber(fiber, lane, eventTime);
  47.   }
  48. }
复制代码
上面的代码已经是我尽力精简的结果了。。。代码上有注释,各位看官凑合看下。
不愿细看的我来总结下
  1. dispatchAction
复制代码
做的事情:

  • 创建一个
    1. update
    复制代码
    并加入到
    1. fiber.hook.queue
    复制代码
    链表中,并且链表指针指向这个
    1. update
    复制代码

  • 判断当前是否是渲染阶段决定要不要马上调度更新;
  • 判断这次的操作和上次的操作是否相同, 如果相同则不进行调度更新;
  • 满足上述条件则将带有
    1. update
    复制代码
    1. fiber
    复制代码
    进行调度更新;
到这里我们又搞明白了一个问题:
为什么
  1. setState
复制代码
的值相同时,函数组件不更新?

updateState

我们这里不详细讲解调度更新的过程, 后面文章安排, 这里我们只需要知道,在接下来更新过程中,会再次执行我们的函数组件,这时又会调用
  1. useState
复制代码
方法了。前面讲过,React维护了两套
  1. hooks
复制代码
,一套用于初始化, 一套用于更新。 这个在调度更新时就已经完成了切换。所以我们这次调用
  1. useState
复制代码
方法会和之前初始化有所不同。
这次我们进入
  1. useState
复制代码
,会看到其实是调用的
  1. updateState
复制代码
方法
  1. function updateState(initialState) {
  2.   return updateReducer(basicStateReducer);
  3. }
复制代码
看到这几行代码,看官们应该就明白为什么网上有人说
  1. useState
复制代码
  1. useReducer
复制代码
相似。原来在
  1. useState
复制代码
的更新中调用的就是
  1. updateReducer
复制代码
啊。

updateReducer

本来很长,想让各位看官忍一忍。于心不忍,忍痛减了很多
  1. function updateReducer(reducer, initialArg, init) {
  2.   // 创建一个新的hook,带有dispatchAction创建的update
  3.   var hook = updateWorkInProgressHook();
  4.   var queue = hook.queue;
  5.   queue.lastRenderedReducer = reducer;
  6.   var current = currentHook;
  7.   var baseQueue = current.baseQueue;
  8.   var pendingQueue = queue.pending;
  9.   current.baseQueue = baseQueue = pendingQueue;
  10.   if (baseQueue !== null) {
  11.     // 从这里能看到之前讲的创建闭环链表插入update的好处了吧?直接next就能找到第一个update
  12.     var first = baseQueue.next;
  13.     var newState = current.baseState;
  14.     var update = first;
  15.     // 开始遍历update链表执行所有setState
  16.     do {
  17.       var updateLane = update.lane;
  18.       // 假如我们这个update上有多个setState,在循环过程中,最终都会做合并操作
  19.       var action = update.action;
  20.       // 这里的reducer会判断action类型,下面讲
  21.       newState = reducer(newState, action);
  22.       update = update.next;
  23.     } while (update !== null && update !== first);
  24.     hook.memoizedState = newState;
  25.     hook.baseState = newBaseState;
  26.     hook.baseQueue = newBaseQueueLast;
  27.     queue.lastRenderedState = newState;
  28.   }
  29.   var dispatch = queue.dispatch;
  30.   return [hook.memoizedState, dispatch];
  31. }
复制代码
上面的更新中,会循环遍历
  1. update
复制代码
进行一个合并操作,只取最后一个
  1. setState
复制代码
的值,这时候可能有人会问那直接取最后一个
  1. setState
复制代码
的值不是更方便吗?
这样做是不行的,因为
  1. setState
复制代码
入参可以是基础类型也可以是函数, 如果传入的是函数,它会依赖上一个
  1. setState
复制代码
的值来完成更新操作,下面的代码就是上面的循环中的
  1. reducer
复制代码
  1. function basicStateReducer(state, action) {
  2.   return typeof action === 'function' ? action(state) : action;
  3. }
复制代码
到这里我们搞明白了一个问题,多个
  1. setState
复制代码
是如何合并的?

updateWorkInProgressHook

下面是伪代码,我把很多的逻辑判断给删除了,免了太长又让各位看官难受,原来的代码里会判断当前的
  1. hook
复制代码
是不是第一个调度更新的
  1. hook
复制代码
,我这里为了简单就按第一个来解析
  1. function updateWorkInProgressHook() {
  2.   var nextCurrentHook;
  3.   nextCurrentHook = current.memoizedState;
  4.   var newHook = {
  5.       memoizedState: currentHook.memoizedState,
  6.       baseState: currentHook.baseState,
  7.       baseQueue: currentHook.baseQueue,
  8.       queue: currentHook.queue,
  9.       next: null
  10.       }
  11.   currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
  12.   return workInProgressHook;
  13. }
复制代码
从上面代码能看出来,
  1. updateWorkInProgressHook
复制代码
抛去那些判断, 其实做的事情也很简单,就是基于
  1. fiber.memoizedState
复制代码
创建一个新的
  1. hook
复制代码
结构覆盖之前的
  1. hook
复制代码
。前面
  1. dispatchAction
复制代码
讲到会把
  1. update
复制代码
加入到
  1. hook.queue
复制代码
中,在这里的
  1. newHook.queue
复制代码
上就有这个
  1. update
复制代码


总结

总结下
  1. useState
复制代码
初始化和
  1. setState
复制代码
更新:

    1. useState
    复制代码
    会在第一次执行函数组件时进行初始化,返回
    1. [state, dispatchAction]
    复制代码

  • 当我们通过
    1. setState
    复制代码
    也就是
    1. dispatchAction
    复制代码
    进行调度更新时,会创建一个
    1. update
    复制代码
    加入到
    1. hook.queue
    复制代码
    中。
  • 当更新过程中再次执行函数组件,也会调用
    1. useState
    复制代码
    方法,此时的
    1. useState
    复制代码
    内部会使用更新时的
    1. hooks
    复制代码

  • 通过
    1. updateWorkInProgressHook
    复制代码
    获取到
    1. dispatchAction
    复制代码
    创建的
    1. update
    复制代码

    1. updateReducer
    复制代码
    通过遍历
    1. update
    复制代码
    链表完成
    1. setState
    复制代码
    合并。
  • 返回
    1. update
    复制代码
    后的
    1. [newState, dispatchAction]
    复制代码
    .
还有两个问题
为什么
  1. setState
复制代码
后不能马上拿到最新的
  1. state
复制代码
的值?
  1. React
复制代码
其实可以这么做,为什么没有这么做,因为每个
  1. setState
复制代码
都会触发更新,
  1. React
复制代码
出于性能考虑,会做一个合并操作。所以
  1. setState
复制代码
只是触发了
  1. dispatchAction
复制代码
生成了一个
  1. update
复制代码
的动作,新的
  1. state
复制代码
会存储在
  1. update
复制代码
中,等到下一次
  1. render
复制代码
, 触发这个
  1. useState
复制代码
所在的函数组件执行,才会赋值新的
  1. state
复制代码
  1. setState
复制代码
到底是同步还是异步的?
同步的,假如我们有这样一段代码:
  1. const handleClick = () => {
  2.   setCount(2)
  3.   setCount(count => count + 1)
  4.   console.log('after setCount')
  5. }
复制代码
你会惊奇的发现页面还没有更新
  1. count
复制代码
,但是控制台已经打印了
  1. after setCount
复制代码

之所以表现上像是异步,是因为内部使用了
  1. try{...}finally{...}
复制代码
。当调用
  1. setState
复制代码
触发调度更新时,更新操作会放在
  1. finally
复制代码
中,返回去继续执行
  1. handlelick
复制代码
的逻辑。于是会出现上面的情况。
看完这篇文章, 我们可以弄明白下面这几个问题:

  • 为什么
    1. setState
    复制代码
    后不能马上拿到最新的
    1. state
    复制代码
    的值?
  • 多个
    1. setState
    复制代码
    是如何合并的?
    1. setState
    复制代码
    到底是同步还是异步的?
  • 为什么
    1. setState
    复制代码
    的值相同时,函数组件不更新?
    1. setState
    复制代码
    是怎么完成更新的?
    1. useState
    复制代码
    是什么时候初始化又是什么时候开始更新的?
到此这篇关于React超详细分析useState与useReducer源码的文章就介绍到这了,更多相关React useState与useReducer内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

来源:https://www.jb51.net/article/266718.htm
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

举报 回复 使用道具