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計算機學生)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章