React源码解读(4)——hooks之useState

·  阅读 394
React源码解读(4)——hooks之useState

前言

建议没有看过前面系列文章从前面看起,这是本系列的第四篇文章,本片文章主要介绍的是reacthooksuseState,useStateuseReducer,但useStateuseReducer在源码当中用的是同一套代码,所以我们只需先搞明白useState,useReduecr即不在话下,接下来一起今天的学习!

准备工作

首先我们来看下面道题

const Increaser = () => {
  const [count, setCount] = useState(0);
  
  const increase = useCallback(() => {
    setCount(count + 1);
  }, [count]);
  
  const handleClick = () => {
    increase();
    increase();
    increase();
  };
  
  return (
    <>
      <button onClick={handleClick}>+</button>
      <div>Counter: {count}</div>
    </>
  );
}
复制代码

在我们执行handleClick时,我们会发现count只会增加一,那么这是为什么呢?这是因为在第一次执行setCount,count会变为1,但在第二次和第三次的时候,count拿的还是旧的状态(count为0),所以计算出count为1,这是因为状态变量在此次才会的到更新。解决这个办法也简单,就是将函数使用函数的方式来更新状态:

const Increaser = () => {
  const [count, setCount] = useState(0);
  
  const increase = useCallback(() => {
    setCount(count => count + 1);
  }, [count]);
  
  const handleClick = () => {
    increase();
    increase();
    increase();
  };
  
  return (
    <>
      <button onClick={handleClick}>+</button>
      <div>Counter: {count}</div>
    </>
  );
}
复制代码

从这个问题中就能发现,至于这个问题的答案在下面有做解释,如果我们不知道hooks的一些运行机制,我们很有可能有遇到一些未知的错误,如闭包问题,我们都知道React hooks是基于闭包实现的,一个场景是我们发现我们明明更新了状态,但是却一直拿到就得状态,这就更加促使我们去了解一些hooks的源码。

为什么会有hooks呢

大家都知道hooks是在函数组件的产物。之前class组件为什么没有出现hooks这种东西呢?

答案很简单,不需要。

因为在class组件中,在运行时,只会生成一个实例,而在这个实例中会保存组件的state等信息。在后续的更新操作中,也只是调用其中的render方法,实例中的信息不会丢失。而在函数组件中,每次渲染,更新都会去执行这个函数组件,所以在函数组件中是没办法保存state等信息的。为了保存state等信息,于是有了hooks,用来记录函数组件的状态,执行副作用

那么我们又会有一个问题,既然函数组件更新都会重新执行函数,那么hooks是怎么记录状态的呢,答案是记录在fiber节点中。

两套hooks

在我们刚学react的时候,我们或许会有疑问,为什么我们的写的hooks不能放在函数或者条件语句里呢?这是因React维护了两套hooks,在mount时一套,在update时一套,其实在在很多时候我们都要有这样一个思想就是mountupdate在这两个时期做的事是不同的。

hooks存储

每个初始化的hook都会创建一个hook结构,多个hook是通过声明顺序用链表的结构相关联,最终这个链表会存放在fiber.memoizedState中:

var hook = {
    memoizedState: null,   // 存储hook操作,不要和fiber.memoizedState搞混了
    baseState: null,
    baseQueue: null,
    queue: null,    // 存储该hook本次更新阶段的所有更新操作
    next: null      // 链接下一个hook
};
复制代码

初始化mount

我们先来看下useState()函数:

function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}
复制代码

上面的dispatcher就会涉及到开始提到的两套hooks的变换使用,initialState是我们传入useState的参数,可以是基础数据类型,也可以是函数,我们主要看dispatcher.useState(initialState)方法,因为我们这里是初始化,它会调用mountState方法:

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  // 创建并返回当前的hook
  const hook = mountWorkInProgressHook();

  // ...赋值初始state

  // 创建queue
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });

  // ...创建dispatch
  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,应该是开发中最常用的hooks之一了,了解一些基本原理能够避免在开发当中的错误,当然挺多错误还是在开发中才有体会,例如我们拿到了旧的状态,闭包问题,关于useEffecthooks在下周应该会更新,最近一段时间大部分花在webpack优化上,导致一段时间没有更新,希望有机会能写一篇文章记录一下关于webpack

React源码系列

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改