想開一個系列的文章,內容是講 JavaScript 中的異步編程。作為開篇,當然是喜聞樂見的入門向介紹文啦。這篇系列文章應該是寫給對異步編程不太了解的人員,其次為了提升我的作文水平。在讀這篇文章之前,你可能需要有這些知識預備:
- 基本的 HTML 知識
- 基本的 CSS 知識
- 了解一些 JS 語法(ES 5)
JavaScript 中的異步編程,想必大多數前端開發者都接觸過,並且體會過它帶來的苦難。我第一次接觸它的時候,搞的暈頭轉向的,就像是這個:
讓一個元素被點擊后發生漸出效果,漸出完成后讓這個元素的值自增 1,最後再漸出出來
好的……幾年前不諳 JS 的我肯定會寫成像下面這樣:
1234567891011121314151617181920212223242526272829303132333435363738// index.html<!DOCTYPE html><html><head> <meta charset="utf-8" /> <title>Async 000</title> <style> html, body { margin: 0; padding: 0; height: 100%; } body { display: flex; justify-content: center; align-items: center; } #num { display: inline; cursor: pointer; font-size: 16em; font-family: "Book Antiqua"; border: 0; background: transparent; outline: none; opacity: 1; transition: opacity 1s; } </style></head><body> <button id="num">0</button> <script src="frame.js"></script></body></html>
1234567891011121314// frame.js// 每點擊一次元素 #num 應該要先漸出// 然後自增 1 后再漸入var num_ele = document.querySelector('#num')num_ele.onclick = function () { num_ele.style.opacity = 0 var text = num_ele.textContent.trim() var num = parseInt(text) num_ele.textContent = num + 1 num_ele.style.opacity = 1}
它原本應該是點擊后隱藏……然後自增了數字 1,最後再顯示出來……但是實際情況並不是這樣[1]。這是怎麼回事?
哦,我想遇到過這種坑的人應該都知道怎麼回事了。問題的痛點在於 onclick
事件里的事情一瞬間就做完了,以至於剛漸出的特效也一下子就被後面的漸入阻止了,所以根本就看不出它有什麼動畫效果。
這種情況,我們要怎麼做?通常來說我們會使用計時器 setTimeout
。這就是異步編程。
123456789101112num_ele.onclick = function () { num_ele.style.opacity = 0 //延遲 1000 毫秒,也就延遲 1 秒后再執行 setTimeout(function () { var text = num_ele.textContent.trim() var num = parseInt(text) num_ele.textContent = num + 1 num_ele.style.opacity = 1 }, 1000)}
嗯……這個效果很好很強大了。那麼還有什麼問題嗎?細心一點的朋友已經發現了,連續點擊元素會出現很奇怪的效果。。。那麼該怎麼辦?
問題的痛點在於 onclick
事件被多次觸發導致執行了多次相同的行為(函數)……導致計時器也進行了多次的定時執行,然後表現就是……連續快速點擊多次后數字自己在跳動。
解決這種問題,可以用一個「鎖」變量來阻止多次觸發 onclick
事件:
12345678910111213141516171819202122232425262728// 鎖初始化num_ele._plusLock = falsenum_ele.onclick = function () { // 判斷鎖的值是否為 true if (num_ele._plusLock) { return } // 給鎖變量賦值 true 以阻止後面再次點擊的問題 num_ele._plusLock = true num_ele.style.opacity = 0 // 延遲 1000 毫秒,也就延遲 1 秒后再執行 setTimeout(function () { var text = num_ele.textContent.trim() var num = parseInt(text) num_ele.textContent = num + 1 num_ele.style.opacity = 1 // 這邊也要延遲了, // 要等到動畫完全播放完成后才「解鎖」 setTimeout(function () { num_ele._plusLock = false }, 1000) }, 1000)}
好的……這個點擊自增的代碼已經很完備了,不會再出現一些詭異的問題……
回到正題
那麼,通過這則案例,我們能知道什麼呢?首先……setTimeout
會造成暫停一段時間后再運行的現象……那麼這種情況呢?
12345678910function foo(value) { setTimeout(function () { value = value * value }, 500) return value}foo(9) // 9foo(-1) // -1
函數 foo()
執行后返回了原原本本的數字,setTimeout()
設置的異步語句就這麼被「略過」了。那麼,它真的是被略過了嗎?再繼續往下看:
123456789101112function fooVer2(value) { setTimeout(function () { value = value * value console.log('now, the value is ' + value) }, 500) return value}fooVer2(-4)// -4// (過了一會兒)now, the value is 16
原來並沒有略過,只是 setTimeout()
所指定的函數被暫停了一段時間后才被執行。這恰好符合了 setTimeout()
的名稱之意思——定時
由此我們得知 JavaScript 異步編程至少應該有兩個特點:
- 不會卡住後續代碼的執行(非阻塞)
- 即使函數
fooVer2()
執行了return
語句后,這個函數沒有被「關閉」[2]
以上。