越是簡單的東西,有時候越是我們時常忽略需要注意的部分,這篇文章將會介紹setInterval與setTimeout一些小觀念卻重要的部分。

基本介紹

setTimeout與setInterval都是始於web api提供的方法,可以透過window來查看。

  1. setTimeout:只會執行一次
  2. setInterval:輪詢執行多次

常見的使用範例

如果將setTimeout放到迴圈內會發生什麼事情:

1
2
3
4
5
for(let i=0; i<10; i++){
setTimeout(function(){
console.log(i);
},100);
}

你猜到答案了嗎?輸出的i全部都會是10。

如果是這樣呢?

1
2
3
4
5
6
7
for(let i=0; i<10; i++){
(function(i){
setTimeout(function(){
console.log(i);
},100);
})(i);
}

因為把setTimeout用function包起來,因此可以確保每次呼叫都是使用不同的i。

關於setInterval

在開發前端的時候,有時候為了做到輪詢的效果,會使用setInterval這個方法。
讓我們先來看看這段程式:

1
2
3
4

setInterval(function() {
console.log('hey');
}, 1000);

你覺得這段程式在做什麼事情,每隔1秒log一次嗎?
事實上這裡的1000指的是對程式執行而言的1秒。

這很奇怪嗎?事實上很正常,因為這1秒包含了程式執行的時間。
要做到真正的delay效果,可以搭配使用setTimeout。

1
2
3
4

setInterval(function() {
setTimeout(()=>console.log('hey'), 1000);
}, 1000);

setTimeout為0的小技巧

可能很多人看過網路上一些關於setTimeout為0的陷阱題:

1
2
3
4
5
let test = function() {
console.log('a')
setTimeout(() => console.log('b'), 0)
console.log('c')
}

event loop執行流程

  1. function test放進call stack
    callstack: [ test() ]

  2. 輸出a
    callstack: [ test() , log a ]

  3. 執行setTimeout
    callstack: [ test() , setTimeout ]
    web api: []

  4. 拿到web api,丟入web api內執行等待時間
    callstack: [ test() ]
    web api: [ setTimeout 0 sec ]

  5. 輸出c
    callstack: [ test(), log c ]
    web api: [ setTimeout 0 sec ]

  6. test()執行完畢
    callstack: []
    web api: [ setTimeout 0 sec ]
    event queue: []

  7. 將執行完的web api丟到event loop
    callstack: []
    web api: []
    event queue: [ callback-setTimeout() ]

  8. callback丟回callstack, 執行callback
    callstack: [ callback-setTimeout() ]
    web api: []
    event queue: []

  9. 執行callback內的log
    callstack: [ callback-setTimeout(), log ]
    web api: []
    event queue: []

小技巧範例

如果今天我有一個程式希望click之後跳到已經render出來的DOM

1
2
3
4
const onClick = () => {
this.onRenderCardList(); // 或是改變state/props來rerender
this.ref.current.scrollIntoView({ behavior: 'smooth' });
}

這個時候你會發現,寫的順序明明就是先畫畫面再scroll到目標的element ref呀?為什麼沒反應
因為如果你現在執行scroll的話,事實上是未rerender時的VDOM,那要怎麼做呢?
可以搭配使用web api做callback,callback開始執行的時候,就會是rerender過後的樣子了。

1
2
3
4
const onClick = () => {
this.onRenderCardList(); // 或是改變state/props來rerender
setTimeout(() => this.ref.current.scrollIntoView({ behavior: 'smooth' }, 0);
})

setInterval需要注意的部分

剛剛上面有提到setInterval如果單純直接使用,時間間隔會不如預期。
假設今天在React上有這個程式在useEffect,可以怎麼優化呢?
有興趣的話可以玩玩看這個範例:

1
2
3
4
5
6
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
console.log(count);
}, 1000);
}, []);

來設定個setTimeout確保1秒後執行callback

1
2
3
4
5
6
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setTimeout(() => console.log(count), 1000);
}, 1000);
}, []);

再還要更進階探討其他部分了,如果今天畫面中有按鈕可以改變count,那我的程式可疼會改成:

1
2
3
4
5
6
7
8
9
10
11
12
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setTimeout(() => console.log(count), 1000);
}, 1000);
}, [count]);

return (
<div className="App">
<button onClick={() => setCount(count + 1)}>click</button>
</div>
);

當你點擊之後…你發現什麼問題了嗎?哎呀… 竟然console.log出0和1了呀……
而且還一直每隔1秒各跑出來,這就是陷阱啦~
請注意,setInterval不像setTimeout一樣會執行程式完之後進行garbage collection。
因此你要記得將舊的邏輯clear掉,不然可能會發生多工的狀況。

1
2
3
4
5
6
7
8
9
10
11
12
13
const [count, setCount] = useState(0);
useEffect(() => {
clearInterval(this.timmer);
this.timmer = setInterval(() => {
setTimeout(() => console.log(count), 1000);
}, 1000);
}, [count]);

return (
<div className="App">
<button onClick={() => setCount(count + 1)}>click</button>
</div>
);

改成這樣就能正常的每隔1秒後輸出count了。

Reference

javascript.info