您现在的位置是:网站首页> 编程资料编程资料

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

2023-12-09 288人已围观

简介 React超详细分析useState与useReducer源码_React_

热身准备

在正式讲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 ( 
勇敢牛牛, 不怕困难{count}
) }

初始化 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(new
                
                

-六神源码网