当我研究如何使用 useReducer 的时候,我同时获得了关于 JavaScript 和 React 的知识

首先

我正在使用React来创建我的投资组合。

在表单页面使用 useReducer 的时候,我发现对于具有层级的对象处理得不够好。
具体来说,当在同一个函数内多次使用 dispatch 来更新对象时,会出现对于深层次的对象更新遗漏的情况。

当我开始调查时,我对React的机制以及JavaScript本身的机制有了更深的理解,所以这是我个人的备忘录。

以下是用于说明的代码:

事件的起因

我正在创建一个关于具有层次结构的对象的表单。
以下是一个简化的例子。

    const initdata = {
      dept1_a: "dept1_a",
      dept1_b: "dept1_b",
      dept1_c: {
        dept2_d: "dept2_d",
        dept2_e: "dept2_e"
      }
    };

我使用 useReducer 来管理这个对象,并将对象的值赋给每个 input 元素的 value,将 dispatch 函数赋给 onChange。(后面会明白,这是 reducer 函数的原因)

  const reducer = (data, newDetails) => ({ ...data, ...newDetails });
  const [data1, dispatch1] = useReducer(reducer, initdata);

在每个项目中创建一个输入元素,然后在其onChange事件中向dispatch函数传递值并更新对象是没有问题的。
然而,当尝试对整个对象进行更新,例如在发送之前进行检查时,深层次的项目将不会被更新。

举个例子, 可以像下面这样想象。

    const initdata = {
      dept1_a: "UPDATED BY BOTTON",
      dept1_b: "UPDATED BY BOTTON",
      dept1_c: {
        dept2_d: "dept2_d", // Not Updated
        dept2_e: "UPDATED BY BOTTON""
      }
    };

究竟是什么原因呢?

原因在于reducer函数的使用方式以及相应的dispatch函数的调用方法。
以下是同时使用reducer函数和dispatch函数的部分代码。

    // reducer関数
    const reducer = (data, newDetails) => ({ ...data, ...newDetails });

    // dispatch関数の使用
    const updateAllparam = () => {
      dispatch1({ dept1_a: "UPDATED BY BOTTON" });
      dispatch1({ dept1_b: "UPDATED BY BOTTON" });
      dispatch1({
        dept1_c: { ...data1.dept1_c, ...{ dept2_d: "UPDATED BY BOTTON" } }
      });
      dispatch1({
        dept1_c: { ...data1.dept1_c, ...{ dept2_e: "UPDATED BY BOTTON" } }
      });
    };

值得注意的是,尝试更新第四次的dept2_e处理。
dept1_c: {…data1.dept1_c, …{ dept2_e: “被按钮更新” }}。
这里我们从…data1.dept1_c获取了data1.dept1_c的值。

在进行`updateAllparam`函数调用之前,`data1.dept1_c`的值是怎样的呢?(它是否在`updateAllparam`函数中发生了变化?)

总结来说,`data1`的值是在`updateAllparam`函数调用之前(可能把它看作是React最后一次渲染时)的值,在这个时点上,它没有发生变化。

因此,在这里获取的data1.dept1_c将保持原样,而dept2_d将被重写为”dept2_d。

这个现象在官方已经有解释说明了。
https://react.dev/reference/react/useReducer#我已经派发了一个action但日志仍显示之前的状态值。

当时,我对React如何处理状态以及JS的事件循环和任务队列一无所知。因此,我当时想的是”React是否在后台执行了一些类似队列的操作,然后使用最终的值进行渲染呢?这样,我也能理解为什么在setState中需要将prev作为参数传入…”,所以我首先搜索了关于React和队列的内容。

React的渲染和队列

经过调查的结果表明,React 在其渲染过程中确实使用了队列。

(说实话,文件上明确写着Baribari Queue)

但这里还有一个因素在起作用。React 在处理状态更新之前会等待所有事件处理程序中的代码运行完毕。这就是为什么只有在所有这些 setNumber() 调用之后才会发生重新渲染的原因。

 

个人而言,我觉得这篇文章也很容易理解。

 

因此,只有等待事件处理程序内的所有代码执行完毕,才会处理状态的更新。因此,在渲染之前,似乎无法从变量中获取更新后的值。

JavaScript中的异步处理机制

在调查队列过程中,我开始怀疑JavaScript究竟以何种方式进行异步处理。

被调用的函数将被放置在堆栈中进行评估。
– 如果是同步函数,则会立即执行。
– 如果是异步函数,
1. 作为异步函数的参数传递的回调函数将被发送到 Web API 中。
2. 在 Web API 中等待的回调函数将在满足条件时添加到任务队列中。

事件循环监视着堆栈,如果堆栈为空,则从任务队列中取出。

这个视频很有参考价值。

 

这个地方看起来相当有深度的感觉…

 

要如何解决好呢?

回到本题,该如何处理reducer。
问题的根源在于,我们使用了 const reducer = (data, newDetails) => ({ …data, …newDetails }) 这样的写法,导致我们在深层次的地方要使用reducer提供的变量,但在React的机制上,却无法实现这一点。

看看公式的例子,使用了type作为参数。

    function reducer(state, action) {
      switch (action.type) {
        case 'incremented_age': {
          return {
            name: state.name,
            age: state.age + 1
          };
        }
        case 'changed_name': {
          return {
            name: action.nextName,
            age: state.age
          };
        }
      }
      throw Error('Unknown action: ' + action.type);

 

然后你需要填写代码来计算并返回下一个状态。按照惯例,通常会使用switch语句来编写。对于switch中的每个情况,计算并返回一些下一个状态。

既然如此,我们就坦率地使用switch吧!

    // action = {paramsName: hoge, payload: fuga } 

    const reducer = (data, action) => {
        if (action.paramsName === "dept1_a") {
          return { ...data, dept1_a: action.payload };
        } else if (action.paramsName === "dept1_b") {
          return { ...data, dept1_b: action.payload };
        } else if (action.paramsName === "dept2_d") {
          return { ...data, dept1_c: { ...data.dept1_c, dept2_d: action.payload } };
        } else if (action.paramsName === "dept2_e") {
          return { ...data, dept1_c: { ...data.dept1_c, dept2_e: action.payload } };
        } else {
          return data;
        }
      };
广告
将在 10 秒后关闭
bannerAds