当我研究如何使用 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;
}
};