我們知道,JavaScript 不管是操作 DOM,還是執行服務端任務,不可避免需要處理許多異步調用。在早期,許多開發者僅僅通過 JavaScript 的回調方式來處理異步,但是那樣很容易造成異步回調的嵌套,產生 “Callback Hell”。
後來,一些開發者使用了 Promise 思想來避免異步回調的嵌套,社區將根據思想提出 Promise/A+ 規範,最終,在 ES6 中內置實現了 Promise 類,隨後又基於 Promise 類在 ES2017 裏實現了 async/await,形成了現在非常簡潔的異步處理方式。
比如 thinkJS 下面這段代碼就是典型的 async/await 用法,它看起來和同步的寫法完全一樣,只是增加了 async/await 關鍵字。
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 可以算是一種語法糖,它將
promise.then(res => {
do sth.
}).catch(err => {
some error
})
複製代碼
轉換成了
try{
res = await promise
do sth
}catch(err){
some error
}
複製代碼
有了 async,await,可以寫出原來很難寫出的非常簡單直觀的代碼: JS Bin 查看效果
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 其實也經常會有這樣麻煩的需求。
我們試着來實現一下:
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)登場了!看下面的例子:
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 庫,它可以實現狀態和控制流的分離,例如: JS Bin 查看效果
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)
複製代碼
有同學説,這樣寫代碼也不簡單啊,代碼量比上面寫法還要多。的確這樣寫代碼量是比較多的,但是它結構清晰,耦合度低,可以很容易擴展,比如: JS Bin 查看效果
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 非常適合於事件控制的場合,再舉一個更簡單的例子,如果我們用一個按鈕控制簡單的動畫的暫停和執行,可以這樣寫: JS Bin 查看效果
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 的詳細信息。
-- EOF --