React超详细分析useState与useReducer源码

目录
  • 热身准备
  • 为什么会有hooks
  • hooks执行时机
  • 两套hooks
  • hooks存储
  • 初始化 mount
    • useState
    • mountWorkInProgressHook
  • 更新update
    • updateState
    • updateReducer
  • updateWorkInProgressHook
  • 总结

热身准备

在正式讲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的链表搞混了。

接下来,让我们带着下面几个问题看文章:

  1. 为什么setState后不能马上拿到最新的state的值?
  2. 多个setState是如何合并的?
  3. setState到底是同步还是异步的?
  4. 为什么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
  • 判断当前是否是渲染阶段决定要不要马上调度更新;
  • 判断这次的操作和上次的操作是否相同, 如果相同则不进行调度更新;
  • 满足上述条件则将带有updatefiber进行调度更新;

到这里我们又搞明白了一个问题:

为什么setState的值相同时,函数组件不更新?

updateState

我们这里不详细讲解调度更新的过程, 后面文章安排, 这里我们只需要知道,在接下来更新过程中,会再次执行我们的函数组件,这时又会调用useState方法了。前面讲过,React维护了两套hooks,一套用于初始化, 一套用于更新。 这个在调度更新时就已经完成了切换。所以我们这次调用useState方法会和之前初始化有所不同。

这次我们进入useState,会看到其实是调用的updateState方法

function updateState(initialState) {
  return updateReducer(basicStateReducer);
}

看到这几行代码,看官们应该就明白为什么网上有人说useStateuseReducer相似。原来在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更新:

  1. useState会在第一次执行函数组件时进行初始化,返回[state, dispatchAction]
  2. 当我们通过setState也就是dispatchAction进行调度更新时,会创建一个update加入到hook.queue中。
  3. 当更新过程中再次执行函数组件,也会调用useState方法,此时的useState内部会使用更新时的hooks
  4. 通过updateWorkInProgressHook获取到dispatchAction创建的update
  5. updateReducer通过遍历update链表完成setState合并。
  6. 返回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的逻辑。于是会出现上面的情况。

看完这篇文章, 我们可以弄明白下面这几个问题:

  1. 为什么setState后不能马上拿到最新的state的值?
  2. 多个setState是如何合并的?
  3. setState到底是同步还是异步的?
  4. 为什么setState的值相同时,函数组件不更新?
  5. setState是怎么完成更新的?
  6. useState是什么时候初始化又是什么时候开始更新的?

到此这篇关于React超详细分析useState与useReducer源码的文章就介绍到这了,更多相关React useState与useReducer内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • React commit源码分析详解

    目录 总览 commitBeforeMutationEffects commitMutationEffects 插入 dom 节点 获取父节点及插入位置 判断当前节点是否为单节点 在对应位置插入节点 更新 dom 节点 更新 HostComponent 更新 HostText 删除 dom 节点 unmountHostComponents commitNestedUnmounts commitUnmount commitLayoutEffects 执行生命周期 处理回调 总结 总览 commit

  • React Fiber源码深入分析

    目录 前言 React架构前世今生 React@15及之前 React@16及之后 Fiber Fiber简单理解 Fiber结构 Fiber工作原理 mount update 前言 本次React源码参考版本为17.0.3. React架构前世今生 查阅文档了解到, React@16.x是个分水岭. React@15及之前 在16之前,React架构大致可以分为两层: Reconciler: 主要职责是对比查找更新前后的变化的组件: Renderer: 主要职责是基于变化渲染页面: 但是Rea

  • React前端开发createElement源码解读

    目录 React 与 Babel 元素标签转译 组件转译 子元素转译 createElement 源码 函数入参 第一段代码 __self 和 __source 第二段代码 props 对象 第三段代码 children 第四段代码 defaultProps 第五段代码 owner ReactElement 源码 REACT_ELEMENT_TYPE 回顾 React 与 Babel 元素标签转译 用过 React 的同学都知道,当我们这样写时: <div id="foo">

  • React深入分析useEffect源码

    目录 热身准备 初始化 mount 更新 update updateEffect 执行副作用 总结 热身准备 这里不再讲useLayoutEffect,它和useEffect的代码是一样的,区别主要是: 执行时机不同: useEffect是异步, useLayoutEffect是同步,会阻塞渲染: 初始化 mount mountEffect 在所有hook初始化时都会通过下面这行代码实现hook结构的初始化和存储,这里不再讲mountWorkInProgressHook方法 var hook =

  • React超详细分析useState与useReducer源码

    目录 热身准备 为什么会有hooks hooks执行时机 两套hooks hooks存储 初始化 mount useState mountWorkInProgressHook 更新update updateState updateReducer updateWorkInProgressHook 总结 热身准备 在正式讲useState,我们先热热身,了解下必备知识. 为什么会有hooks 大家都知道hooks是在函数组件的产物.之前class组件为什么没有出现hooks这种东西呢? 答案很简单,

  • 超详细的Intellij IDEA 看源码必备技能

    最近正好也没什么可忙的,就回过头来鼓捣过去的知识点,到Servlet部分时,以前学习的时候硬是把从上到下的继承关系和接口实现记得乱七八糟. 这次利用了IDEA的diagram,结果一目了然,也是好用到炸裂,就此分享. 1.查看图形形式的继承链 在你想查看的类的标签页内,点击右键,选择 Diagrams,其中有 show 和 show ... Popup,只是前者新建在标签页内,后者以浮窗的形式展示: 实际上,你也可以从左边的项目目录树中,对你想查看的类点击右键,同样选择Diagrams,效果是一

  • Android用于加载xml的LayoutInflater源码超详细分析

    1.在view的加载和绘制流程中:文章链接 我们知道,定义在layout.xml布局中的view是通过LayoutInflate加载并解析成Java中对应的View对象的.那么具体的解析过程是哪样的. 先看onCreate方法,如果我们的Activity是继承自AppCompactActivity.android是通过getDelegate返回的对象setContentView,这个mDelegate 是AppCompatDelegateImpl的实例. @Override protected

  • Java CopyOnWriteArrayList源码超详细分析

    目录 一.概述 二.类图 三.核心方法 1.add() 2.set() 3.remove() 4.get() 5.size() 四.总结 一.概述 CopyOnWriteArrayList是基于写时复制技术实现的,适用于读多写少场景下的线程安全的并发容器.读操作永远不会加锁,读读.读写都不会冲突,只有写写需要等待.写操作时,为了不影响其它线程的读取,它会进行一次自我复制,待数据写入完成后再替换array数组.array数组是被volatile修饰的,它被修改后可以被其他线程立刻发现. publi

  • Redis对象与redisObject超详细分析源码层

    目录 一.对象 二.对象的类型及编码 redisObject 结构体 三.不同对象编码规则 四.redisObject结构各字段使用范例 4.1 类型检查(type字段) 4.2 多态命令的实现(encoding) 4.3 内存回收和共享对象(refcount) 4.4 对象的空转时长(lru) 五.对象在源码中的使用 5.1 字符串对象 5.1.1字符串对象创建 5.1.2 字符串对象编码 5.1.3 字符串对象解码 5.1.4 redis对象引用计数及自动清理 六.总结 以下内容是基于Red

  • Java超详细分析泛型与通配符

    目录 1.泛型 1.1泛型的用法 1.1.1泛型的概念 1.1.2泛型类 1.1.3类型推导 1.2裸类型 1.3擦除机制 1.3.1关于泛型数组 1.3.2泛型的编译与擦除 1.4泛型的上界 1.4.1泛型的上界 1.4.2特殊的泛型上界 1.4.3泛型方法 1.4.4类型推导 2.通配符 2.1通配符的概念 2.2通配符的上界 2.3通配符的下界 题外话: 泛型与通配符是Java语法中比较难懂的两个语法,学习泛型和通配符的主要目的是能够看懂源码,实际使用的不多. 1.泛型 1.1泛型的用法

  • 非常适合新手学生的Java线程池超详细分析

    目录 线程池的好处 创建线程池的五种方式 缓存线程池CachedThreadPool 固定容量线程池FixedThreadPool 单个线程池SingleThreadExecutor 定时任务线程池ScheduledThreadPool ThreadPoolExecutor创建线程池(十分推荐) ThreadPoolExecutor的七个参数详解 workQueue handler 如何触发拒绝策略和线程池扩容? 线程池的好处 可以实现线程的复用,避免重新创建线程和销毁线程.创建线程和销毁线程对

  • Java超详细分析@Autowired原理

    目录 @Autowired使用 @Autowired源码分析 1.查找所有@Autowired 2. 注入 2.1 字段注入(AutowiredFieldElement) 2.2 方法注入(AutowiredMethodElement) @Autowired使用 构造函数注入 public Class Outer { private Inner inner; @Autowired public Outer(Inner inner) { this.inner = inner; } } 属性注入 p

  • Spring Boot超详细分析启动流程

    目录 一.Spring Boot 工程结构 二.Spring Boot 启动流程 三.Spring Boot 启动流程源码剖析 1.创建一个Spring Boot 工程 2.SpringBootApplication启动入口 3.Spring Boot 初始化分析 4.Spring Boot 启动深入分析 四.总结 一.Spring Boot 工程结构 下载Spring Boot工程源码, 下载地址 模块代码结构: 比较重要的是Spring-boot.Spring-boot-autoconfig

  • C++超详细分析type_traits

    目录 定义基础常量 基础类型判断 类型处理 类型选择 判断是否相同 tips 实现is_base_of 本篇文章旨在引导大家自行实现type_traits的基础代码. 模板编程不像常规的代码,可以有if-else这些流控制语句,我们需要充分利用模板.模板特例.类型转换等特性来实现编译期的一系列判断和类型转换. 定义基础常量 第一步,我们需要定义true和false两个常量,所有的type_traits都基于此.我们的目的就是要用一个模板类型来表示是非,其中的value正好是这两个值.之后我们更高

随机推荐