异步解决方案的进阶历史

747 阅读12分钟

我们可以先看下异步解决方案的进阶历史:

回调函数 ---> 事件监听 ---> 发布/订阅 ---> Promise ---> Generator ---> async/await

我们按顺序看下这些解决方案的优缺点,进阶的必要性。着重说一下 Promise Generator async/await

回调函数

优点:

简单

缺点:

回调多的情况下容易产生回调地狱

示例

// normal
fs.readFile(xxx, 'utf-8', function(err, data) {
    // code
});

// callback hell
fs.readFile(A, 'utf-8', function(err, data) {
    fs.readFile(B, 'utf-8', function(err, data) {
        fs.readFile(C, 'utf-8', function(err, data) {
            fs.readFile(D, 'utf-8', function(err, data) {
                //....
            });
        });
    });
});

事件监听

优点

可以绑定多个事件,每个事件可以指定多个回调函数

缺点

整个流程都要变成事件驱动型,运行流程会变得不清晰

示例

f1.on('done', f2);
function f1(){
    setTimeout(function () {
        // f1的任务代码
        f1.trigger('done');
    }, 1000);
}

发布/订阅

优点

我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行

缺点

写法依然不直观

示例

jQuery.subscribe("done", f2);
function f1(){
    setTimeout(function () {
    // f1的任务代码
        jQuery.publish("done");
    }, 1000);
}
jQuery.unsubscribe("done", f2);

Promise

Promise 最早由社区提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象。

特点

  • 对象的状态不受外界影响。 Promise 对象代表一个异步操作,有三种状态: pendingfulfilledrejected 。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected

优点

  • 可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数
  • Promise 对象提供统一的接口,使得控制异步操作更加容易

缺点

  • 无法取消 Promise
  • 当处于 pending 状态时,无法得知目前进展到哪一个阶段(待详解)

示例

function read(url) {
    return new Promise((resolve, reject) => {
        fs.readFile(url, 'utf8', (err, data) => {
            if(err) reject(err);
            resolve(data);
        });
    });
}
read(A).then(data => {
    return read(B);
}).then(data => {
    return read(C);
}).then(data => {
    return read(D);
}).catch(reason => {
    console.log(reason);
});

无疑,Promise 的实现对于异步处理来说是一个巨大的进步,那么其原理是什么呢? 我们可以先思考一下如果自己实现一个 Promise 的对象,应该考虑哪些方面:

  • 状态变更,PENDING / RESLOVED / REJECTED
  • 参数是一个立即执行函数,它接收 resolvereject 两个回调方法
  • then 的链式调用及状态问题
  • 存储成功和失败回调函数列表
  • ...

原理

我们可以手动实现一下:

// 判断变量否为function
const isFunction = variable => typeof variable === 'function'
// 定义Promise的三种状态常量
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'

export class MyPromise {
  constructor(handle) {
    // 非函数直接报错
    if (!isFunction(handle)) {
      throw new Error('MyPromise must accept a function as a parameter')
    }
    // 添加状态
    this._status = PENDING
    // 接收数据变量
    this._value = undefined
    // 添加成功回调函数队列
    this._fulfilledQueues = []
    // 添加失败回调函数队列
    this._rejectedQueues = []
    // 执行handle
    try {
      handle(this._resolve.bind(this), this._reject.bind(this))
    } catch (err) {
      this._reject(err)
    }
  }
  // 添加resovle时执行的函数
  _resolve(val) {
    console.log('MyPromise -> _resolve -> _resolve', val)
    const run = () => {
      // 改变状态之后不可变,所以不再执行
      if (this._status !== PENDING) return
      // 依次执行成功队列中的函数,并清空队列
      const runFulfilled = value => {
        console.log('MyPromise -> runFulfilled -> runFulfilled', value)
        let cb
        while ((cb = this._fulfilledQueues.shift())) {
          cb(value)
        }
      }
      // 依次执行失败队列中的函数,并清空队列
      const runRejected = error => {
        console.log('MyPromise -> runRejected -> runRejected', error)
        let cb
        while ((cb = this._rejectedQueues.shift())) {
          cb(error)
        }
      }
      /* 如果resolve的参数为Promise对象,则必须等待该Promise对象状态改变后,
        当前Promsie的状态才会改变,且状态取决于参数Promsie对象的状态
      */
      if (val instanceof MyPromise) {
        console.log('MyPromise -> run -> MyPromise', ' val is MyPromise')
        val.then(
          value => {
            this._value = value
            this._status = FULFILLED
            runFulfilled(value)
          },
          err => {
            this._value = err
            this._status = REJECTED
            runRejected(err)
          }
        )
      } else {
        console.log('MyPromise -> run -> MyPromise', ' val is not MyPromise')
        this._value = val
        this._status = FULFILLED
        runFulfilled(val)
      }
    }
    // 为了支持同步的Promise,这里采用异步调用
    setTimeout(run, 0)
  }
  // 添加reject时执行的函数
  _reject(err) {
    console.log('MyPromise -> _reject -> _reject', err)
    if (this._status !== PENDING) return
    // 依次执行失败队列中的函数,并清空队列
    const run = () => {
      this._status = REJECTED
      this._value = err
      let cb
      while ((cb = this._rejectedQueues.shift())) {
        cb(err)
      }
    }
    // 为了支持同步的Promise,这里采用异步调用
    setTimeout(run, 0)
  }
  // 添加then方法
  then(onFulfilled, onRejected) {
    console.log('MyPromise -> then -> onFulfilled', onFulfilled)
    console.log('MyPromise -> then -> onRejected', onRejected)
    const { _value, _status } = this
    // 返回一个新的Promise对象
    return new MyPromise((onFulfilledNext, onRejectedNext) => {
      console.log('MyPromise -> then -> onFulfilledNext', onFulfilledNext)
      console.log('MyPromise -> then -> onRejectedNext', onRejectedNext)
      // 封装一个成功时执行的函数
      let fulfilled = value => {
        try {
          if (!isFunction(onFulfilled)) {
            onFulfilledNext(value)
          } else {
            let res = onFulfilled(value)
            if (res instanceof MyPromise) {
              // 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
              res.then(onFulfilledNext, onRejectedNext)
            } else {
              //否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
              onFulfilledNext(res)
            }
          }
        } catch (err) {
          // 如果函数执行出错,新的Promise对象的状态为失败
          onRejectedNext(err)
        }
      }
      // 封装一个失败时执行的函数
      let rejected = error => {
        try {
          if (!isFunction(onRejected)) {
            onRejectedNext(error)
          } else {
            let res = onRejected(error)
            if (res instanceof MyPromise) {
              // 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
              res.then(onFulfilledNext, onRejectedNext)
            } else {
              //否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
              onFulfilledNext(res)
            }
          }
        } catch (err) {
          // 如果函数执行出错,新的Promise对象的状态为失败
          onRejectedNext(err)
        }
      }
      switch (_status) {
        // 当状态为pending时,将then方法回调函数加入执行队列等待执行
        case PENDING:
          this._fulfilledQueues.push(fulfilled)
          console.log('MyPromise -> then -> _fulfilledQueues', this._fulfilledQueues)
          this._rejectedQueues.push(rejected)
          console.log('MyPromise -> then -> _rejectedQueues', this._rejectedQueues)
          break
        // 当状态已经改变时,立即执行对应的回调函数
        case FULFILLED:
          fulfilled(_value)
          break
        case REJECTED:
          rejected(_value)
          break
      }
    })
  }
  // 添加catch方法
  catch(onRejected) {
    console.log('MyPromise -> catch')
    return this.then(undefined, onRejected)
  }
  
  // 无论状态,最后执行方法
  finally(cb) {
    console.log('MyPromise -> finally -> finally')
    return this.then(
      value => MyPromise.resolve(cb()).then(() => value),
      reason =>
        MyPromise.resolve(cb()).then(() => {
          throw reason
        })
    )
  }
}

那么 Promise 有没有改变 callback 的本质?并没有,Promise 只是换了种对异步的写法,优化了对代码的可读性,从实现来看还是依赖 callback 获得的数据,还是在 thencallback 里获取到的,还没有真正像同步那样的写法。

真正的同步写法,要看下面的 generator


Generator

Generator 函数是 ES6 提供的一种异步编程解决方案。

特点:

  • function* 开始,注意这个*
  • 内部有一个 yield 关键字,跟 return 有点像,不同是 yield 可以有多个
  • 最突出的特点:可以交出执行权,暂停执行

优点

  • 同步写法简单明了
  • 特殊命名便于区分

缺点

  • 多个任务要写多个 next() ,需要自己手动加上自动执行

示例

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

可以通过不断地执行 next() 方法,可以改变当前函数的状态,其原理是什么呢?
这里就需要引申出一个迭代器的概念。

Iterator迭代器

它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

Iterator 的遍历过程是这样的。

(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

(2)第一次调用指针对象的 next 方法,可以将指针指向数据结构的第一个成员。

(3)第二次调用指针对象的 next 方法,指针就指向数据结构的第二个成员。

(4)不断调用指针对象的 next 方法,直到它指向数据结构的结束位置。

Generator 对象本身就具有 Symbol.iterator 的属性,所以执行 Generator 函数就会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

我们可以通过不断地执行 next() 获取内部状态的改变,但是当状态很多的时候,我们还要手动去执行?显然是不合理的,所以 Generator 函数一般要搭配自动执行一起使用。

generator 自动执行

我们先来看一个最简单的自动执行

function* gen() {
  // ...
}

var g = gen();
var res = g.next();

while(!res.done){
  console.log(res.value);
  res = g.next();
}

但是,这不适合异步操作。如果必须保证前一步执行完,才能执行后一步,上面的自动执行就不可行。

所以我们在这个时候就要用 yield 命令将程序的执行权移出 Generator 函数,然后再这一种方法,将执行权再交还给 Generator 函数。这种方法有两种:

  • Thunk函数
  • co模块

Thunk函数

我们先简单了解下什么 Thunk函数

编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数

JavaScript 语言的 Thunk 函数有所不同。

JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。

// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);

上面代码中,fs 模块的 readFile 方法是一个多参数函数,两个参数分别为文件名和回调函数。经过转换器处理,它变成了一个单参数函数,只接受回调函数作为参数。这个单参数版本,就叫做 Thunk 函数

我们可以使用已经封装好的第三方插件 thunkify 模块,比较便捷。

var thunkify = require('thunkify');
var fs = require('fs');

var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
  // ...
});

GeneratorThunk 函数 搭配使用就可以达到自动流程管理。

var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);

var gen = function* (){
  var r1 = yield readFileThunk('/etc/fstab');
  console.log(r1.toString());
  var r2 = yield readFileThunk('/etc/shells');
  console.log(r2.toString());
};

var g = gen();

var r1 = g.next();
r1.value(function (err, data) {
  if (err) throw err;
  var r2 = g.next(data);
  r2.value(function (err, data) {
    if (err) throw err;
    g.next(data);
  });
});

上面的代码我们可以发现 Generator 函数的执行过程,其实是将同一个回调函数,反复传入 next 方法的 value 属性。这使得我们可以用递归来自动完成这个过程。

// 执行器
function run(generator) {
  const gen = generator();
  // 这个next其实就是Thunk函数的回调函数
  function next(err, data) {
    const res = gen.next(data); // 类似{value: Thunk函数, done: false}
    if (res.done) {
      return;
    }
    res.value(next); // res.value是一个Thunk函数,而参数next就是一个callback
  }
  next();
}

const gen = function* () {
  var f1 = yield readFileThunk('fileA');
  var f2 = yield readFileThunk('fileB');
  // ...
  var fn = yield readFileThunk('fileN');
}

// run执行Generator函数
run(gen);

Thunk 函数并不是 Generator 函数自动执行的唯一方案。下面的 co模块 会更加便捷。

co模块

var co = require('co');
var gen = function* () {
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

co(gen);

// 或者
co(gen).then(function (){
  console.log('Generator 函数执行完成');
});

co 模块的原理

为什么 co 可以自动执行 Generator 函数?

前面说过,Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。

两种方法可以做到这一点。

  • 回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。

  • Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。

co 模块其实就是将两种自动执行器(Thunk 函数Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的yield命令后面,只能是 Thunk 函数Promise 对象。

除了co模块,Promise也可以实现自动执行,可参考Generator 函数的异步应用

generatorPromise 写法更加直观,但是自动执行需要我们自己添加,ES7在这个问题的基础上,提出了 async/await 方法,很多人认为它是异步操作的终极解决方案。


async/await

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
async 函数是什么?一句话,它就是 Generator 函数的语法糖。

优点

asyncFn 函数对 Generator 函数的改进,体现在以下四点。

  • 内置执行器
  • 更好的语义
  • 更广的适用性
  • 返回值是 Promise

缺点

  • await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低

Tip: await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
另一种情况是, await 命令后面是一个thenable对象(即定义 then 方法的对象),那么 await 会将其等同于 Promise 对象。

示例

// 定义async函数,注意async关键字
const readAsync = async function() {
    const data1 = await readPromise('./test1.txt'); // 注意await关键字
    const data2 = await readPromise('./test2.txt');
    return 'ok'; // 返回值可以在调用处通过then拿到
}

// 执行
readAsync();
// 或者
readAsync().then((data) => {
    console.log(data); // 'ok'
});

底层其实就是可以理解为 GeneratorThunk函数 的搭配使用

// 执行器
function asyncFn(generator) {
  const gen = generator();
  // 这个next其实就是Thunk函数的回调函数
  function next(err, data) {
    const res = gen.next(data); // 类似{value: Thunk函数, done: false}
    if (res.done) {
      return;
    }
    res.value(next); // res.value是一个Thunk函数,而参数next就是一个callback
  }
  next();
}

const gen = function* () {
  var f1 = yield readFileThunk('fileA');
  var f2 = yield readFileThunk('fileB');
  // ...
  var fn = yield readFileThunk('fileN');
}

// run执行Generator函数
asyncFn(gen);

目前为止, async/await 被大多数人认为是最终的异步解决方案,但是未来不可知,也许会有更高级的解决方案被提出呢?