乐趣区

React进阶为什么useReducer能提高代码的可读性和性能

或许你已经知道,当多个 state 需要一起更新时,就应该考虑使用 useReducer。或许你也已经听说过,” 使用 useReducer 能够提高应用的性能 ”。但是篇文章希望帮助你理解:为什么 useReducer 能提高代码的可读性和性能。

举一个例子:

function Counter() {const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  useEffect(() => {const id = setInterval(() => {setCount(c => c + step); // 依赖其他 state 来更新
    }, 1000);
    return () => clearInterval(id);
    // 为了保证 setCount 中的 step 是最新的,// 我们还需要在 deps 数组中指定 step
  }, [step]);

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => setStep(Number(e.target.value))} />
    </>
  );
}

这段代码能够正常工作,但是随着相互依赖的状态变多,setState 中的逻辑会变得很复杂useEffect 的 deps 数组也会变得更复杂,降低可读性的同时,useEffect 重新执行时机变得更加难以预料。

使用 useReducer 替代 useState 以后:

function Counter() {const [state, dispatch] = useReducer(reducer, initialState);
  const {count, step} = state;

  useEffect(() => {const id = setInterval(() => {dispatch({ type: 'tick'});
      }, 1000);
    return () => clearInterval(id);
  }, []); // deps 数组不需要包含 step

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => setStep(Number(e.target.value))} />
    </>
  )
}

现在组件只需要发出 action,而无需知道如何更新状态 。也就是将What to doHow to do解耦。
另一方面,step 的更新不会造成 useEffect 的失效、重执行。因为现在 useEffect 依赖于 dispatch,而不依赖于 状态值 (得益于上面的解耦模式)。 这是一个重要的模式,能用来避免 useEffect、useMemo、useCallback 需要频繁重执行的问题

以下是 state 的定义,其中 reducer 封装了“如何更新状态”的逻辑:

const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {const { count, step} = state;
  if (action.type === 'tick') {return { count: count + step, step};
  } else if (action.type === 'step') {return { count, step: action.step};
  } else {throw new Error();
  }
}

总结:

  • 当状态更新逻辑比较复杂的时候,就应该考虑使用 useReducer。因为:

    • reducer 比 setState 更加擅长描述“如何更新状态”。比如,reducer 能够读取相关的状态、同时更新多个状态。
    • 【组件负责发出 action,reducer 负责更新状态】的解耦模式,使得代码逻辑变得更加清晰。
    • 简单来记,就是每当编写 setState(prevState => newState) 的时候,就应该考虑是否值得将它换成 useReducer。
  • 通过传递 useReducer 的 dispatch,可以减少状态值的传递

    • useReducer 返回的 dispatch 总是同一个函数引用。
    • 得益于前面的解耦模式,useEffect 函数体、callback function 只需要使用 dispatch 来发出 action,而无需直接依赖 状态值 。因此在 useEffect、useCallback、useMemo 的 deps 数组中无需包含 状态值,也减少了它们更新的需要。不但能提高可读性,而且能提升性能(useCallback、useMemo 的更新往往会造成子组件的刷新)。

高级用法:内联 reducer

你可以将 reducer 声明在组件内部,从而能够通过闭包访问 props、以及前面的 hooks 结果:

function Counter({step}) {const [count, dispatch] = useReducer(reducer, 0);
  function reducer(state, action) {if (action.type === 'tick') {
      // 可以通过闭包访问到组件内部的任何变量
      // 包括 props,以及 useReducer 之前的 hooks 的结果
      return state + step;
    } else {throw new Error();
    }
  }

  useEffect(() => {const id = setInterval(() => {dispatch({ type: 'tick'});
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

这个能力可能会出乎很多人的意料。因为大部分人对 reducer 的触发时机的理解是错误的(包括以前的我)。我以前理解的触发时机是这样:

  1. 某个 button 被用户点击,它的 onClick 被调用,其中执行了dispatch({type:'add'}),React 框架安排一次更新
  2. React 框架处理刚才安排的更新,调用reducer(prevState, {type:'add'}),来得到新的状态
  3. React 框架用新的状态来渲染组件树,渲染到 Counter 组件的 useReducer 时,返回上一步得到的新状态即可

但是实际上,React 会 在下次渲染的时候 再调用 reducer 来处理 action:

  1. 某个 button 被用户点击,它的 onClick 被调用,其中执行了dispatch({type:'add'}),React 框架安排一次更新
  2. React 框架处理刚才安排的更新,开始重渲染组件树
  3. 渲染到 Counter 组件的 useReducer 时,调用reducer(prevState, {type:'add'}),处理之前的 action

重要的区别在于,被调用的 reducer 是 本次渲染 的 reducer 函数,它的闭包捕获到了 本次渲染 的 props。

如果按照上面的错误理解,被调用的 reducer 是上次渲染的 reducer 函数,它的闭包捕获到上次渲染的 props(因为本次渲染还没开始呢)

事实上,如果你简单地使用 console.log 来打印执行顺序,会发现reducer 是在新渲染执行 useReducer 的时候被同步执行的

  console.log("before useReducer");
  const [state, dispatch] = useReducer(reducer, initialState);
  console.log("after useReducer", state);

  function reducer(prevState, action) {
    // these current state var are not initialized yet
    // would trigger error if not transpiled to es5 var
    console.log("reducer run", state, count, step);
    return prevState;
  }

调用 dispatch 以后会输出:

before useReducer
reducer undefined undefined undefined
after useReducer {count: 1, step: 1}

证明 reducer 确实被 useReducer 同步地调用来获取新的 state。
codesandbox demo

参考资料

  • Decoupling Updates from Actions
  • https://twitter.com/dan_abram…
退出移动版