React 源码分析 - setState() 之后没有重新渲染的问题原因与解决方法

问题背景

假设我们正在开发一个让用户填写的表单,比如说登录页面的账号密码表单。

return (
	<form>
	  <p>账户:</p>
	  <input type="text" name="firstname"><br>
	  <p>密码</p>
	  <input type="password" name="lastname">
	  <p class="error"> { errorState } </p>
	</form>
);

从中可以看到,我们有一块区域是专门用来展示错误信息的。errorState 是我们在 React 中用来储存错误信息的状态 (state)。 当用户填写的账号密码不正确时,我们希望在这个区域展示用户名或密码错误的信息。

我们可以在 React 中使用状态来储存我们的错误信息,一旦服务器返回错误信息,我们就将该错误信息存进这个状态的 数组 (array) 当中。

这里我们使用 react hook 作为例子。

// 初始一个空数组,用来储存错误信息
const [errorState, setErrorState] = useState([]);

当我们收到从服务器返回的错误时,我们希望将该错误信息添加进我们的状态之中。

// 变量 newMessage 储存着新返回的错误信息
setErrorState(prevErrorState => {
	const errorMessages = prevErrorState;
	errorMessages.push(newMessage);
	return errorMessages;
});

从上面的代码可以看出,我们先是将现有的错误信息数组取出,将新的错误信息作为新的元素添加进数组中,再将该数组传回我们的状态中。

发生问题

然而,当我们测试的时候却发现,即使这段代码运行成功,React 也不会重新渲染页面,导致新的错误信息不会被渲染出来。

问题分析

查看源代码是一个很好的学习方法,这不只可以让我们看见问题的根源,也能帮助我们提升阅读代码的能力。

让我们来看看 React 的源代码,瞧瞧为什么即使我们调用了设置状态的方法 (setErrorState) ,React 却不重新渲染呢?

如果我们在调用 setErrorState 这一行设置断点,并一行一行前进的话,我们会看到 React 有下面这段代码:

// _eagerState: 我们传入的新状态,这里是我们传入的错误信息数组
// currentState: 当前状态,也就是还没改变的旧状态
if (is(_eagerState, currentState)) {
  return;
}

从这里我们可以看出,React 会通过比较之前的状态与我们新传入的状态,来决定是否要将重新渲染的任务加入到 scheduler 里面。在这里,如果旧状态与新状态相等,React将直接返回,并不会 schedule 任何重新渲染的任务。

我们再来详细看看 is() 方法的细节:

function is(x, y) {
  return x === y && (x !== 0 || 1 / x === 1 / y) || x !== x && y !== y;
}

虽然这段代码看起来很混乱,但不必紧张。我们只需要看第一部分的代码:

return x === y

我们就可以知道这段代码的核心思想是什么,而不必去纠结于后面部分的细节,因为它们基本上与我们目前遇到的问题无关。这也是阅读代码的一个技巧

从这里就可以知道为什么 React 不会重新渲染我们的错误信息了。虽然我们将新的错误信息作为新的元素插入进旧的数组里,但在这段代码中,React 并不会检查数组里的元素是否有改变,它只会检查数组本身的引用是否有改变。

因此,从 React 的眼中来看,数组的引用依然一样 (引用即代表内存位置),那就表示状态没有更新,因此不会重新渲染我们的错误信息。

解决问题

要解决这个问题,我们可以克隆一个新的数组,再将我们的新元素插入进去,然后再使用新的克隆数组来设置我们的错误信息状态。

新的克隆数组是一个全新的数组,在内存中占有自己的一席之地,因此也拥有着属于自己的引用,因此在 React 的眼中看来,该数组与之前的数组完全不同,因此会安排重新渲染的任务。

// [...errorState] 为 JavaScript 里的语法糖,会克隆并返回一个装有相同元素的新数组
setErrorState(prevErrorState => {
	const errorMessages = [...prevErrorState];
	errorMessages.push(newMessage);
	return errorMessages;
});

将代码改成如上所述一样,React 就会重新渲染我们的错误信息状态啦!

作者仍在学习中, 如果有什么错误,请各位指出并包含,谢谢!

作者:David Chou(温哥华SFU计算机学生)

发布了14 篇原创文章 · 获赞 8 · 访问量 2210

猜你喜欢

转载自blog.csdn.net/vandavidchou/article/details/102618866