【问题标题】:Order of reducer execution in ReactReact 中 reducer 的执行顺序
【发布时间】:2020-09-25 10:20:17
【问题描述】:

Here,提到“你能相信 React 以与调用 setState 相同的顺序更新状态......是的”。我的问题是调度(useReducer)事件是否也以它们被调用的顺序运行?例如,考虑这样的事情:

const [states, dispatch] = useReducer(reducer, initialState)
dispatch('a')
dispatch('b')

我能否确定 reducer 函数中的所有逻辑 都使用参数 'a' 执行,并且使用参数 'b' 执行(调用参数'b'使用第一次调用修改的状态)?

修改:reducers 和 setStates 结合起来怎么样?他们的通话顺序是否也保持不变?例如,setState、reducer(使用 setState 中设置的状态值)。

【问题讨论】:

    标签: reactjs react-hooks use-reducer


    【解决方案1】:

    是的,reducer 是同步运行的纯函数,因此任何单个调度的动作都会在下一个动作之前处理。

    这可能有助于您更好地理解 reducer 功能:https://redux.js.org/basics/data-flow

    功能组件主体是完全同步的,所以一切都是按照每个渲染周期调用的顺序进行的。链接来自 redux,但 reducer 的工作方式相同,并且可以互换,即 reducer 函数具有签名 (state, action) => nextState

    所有钩子值都在渲染周期之间起作用,这意味着,在渲染周期中“排队”的所有状态更新和调度动作都按该顺序进行批处理。

    给定:

    dispatch('a')
    dispatch('b')
    

    dispatch('b') 减速器案例将在dispatch('a') 案例完成后运行

    更新

    您可以查看useStateuseReducer 挂钩的源代码,看看它们将以相同的同步顺序方式处理更新。

    Dispatch

    type Dispatch<A> = A => void;
    

    这里可以看到dispatch是一个同步函数。

    useState

    export function useState<S>(
      initialState: (() => S) | S,
    ): [S, Dispatch<BasicStateAction<S>>] {
      const dispatcher = resolveDispatcher();
      return dispatcher.useState(initialState);
    }
    

    useReducer

    export function useReducer<S, I, A>(
      reducer: (S, A) => S,
      initialArg: I,
      init?: I => S,
    ): [S, Dispatch<A>] {
      const dispatcher = resolveDispatcher();
      return dispatcher.useReducer(reducer, initialArg, init);
    }
    

    useStateuseReducer 都使用相同的内部调度系统(即resolveDispatcher)。由于useReducer 无法处理异步操作(例如 Redux-Thunks),因此没有其他选项可以在减速器中同步处理操作之前 em> 处理下一个分派的动作。

    更新 2

    修改:reducers 和 setStates 结合起来怎么样?是他们的 通话顺序还保留吗?例如,一个 setState、一个 reducer(它使用 setState 中设置的状态值)。

    将保持顺序,但您将无法将状态更新加入队列,然后根据更新后的状态调度操作。这是由于 React 状态更新是在 渲染周期之间异步处理的。换句话说,任何setState(newState) 在下一个渲染周期之前都无法访问。

    例子:

    const [countA, setCountA] = useState(0);
    const [countB, dispatch] = useReducer(.....);
    
    ...
    
    setCountA(c => c + 1); // enqueues countA + 1, next countA is 1
    dispatch(setCountB(countA)); // dispatches with countA === 0
    setCountA(c => c + 1); // enqueues countA + 1, next countA is 2
    dispatch(setCountB(countA)); // dispatches with countA === 0
    

    【讨论】:

    • 那么 reducer 和 setStates 呢?他们的通话顺序是否也保持不变?例如,setState、reducer(使用 setState 中设置的状态值)。
    • 您提供的链接与redux有关。我的问题是关于 React。
    • 任何使用任何状态值的函数都将使用当前渲染周期中的状态值。功能组件主体是完全同步的,所以一切都是按照每个渲染周期调用的顺序处理的。是的,它是 redux,但 reducer 的工作方式相同,并且可以互换,即 reducer 函数具有签名 (state, action) =&gt; nextState。当您询问“reducers 和 setStates 结合”时,您能否进一步澄清您的意思?
    • 看来setStates的顺序是保留的。此外,还保留了调度的顺序。 setStates 和 dispatches 之间的顺序是否也保留了?
    • @Shayan 我相信这两者之间是不相关的,因为来自渲染周期的所有更新都将使用当前状态值,这意味着包含任何状态值的 any 调度操作将使用that 渲染周期的当前状态的值,所以无论你调用setState 然后dispatch,还是dispatch 然后setState,结果都是一样的。状态集将在在下一个渲染周期中可用,与来自调度操作的reducer 的nextState 相同。
    【解决方案2】:

    我看到的行为是react 中的dispatch 不是同步的,尽管许多人坚持。 reducer 是同步的,但似乎不能保证 dispatch 的完成保证生成的 reducer 已经完成。这在redux 中很多不同,但如果我们在谈论React,至少在17.0.2 中。

    这是一个示例测试用例:

    import { useReducer } from "react";
    import "./styles.css";
    
    function reducer(state, action) {
      console.log("REDUCER", state, action);
      switch (action.type) {
        case "INCREMENT":
          return state + 1;
        default:
          return state;
      }
    }
    
    export default function App() {
      const [state, dispatch] = useReducer(reducer, 0);
    
      const myDispatch = function (...args) {
        console.log("dispatched", args);
        return dispatch(...args);
      };
    
      const onClick = function () {
        // BOTH dispatches fired before any reducer!
        myDispatch({ type: "INCREMENT" });
        myDispatch({ type: "INCREMENT" });
      };
    
      const onClickAsync = async function () {
        // reducer finishes before next dispatch called
        await myDispatch({ type: "INCREMENT" });
        await myDispatch({ type: "INCREMENT" });
      };
    
      return (
        <>
          <pre>{state}</pre>
          <button onClick={onClick}>Increment Broken</button>
          <button onClick={onClickAsync}>Increment Working</button>
        </>
      );
    }
    

    沙盒:https://codesandbox.io/s/dispatch-not-sync-db9wn?file=/src/App.tsx

    在该示例中,如果您单击 Increment Broken 按钮,我将一个接一个地执行调度,您将在控制台中看到两个调度的日志语句在任何 reducer 调用之前显示。

    另一方面,如果您单击Increment Working 按钮,您将看到它正确地进行了调度 -> 减速器 -> 调度 -> 减速器。

    reducer 的执行保证按照排队的顺序执行,第二个 reducer 收到的 state 确实是前一个 reducer 的 state,正确。因此,基本上这仅在第二次调度的排队取决于减速器在开始下一次调度之前完成时才重要。

    【讨论】:

    • 我在单击任一按钮时看到相同的行为,即记录的输出是“dispatch,reducer,dispatch,reducer”。我不确定您要表达什么观点,因为您的 myDispatch 函数既没有声明为 async 也没有返回 Promise,因此您不能 await 它,因为没有什么可等待的。除了onClickAsync 之外的所有代码都是完全同步的。 OP 的问题是关于调度的处理顺序,您的代码证明并同意我的回答,即它们是按照调度的顺序处理的。
    • 啊,我突然想到为什么您会看到您在沙盒中看到的行为。更新不是批处理的,因为您将它们排入异步回调队列。请参阅此answer,其中包含有关 React 和批量/非批量更新的一些解释。
    猜你喜欢
    • 1970-01-01
    • 2017-02-02
    • 1970-01-01
    • 1970-01-01
    • 2020-08-08
    • 1970-01-01
    • 2012-09-23
    • 2021-08-01
    • 1970-01-01
    相关资源
    最近更新 更多