谈一谈几种处理JavaScript异步操做的办法

引言

js的异步操做,已是一个老生常谈的话题,关于这个话题的文章随便google一下均可以看到一大堆。那么为何我还要写这篇东西呢?在最近的工做中,为了编写一套相对比较复杂的插件,须要处理各类各样的异步操做。可是为了体积和兼容性,不打算引入任何的pollyfill,甚至连babel也不容许使用,这也意味着只能以es5的方式去处理。使用回调的方式对于解耦很是不利,因而找了别的方法去处理这个问题。问题是处理完了,却也引起了本身的一些思考:处理js的异步操做,都有一些什么方法呢?es6

1、回调函数

传说中的“callback hell”就是来自回调函数。而回调函数也是最基础最经常使用的处理js异步操做的办法。咱们来看一个简单的例子:设计模式

首先定义三个函数:数组

function fn1 () {
  console.log('Function 1')
}

function fn2 () {
  setTimeout(() => {
    console.log('Function 2')
  }, 500)
}

function fn3 () {
  console.log('Function 3')
}

其中fn2能够视做一个延迟了500毫秒执行的异步函数。如今我但愿能够依次执行fn1fn2fn3。为了保证fn3在最后执行,咱们能够把它做为fn2的回调函数:浏览器

function fn2 (f) {
  setTimeout(() => {
    console.log('Function 2')
    f()
  }, 500)
}

fn2(fn3)

能够看到,fn2fn3彻底耦合在一块儿,若是有多个相似的函数,颇有可能会出现fn1(fn2(fn3(fn4(...))))这样的状况。回调地狱的坏处我就不赘述了,相信各位读者必定有本身的体会。babel

2、事件发布/订阅

发布/订阅模式也是诸多设计模式当中的一种,刚好这种方式能够在es5下至关优雅地处理异步操做。什么是发布/订阅呢?以上一节的例子来讲,fn1fn2fn3均可以视做一个事件的发布者,只要执行它,就会发布一个事件。这个时候,咱们能够经过一个事件的订阅者去批量订阅并处理这些事件,包括它们的前后顺序。下面咱们基于上一章节的例子,增长一个消息订阅者的方法(为了简单起见,代码使用了es6的写法):异步

class AsyncFunArr {
  constructor (...arr) {
    this.funcArr = [...arr]
  }

  next () {
    const fn = this.funcArr.shift()
    if (typeof fn === 'function') fn()
  }

  run () {
    this.next()
  }
}

const asyncFunArr = new AsyncFunArr(fn1, fn2, fn3)

而后在fn1fn2fn3内调用其next()方法:async

function fn1 () {
  console.log('Function 1')
  asyncFunArr.next()
}

function fn2 () {
  setTimeout(() => {
    console.log('Function 2')
    asyncFunArr.next()
  }, 500)
}

function fn3 () {
  console.log('Function 3')
  asyncFunArr.next()
}

// output =>
// Function 1
// Function 2
// Function 3

能够看到,函数的处理顺序等于传入AsyncFunArr的参数顺序。AsyncFunArr在内部维护一个数组,每一次调用next()方法都会按顺序推出数组所保存的一个对象并执行,这也是我在实际的工做中比较经常使用的方法。函数

3、Promise

使用Promise的方式,就不须要额外地编写一个消息订阅者函数了,只须要异步函数返回一个Promise便可。且看例子:学习

function fn1 () {
  console.log('Function 1')
}

function fn2 () {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('Function 2')
      resolve()
    }, 500)
  })
}

function fn3 () {
  console.log('Function 3')
}

一样的,咱们定义了三个函数,其中fn2是一个返回Promise的异步函数,如今咱们但愿按顺序执行它们,只须要按如下方式便可:this

fn1()
fn2().then(() => { fn3() })

// output =>
// Function 1
// Function 2
// Function 3

使用Promise与回调有两个最大的不一样,第一个是fn2fn3得以解耦;第二是把函数嵌套改成了链式调用,不管从语义仍是写法上都对开发者更加友好。

4、generator

若是说Promise的使用可以化回调为链式,那么generator的办法则能够消灭那一大堆的Promise特征方法,好比一大堆的then()

function fn1 () {
  console.log('Function 1')
}

function fn2 () {
  setTimeout(() => {
    console.log('Function 2')
    af.next()
  }, 500)
}

function fn3 () {
  console.log('Function 3')
}

function* asyncFunArr (...fn) {
  fn[0]()
  yield fn[1]()
  fn[2]()
}

const af = asyncFunArr(fn1, fn2, fn3)

af.next()

// output =>
// Function 1
// Function 2
// Function 3

在这个例子中,generator函数asyncFunArr()接受一个待执行函数列表fn,异步函数将会经过yield来执行。在异步函数内,经过af.next()激活generator函数的下一步操做。

这么粗略的看起来,其实和发布/订阅模式很是类似,都是经过在异步函数内部主动调用方法,告诉订阅者去执行下一步操做。可是这种方式仍是不够优雅,好比说若是有多个异步函数,那么这个generator函数确定得改写,并且在语义化的程度来讲也有一点不太直观。

5、优雅的async/await

使用最新版本的Node已经能够原生支持async/await写法了,经过各类pollyfill也能在旧的浏览器使用。那么为何说async/await方法是最优雅的呢?且看代码:

function fn1 () {
  console.log('Function 1')
}

function fn2 () {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('Function 2')
      resolve()
    }, 500)
  })
}

function fn3 () {
  console.log('Function 3')
}

async function asyncFunArr () {
  fn1()
  await fn2()
  fn3()
}

asyncFunArr()

// output =>
// Function 1
// Function 2
// Function 3

有没有发现,在定义异步函数fn2的时候,其内容和前文使用Promise的时候如出一辙?再看执行函数asyncFunArr(),其执行的方式和使用generator的时候也很是相似。

异步的操做都返回Promise,须要顺序执行时只须要await相应的函数便可,这种方式在语义化方面很是友好,对于代码的维护也很简单——只须要返回Promise并await它就好,无需像generator那般须要本身去维护内部yield的执行。

6、尾声

做为一个概括和总结,本文内容的诸多知识点也是来自于他人的经验,不过加入了一些本身的理解和体会。不求科普于他人,但求做为我的的积累。但愿读者能够提出订正的意见,共同窗习进步!

相关文章
相关标签/搜索