用信号来控制异步流程

最近10条群发

(点击上方公众号,可快速关注)


作者:十年踪迹 

www.h5jun.com/post/signals-and-async.html

如有好文章投稿,请点击 → 这里了解详情

我们知道,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,可以写出原来很难写出的非常简单直观的代码:

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 库,它可以实现状态和控制流的分离,例如:

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)

有同学说,这样写代码也不简单啊,代码量比上面写法还要多。的确这样写代码量是比较多的,但是它结构清晰,耦合度低,可以很容易扩展,比如:

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 非常适合于事件控制的场合,再举一个更简单的例子,如果我们用一个按钮控制简单的动画的暂停和执行,可以这样写:

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 的详细信息。

觉得本文对你有帮助?请分享给更多人

关注「前端大全」,提升前端技能




热门问题 最新问题 推荐话题

微信扫一扫
关注该公众号