我們知道,JavaScript 不管是操作 DOM,還是執行服務端任務,不可避免需要處理許多非同步呼叫。在早期,許多開發者僅僅通過 JavaScript 的回撥方式來處理非同步,但是那樣很容易造成非同步回撥的巢狀,產生 “Callback Hell”。
後來,一些開發者使用了 Promise 思想來避免非同步回撥的巢狀,社群將根據思想提出 Promise/A+ 規範,最終,在 ES6 中內建實現了 Promise 類,隨後又基於 Promise 類在 ES2017 裡實現了 async/await,形成了現在非常簡潔的非同步處理方式。
比如 thinkJS 下面這段程式碼就是典型的 async/await 用法,它看起來和同步的寫法完全一樣,只是增加了 async/await 關鍵字。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
module.exports = class extends think.Controller { async indexAction(){ let model = this.model('user'); try{ await model.startTrans(); let userId = await model.add({name: 'xxx'}); let insertId = await this.model('user_group').add({user_id: userId, group_id: 1000}); await model.commit(); }catch(e){ await model.rollback(); } } } |
async/await 可以算是一種語法糖,它將
1 2 3 4 5 |
promise.then(res => { do sth. }).catch(err => { some error }) |
轉換成了
1 2 3 4 5 6 |
try{ res = await promise do sth }catch(err){ some error } |
有了 async,await,可以寫出原來很難寫出的非常簡單直觀的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function idle(time){ return new Promise(resolve=>setTimeout(resolve, time)) } (async function(){ //noprotect do { traffic.className = 'stop' await idle(1000) traffic.className = 'pass' await idle(1500) traffic.className = 'wait' await idle(500) }while(1) })() |
上面的程式碼中,我們利用非同步的 setTimeout 實現了一個 idle 的非同步方法,返回 promise。許多非同步處理過程都能讓它們返回 promise,從而產生更簡單直觀的程式碼。
網頁中的 JavaScript 還有一個問題,就是我們要響應很多非同步事件,表示使用者操作的非同步事件其實不太好改寫成 promise,事件代表控制,它和資料與流程往往是兩個層面的事情,所以許多現代框架和庫通過繫結機制把這一塊封裝起來,讓開發者能夠聚焦於運算元據和狀態,從而避免增加系統的複雜度。
比如上面那個“交通燈”,這樣寫已經是很簡單,但是如果我們要增加幾個“開關”,表示“暫停/繼續“和”開啟/關閉”,要怎麼做呢?如果我們還想要增加開關,人工控制和切換燈的轉換,又該怎麼實現呢?
有同學想到這裡,可能覺得,哎呀這太麻煩了,用 async/await 搞不定,還是用之前傳統的方式去實現吧。
其實即使用“傳統”的思路,要實現這樣的非同步狀態控制也還是挺麻煩的,但是我們的 PM 其實也經常會有這樣麻煩的需求。
我們試著來實現一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
function defer(){ let deferred = {}; deferred.promise = new Promise((resolve, reject) => { deferred.resolve = resolve deferred.reject = reject }) return deferred } class Idle { wait(time){ this.deferred = new defer() this.timer = setTimeout(()=>{ this.deferred.resolve({canceled: false}) }, time) return this.deferred.promise } cancel(){ clearTimeout(this.timer) this.deferred.resolve({canceled: true}) } } const idleCtrl = new Idle() async function turnOnTraffic(){ let state; //noprotect do { traffic.className = 'stop' state = await idleCtrl.wait(1000) if(state.canceled) break traffic.className = 'pass' state = await idleCtrl.wait(1500) if(state.canceled) break traffic.className = 'wait' state = await idleCtrl.wait(500) if(state.canceled) break }while(1) traffic.className = '' } turnOnTraffic() onoffButton.onclick = function(){ if(traffic.className === ''){ turnOnTraffic() onoffButton.innerHTML = '關閉' } else { onoffButton.innerHTML = '開啟' idleCtrl.cancel() } } |
上面這麼做實現了控制交通燈的開啟關閉。但是實際上這樣的程式碼讓 onoffButton、 idelCtrl 和 traffic 各種耦合,有點慘不忍睹……
這還只是最簡單的“開啟/關閉”,“暫停/繼續”要比這個更復雜,還有使用者自己控制燈的切換呢,想想都頭大!
在這種情況下,因為我們把控制和狀態混合在一起,所以程式邏輯不可避免地複雜了。這種複雜度與 callback 和 async/await 無關。async/await 只能改變程式的結構,並不能改變內在邏輯的複雜性。
那麼我們該怎麼做呢?這裡我們就要換一種思路,讓訊號(Signal)登場了!看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
class Idle extends Signal { async wait(time){ this.state = 'wait' const timer = setTimeout(() => { this.state = 'timeout' }, time) await this.while('wait') clearTimeout(timer) } cancel(){ this.state = 'cancel' } } class TrafficSignal extends Signal { constructor(id){ super('off') this.container = document.getElementById(id) this.idle = new Idle() } get lightStat(){ return this.state } async pushStat(val, dur = 0){ this.container.className = val this.state = val await this.idle.wait(dur) } get canceled(){ return this.idle.state === 'cancel' } cancel(){ this.pushStat('off') this.idle.cancel() } } const trafficSignal = new TrafficSignal('traffic') async function turnOnTraffic(){ //noprotect do { await trafficSignal.pushStat('stop', 1000) if(trafficSignal.canceled) break await trafficSignal.pushStat('pass', 1500) if(trafficSignal.canceled) break await trafficSignal.pushStat('wait', 500) if(trafficSignal.canceled) break }while(1) trafficSignal.lightStat = 'off' } turnOnTraffic() onoffButton.onclick = function(){ if(trafficSignal.lightStat === 'off'){ turnOnTraffic() onoffButton.innerHTML = '關閉' } else { onoffButton.innerHTML = '開啟' trafficSignal.cancel() } } |
我們對程式碼進行一些修改,封裝一個 TrafficSignal,讓 onoffButton 只控制 traficSignal 的狀態。這裡我們用一個簡單的 Signal 庫,它可以實現狀態和控制流的分離,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
const signal = new Signal('default') ;(async () => { await signal.while('default') console.log('leave default state') })() ;(async () => { await signal.until('state1') console.log('to state1') })() ;(async () => { await signal.until('state2') console.log('to state2') })() ;(async () => { await signal.until('state3') console.log('to state3') })() setTimeout(() => { signal.state = 'state0' }, 1000) setTimeout(() => { signal.state = 'state1' }, 2000) setTimeout(() => { signal.state = 'state2' }, 3000) setTimeout(() => { signal.state = 'state3' }, 4000) |
有同學說,這樣寫程式碼也不簡單啊,程式碼量比上面寫法還要多。的確這樣寫程式碼量是比較多的,但是它結構清晰,耦合度低,可以很容易擴充套件,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
class Idle extends Signal { async wait(time){ this.state = 'wait' const timer = setTimeout(() => { this.state = 'timeout' }, time) await this.while('wait') clearTimeout(timer) } cancel(){ this.state = 'cancel' } } class TrafficSignal extends Signal { constructor(id){ super('off') this.container = document.getElementById(id) this.idle = new Idle() } get lightStat(){ return this.state } async pushStat(val, dur = 0){ this.container.className = val this.state = val if(dur) await this.idle.wait(dur) } get canceled(){ return this.idle.state === 'cancel' } cancel(){ this.idle.cancel() this.pushStat('off') } } const trafficSignal = new TrafficSignal('traffic') async function turnOnTraffic(){ //noprotect do { await trafficSignal.pushStat('stop', 1000) if(trafficSignal.canceled) break await trafficSignal.pushStat('pass', 1500) if(trafficSignal.canceled) break await trafficSignal.pushStat('wait', 500) if(trafficSignal.canceled) break }while(1) trafficSignal.lightStat = 'off' } turnOnTraffic() onoffButton.onclick = function(){ if(trafficSignal.lightStat === 'off'){ turnOnTraffic() onoffButton.innerHTML = '關閉' } else { onoffButton.innerHTML = '開啟' trafficSignal.cancel() } } turnRed.onclick = function(){ trafficSignal.cancel() trafficSignal.pushStat('stop') } turnGreen.onclick = function(){ trafficSignal.cancel() trafficSignal.pushStat('pass') } turnYellow.onclick = function(){ trafficSignal.cancel() trafficSignal.pushStat('wait') } |
Signal 非常適合於事件控制的場合,再舉一個更簡單的例子,如果我們用一個按鈕控制簡單的動畫的暫停和執行,可以這樣寫:
1 2 3 4 5 6 7 8 9 10 11 |
let traffic = new Signal('stop') requestAnimationFrame(async function update(t){ await traffic.until('pass') block.style.left = parseInt(block.style.left || 50) + 1 + 'px' requestAnimationFrame(update) }) button.onclick = e => { traffic.state = button.className = button.className === 'stop' ? 'pass' : 'stop' } |
總結
我們可以用 Signal 來控制非同步流程,它最大的作用是將狀態和控制分離,我們只需要改變 Signal 的狀態,就可以控制非同步流程,Signal 支援 until 和 while 謂詞,來控制狀態的改變。
可以在 GitHub repo 上進一步瞭解關於 Signal 的詳細資訊。