react 函数组件中使用 useState 改变值后立刻获取最新值

为什么说 redux 和 react-hooks 有些渊源?

redux 是基于发布订阅模式,在内部实现一个状态,然后加一些约定和限制,外部不可以直接改变这个状态,只能通过 redux 提供的一个代理方法去触发这个状态的改变,使用者可以通过 redux 提供的订阅方法订阅事件,当状态改变的时候,redux 帮助我们发布这个事件,使得各个订阅者获得最新状态,是不是很简单,大道至简~

下面提供了个简版的 redux 代码

1/**
2 *
3 * @param {*} reducer 处理状态方法
4 * @param {*} preloadState 初始状态
5 */
6function createStore(reducer, preloadState) {
7  /** 存储状态 */
8  let state = preloadState;
9
10  /** 存储事件 */
11  const listeners = [];
12
13  /** 获取状态方法,返回state */
14  const getState = () => {
15    return state;
16  };
17
18  /**
19   * 订阅事件,将事件存储到listeners中
20   * @param {*} listener 事件
21   */
22  const subscribe = (listener) => {
23    listeners.push(listener);
24  };
25
26  /**
27   * 派发action,发布事件
28   * @param {*} action 动作
29   */
30  const dispatch = (action) => {
31    state = reducer(state, action);
32    listeners.forEach((listener) => listener());
33  };
34
35  /** 状态机 */
36  const store = {
37    getState,
38    subscribe,
39    dispatch,
40  };
41
42  return store;
43}
44
45export default createStore;

为什么说 react-hooks 跟 redux 有渊源,因为 Dan 总把 redux 的这种思想带到了 react-hooks 中,通过创建一个公共 hookStates 来存储所有的 hooks,通过添加一个索引 hookIndex 来顺序执行每一个 hook,所以 react-hooks 规定 hooks 不能使用在 if 语句和 for 语句中,来保持 hooks 按顺序执行,在 react-hooks 中有一个叫 useReducer 的 hooks,正是按 redux 的这个思想实现的,而我们常用的 useState 只是 useReducer 的一个语法糖,有人可能会说这个 hookStates 会不会很大,所有的 hooks 都存在里边,毕竟一个项目中有那么多组件,我说的只是简单的实现原理,我们知道 react 在 16 的几个版本之后就加入了 fiber 的概念,在 react 内部实现中,会把每个组件的 hookStates 存放到 fiber 节点上,来维护各个节点的状态,react 每次 render 之后都会把 hookIndex 置为 0,来保证下次 hooks 执行是从第一个开始执行,每个 hooks 执行完也会把 hookIndex++,下面是 useReducer 的简单实现:

1/** @param hookStates 存储所有hooks */
2const hookStates = [];
3
4/** @param hookIndex 索引 */
5let hookIndex = 0;
6
7/**
8 *
9 * useReducer
10 * @param {*} reducer 状态处理函数
11 * @param {*} initialState 初始状态,可以传入函数实现懒加载
12 * @returns
13 */
14function useReducer(reducer, initialState) {
15  hookStates[hookIndex] =
16    hookStates[hookIndex] || typeof initialState === "function"
17      ? initialState()
18      : initialState;
19
20  /**
21   *
22   * dispatch
23   * @param {*} action 动作
24   * @returns
25   */
26  const dispatch = (action) => {
27    hookStates[hookIndex] = reducer
28      ? reducer(hookStates[hookIndex], action)
29      : action;
30    /** React内部方法实现重新render */
31    scheduleUpdate();
32  };
33  return [hookStates[hookIndex++], dispatch];
34}

前文说过 useState 是 useReducer 的语法糖,所以下面是 useState 的实现

1/**
2 *
3 * useState 直接将reducer传为null,调用useReducer并返回
4 * @param {*} initialState 初始化状态
5 * @returns
6 */
7function useState(initialState) {
8  return useReducer(null, initialState);
9}

如何实现 useState 改变值之后立刻获取最新的状态?

react-hooks 的思想其实是同步逻辑,但是在 react 的合成事件中状态更新是异步的,看一下下面这个场景:

1function App() {
2  const [state, setstate] = useState(0);
3
4  const setT = () => {
5    setstate(2);
6    func();
7  };
8
9  const func = () => {
10    console.log(state);
11  };
12
13  return (
14    <div className="App">
15      <header className="App-header">
16        <button onClick={setT}>set 2</button>
17      </header>
18    </div>
19  );
20}

当我们点击 set 2 按钮时,控制台打印的还是上一次的值 0,而不是最新的 2

因为在 react 合成事件中改变状态是异步的,出于减少 render 次数,react 会收集所有状态变更,然后比对优化,最后做一次变更,在代码中可以看出,func 的调用和 setstate 在同一个宏任务中,这是 react 还没有 render,所以直接使用 state 获取的肯定是上一次闭包里的值 0

有的人可能会说,直接将最新的值当作参数传递给 func 不就行了吗,对,这也是一种解决办法,但是有时不只是一个状态,可能要传递的参数很多,再有也是出于对 react-hooks 的深入研究,所以我选择通过自定义 hooks 实现在 useState 改变值之后立刻获取到最新的值,我们先看下实现的效果,代码变更如下:

如何实现 useSyncCallback?

1// param: callback为回调函数
2// 用法 const newFunc = useSyncCallback(yourCallback)
3import { useEffect, useState, useCallback } from "react";
4
5const useSyncCallback = (callback) => {
6  const [proxyState, setProxyState] = useState({ current: false });
7
8  const Func = useCallback(() => {
9    setProxyState({ current: true });
10  }, [proxyState]);
11
12  useEffect(() => {
13    if (proxyState.current === true) setProxyState({ current: false });
14  }, [proxyState]);
15
16  useEffect(() => {
17    proxyState.current && callback();
18  });
19
20  return Func;
21};
22
23export default useSyncCallback;

在这里还需要介绍一个知识点 useEffect,useEffect 会在每次函数式组件 render 之后执行,可以通过传递第二个参数,配置依赖项,当依赖项变更 useEffect 才会更新,如果第二个参数传递一个空数组[],那么 useEffect 就会只执行一次,react-hooks 正是使用 useEffect 来模拟 componentDidMount 和 componentDidUpdate 两个生命周期,也可以通过在 useEffect 中 return 一个函数模拟 componentWillUnmount,那么 useEffect 是如何实现在 render 之后调用的呢,原理就是在 useEffect hooks 中会封装一个宏任务,然后把传进来的回调函数放到宏任务中去执行,下面实现了一个简版的 useEffect:

1/** @param hookStates 存储所有hooks */
2const hookStates = [];
3
4/** @param hookIndex 索引 */
5let hookIndex = 0;
6
7/**
8 *
9 * useReducer
10 * @param {*} callback 副作用函数
11 * @param {*} dependencies 依赖项
12 * @returns
13 */
14function useEffect(callback, dependencies) {
15  /** 判断当前索引下hookStates是否存在值 */
16  if (hookStates[hookIndex]) {
17    /** 如果存在就取出,并结构出destroyFunc销毁函数,和上一次依赖项 */
18    const [destroyFunc, lastDep] = hookStates[hookIndex];
19    /** 判断当前依赖项中的值和上次依赖项中的值有没有变化 */
20    const isSame =
21      dependencies &&
22      dependencies.every((dep, index) => dep === lastDep[index]);
23    if (isSame) {
24      /** 如果没有变化就把索引加一,hooks向后遍历 */
25      hookIndex++;
26    } else {
27      /** 如果有变化,并且存在销毁函数,就先调用销毁函数 */
28      destroyFunc && destroyFunc();
29      /** 创建一个新的宏任务,调用callback获取最新的销毁函数,并将销毁函数和依赖项存入hookStates中 */
30      setTimeout(() => {
31        const destroyFunction = callback();
32        hookStates[hookIndex++] = [destroyFunction, dependencies];
33      });
34    }
35  } else {
36    /** 如果hookStates中不存在,证明是首次执行,直接创建一个宏任务,调用callback获取最新的销毁函数,并将销毁函数和依赖项存入hookStates中 */
37    setTimeout(() => {
38      const destroyFunction = callback();
39      hookStates[hookIndex++] = [destroyFunction, dependencies];
40    });
41  }
42}

接下来我们就来使用 useEffect 实现我们想要的结果,因为 useEffect 是在 react 组件 render 之后才会执行,所以在 useEffect 获取的状态一定是最新的,所以利用这一点,把我们写的函数放到 useEffect 执行,函数里获取的状态就一定是最新的

1useEffect(() => {
2  proxyState.current && callback();
3});

首先,在 useSyncCallback 中创建一个标示 proxyState,初始的时候会把 proxyState 的 current 值赋成 false,在 callback 执行之前会先判断 current 是否为 true,如果为 true 就允许 callback 执行,若果为 false,就跳过不执行,因为 useEffect 在组件 render 之后,只要依赖项有变化就会执行,所以我们无法掌控我们的函数执行,在 useSyncCallback 中创建一个新的函数 Func,并返回,通过这个 Func 来模拟函数调用,

1const [proxyState, setProxyState] = useState({ current: false });

在这个函数中我们要做的就是变更 prxoyState 的 current 值为 true,来使得让 callback 被调用的条件成立,同时触发 react 组件 render 这样内部的 useEffect 就会执行,随后调用 callback 实现我们想要的效果。