與伊神哥在討論其中一段程式碼時,發現某一段程式碼在 setTimeout()
裡面執行結果如預期,但搬出 setTimeout()
之後卻會有問題。
1. 以下僅為示意程式碼,並非案例當事人。
2. 其實 setTimeout()
在原本的案例是 Promise Callback,為了解釋則替換為 setTimeout()
。
// Example 1
// 原先執行結果如預期的程式碼
class Component extends React.Component {
constructor() {
this.data = [];
}
render() {
return (
<section>
{JSON.stringify(this.state.data)}
</section>
);
}
componentDidMount() {
setTimeout(() => {
this.setState({
data: [...this.state.data, 1]
});
this.setState({
data: [...this.state.data, 2]
});
this.setState({
data: [...this.state.data, 3]
});
}, 0);
}
}
// Latest Output:
// [1, 2, 3]
不過伊神哥因為某些原因、某些理由,決定把那三組 this.setState()
從 setTimeout()
裡面搬出來,導致了輸出結果不正常,如下所示:
// Example 2
// 執行結果不如預期的程式碼
class Component extends React.Component {
constructor() {
this.data = [];
}
render() {
return (
<section>
{JSON.stringify(this.state.data)}
</section>
);
}
componentDidMount() {
this.setState({
data: [...this.state.data, 1]
});
this.setState({
data: [...this.state.data, 2]
});
this.setState({
data: [...this.state.data, 3]
});
}
}
// Latest Output:
// [3]
很明顯可以發現,最後的結果僅留下了第三次執行 setState()
的結果。
於是開始思考,為什麼會這樣呢?為什麼呢為什麼呢為什麼呢?
分為兩件事來探討
setState()
的行為setTimeout()
的作用
引用 官方文件 的解釋:
setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value. There is no guarantee of synchronous operation of calls to setState and calls may be batched for performance gains.
也就是官方不保證 setState()
的行為一定是同步執行的,因此希望開發者一律將他視為非同步執行 (以結論來說,我們也應該這麼做)。
- 發生情境:當
setState()
被呼叫的位置,在 React Component 能掌管的範圍內。 setState()
的位置在 React Component 的 Life Cycle 或是其延伸的 Event Handlers 當中。- 也就是官方預期的執行情境
- Demo
- 發生情境:當
setState()
被呼叫的位置,不在 React Component 能掌管的範圍內。 setState()
已經脫離了 React Component 的 Life Cycle 或是其延伸的 Event Handlers- 例如:在
setTimeout()
、addEventListener()
的 Callback 裡面呼叫setState()
- Demo
為什麼會有上述兩種差異,主要的原因就是 React 希望降低 Re-render 的次數,因此在 React 能夠掌控的範圍內 (如 onClick 等 SyntheticEvent),會打包事件裡所有的操作之後再一併進行更新。
這在 React 的運作機制裡叫做 Update Batching
。
#Ref1、#Ref2
- Q、有時候我們會看到某些 JavaScript Developer 會使用
setTimeout(Callback, 0)
來呼叫一組函式,那背後的原因是什麼呢? - A、用兩個方向來解釋
- 以目的來說:跳脫現有的 Synchronous Call,讓 Callback 成為一個 Asynchronous Call
- 以原理來說:因為 JavaScript 是使用單執行緒逐行地執行指令,使用
setTimeout(Callback, 0)
(不管 delay 是否為 0) 會讓這個 Callback 被排在此次 Synchronous Call 執行完畢之後,Callback 才會被執行到。間接達成 Callback 成為 Asynchronous Call 的目的。 - 而事實上
setTimeout
設定為 0 毫秒,並不會真的就在 0 毫秒之後執行。#Ref
- Why are some JavaScript developers using setTimeout for one millisecond? - Stack Overflow
- Effect of Setting setTimeout to 0 and Explanation of JavaScript's Single Threaded, Non Blocking I/O, Async Event Driven Model
總結來說,在一開始的兩個範例當中:
-
範例二的 3 個
setState()
在componentDidMount()
之中,所以對setState()
來說,會因為 Update Batching 機制,在函式的最後被打包在一起執行,也就是當setState()
真正被執行的期間,this.state
是完全沒有被更新的狀態(因為setState()
原本就是一個 Asynchronous Call),也就是為什麼最後的 State 會只有第三次的執行結果。 -
範例一的 3 個
setState()
在setTimeout()
之中,已經成為了 Asynchronous Call,所以對setState()
來說,就脫離了 React Component 掌控的範圍,因此setState()
會變成 Synchronous Call,所以三次的 Execution 都可以順利抓到上一個 Execution 更新的結果。