JS 异步编程解决方案:Promise、Generator、Async/Await

一、Prommise

1.1 简介

回想一下我们使用传统方式(回调函数)是如何来解决异步编程依赖问题的

login(param, function() {
  consloe.log('登录成功,开始获取用户信息')
  getUser(param, function() {
    console.log('已经获取到用户信息,现在来获取菜单')
    getMenu(param, function() {
      console.log('获取到菜单了')
    })
  })
})

恐怖吗?这还只是三层依赖,如果有更深的依赖关系,那真的会陷入 ajax 的回调地狱。为了解决这个问题, 晴空一声霹雳响,天上掉下个 Promise

Promise 是异步编程的一种解决方案,ES6 将其写入了语言标准。译为 “承诺”,你将一件事情交给他管理,等到这个事情有了结果,或者成功或者失败,它会承诺给你一个结果。

Promise 可以理解为一个容器,它肚子里装着一个函数,通常在这个函数里进行一些异步请求,会在将来某个时候拿到结果。Promsie 对象具有以下两个特点:

  • 三个状态:pending(进行中)、fullfiled(已成功)、rejected(已失败),状态只能在异步函数中改变,外界无法改变其状态
  • 状态不可逆:状态只能从 pending -> fullfiled 或 pending -> rejected

1.2 基本用法

先介绍下 Promise 的基本用法,ES6 规定,Promise 是一个构造函数,使用生成 Promise 实例。

var login = function (param) {
  return new Promise(function (resolve, reject) {
    ajax('http://127.0.0.1/login', function(rest) {
      if (rest.success) {
        resolve(rest)
      } else {
        reject(new Error('登录失败'))
      }
    })
  })
}

构造函数 Promise 接收一个函数作为入参,该函数接收 resolve、reject 两个参数,这两个参数会由 js 引擎提供,无需自己传入

  1. 当异步函数判断结果为成功时调用 resolve,这时 Promise 的状态就会从 pending 变成 fullfiled
  2. 当异步函数判断结果为失败时调用reject,这时 Promise 的状态就会从 pending 变成 rejected

现在我们用 Promise 把 login 做了改造,return 了一个 Promise 实例。那么这个 login 该怎么用呢?

login(param).then(function (rest) {
  // 当执行 resolve 时就会进入这里,表示成功,rest就是后端返回的登陆信息
}, function (e) {
  // 当执行 reject 时就会进入这里,表示失败
})

login 函数执行会返回一个 Promise 对象,Promise 对象上有个 then 方法,它接受两个函数

  • 前者为异步成功时的回调,暂时可以理解为注册了 resolve 函数
  • 后者为异步失败时的回调,暂时可以理解为注册了 reject 函数

reject 函数除了可以注册在 then 函数的第二个参数上以外,还可以注册在 Promise.prototype.catch 函数中

login(param).then(function (rest) {
  // 当执行 resolve 时就会进入这里,表示成功,rest就是后端返回的登陆信息
}).catch(function (e) {
  // 当执行 reject 时就会进入这里,表示失败
})

不管成功或者失败,我都想执行一步操作怎么办呢?ES9 引入了 Promise.prototype.finally 函数

login(param).then(function (rest) {
  // 当执行 resolve 时就会进入这里,表示成功,rest就是后端返回的登陆信息
}).catch(function (e) {
  // 当执行 reject 时就会进入这里,表示失败
}).finally(function () {
  // 不管成功或者失败,都会走到这里
})

1.3 链式调用

咋一看,用了 Promise 之后代码量还增加了一点对吧!那它带来的好处是什么呢?别急,接着往下看

当我们把另外两个接口 getUser、getMenu 两个接口都用 Promise 改造之后......

login(param)
.then(function(loginData) {
    return getUser(loginData)
}).then(funtion(userData) {
    return getMenu(userData)
}).then(function(menuData) {
    // 可以一直链下去
})

 我们跳出了嵌套地狱,函数不用再写在回调里了,只需要使用 Promise 提供的 then 函数把所以的依赖按顺序给链接起来

  • 前一个异步执行结束之后才会执行下一个异步
  • 前一个 then 里面的异步执行结果会传入到下一个 then 注册的函数里,如上述代码中 getUser 的结果会传给 getMenu 这一行

1.4 Promise.all

上面是链式串行调用的方式,各个函数之间是有依赖关系的。但是现在我有这个需求

  • 异步函数之间没有依赖关系
  • 我想要它们并行的去执行,这样会比串行快很多
  • 等到所有的函数都执行完了把结果一起返回给我

这个时候 Promise.all 就排上用场了,现在假设上文中的 login、getUser、getMenu 三个函数之后没有依赖关系

Promise.all([login(), getUser(), getMenu()]).then(function(rest) {
    // 三个异步函数都执行完了才会走到这里
    console.log(rest); // [loginData, userData, menuData]
}).catch(function(e) {
    // 只要有一个函数失败,执行了 reject,就会走到这里,promise 结束
})

Promise.all 函数的入参是一个数组,数组中的每个元素都是一个 promise 实例,经过 Promise.all 封装成了一个新的 promise 实例。等到数组中所有 promise 实例的状态都变成 resolved 的时候就会,这个新的 promise 实例的状态页面变成 resolved。如果数组中有一个 promise 的状态变成了 rejected,那么这个新的 promise 的状态也就变成了 rejected。

1.5 Promise.race

现在我的需求又变了,这三个异步函数谁先执行完我就先处理谁,其他两个我就不管了。这个时候就得用 Promise.race 了,我们称之为“竞赛模式”。

Promise.race([login(), getUser(), getMenu()]).then(function(rest) {
    // 只要有一个先执行结束,它的结果就会返回到 rest 上
    console.log(rest); // loginData 或 userData 或 menuData
}).catch(function(e) {
    // 只要有一个函数失败,执行了 reject,就会走到这里,promise 结束
})

1.6 自己撸一个

现在都讲究造轮子,既然 Promise 的用法我们都已经知道了,那就自己来撸一个吧!

从用法上我们可以看出一下几点

  • Promise 是一个构造函数
  • 有一个状态
  • 有两个数组,一个用来保存成功时的回调函数,一个用来保存失败时的回调函数
  • 原型上有 then、catch、finally 这么几个方法
  • 有 resolve、reject、all、reace 这么几个静态方法

构造函数

function Promise (executor) {
  var self = this;
  this.status = 'pending';  // 状态
  this.data = undefined;
  this.onResolvedCallback = []; // 通过 then 注册的成功回调
  this.onRejectedCallback = []; // 通过 then 或者 catch 注册的失败回调
  
  function resolve (value) {
    if (this.status === 'pending') {
      self.status = 'resolved'; // 成功时将状态改为 resolved
      self.data = value;
      for (var i=0; i<this.onResolvedCallback.length; i++) {
        self.onResolvedCallback[i](value) // 执行注册的成功函数
      }
    }
  }

  function reject (reason) {
    if (this.status === 'pending') {
      self.status = 'rejected'; // 失败时将状态改为 rejected
      for (var i=0; i<this.onRejectedCallback.length; i++) {
        self.onRejectedCallback[i](reason) // 执行注册的失败函数
      }
    }
  }

  try {
    executor(resolve, reject);  // new Promise 的时候立即执行 executor
  } catch (e) {
    reject(e)
  }
}

then 

then 函数是用来注册成功或者失败时的回调函数的。需要注意的是,当调用 then 函数来注册回调时,promise 实例的状态有可能是 pending、resolved、rejected。

Promise.prototype.then = function (onResolved, onRejected) {
  // 为了可以使用链式写法,then 方法返回的是一个 promise 对象
  if (self.status === 'pending') {
    // 通常情况下,如果 promise 包裹的是一个异步操作,那么走到 then 是,promise 应该还是 pending状态
    return promise1 = new Promise(function (resolve, reject) {
      // 注册成功回调
      // self.onResolveCallback.push(onResolved)
      // 本来我们直接把 onResolved push 到 onResolveCallback 这个数组里就可以了,异步执行完成时会调用到这个 onResolved,并把结果传进去
      // 但是 onResolved 里面可能还有一个异步操作(假设p),我们得等到这个异步p的状态改变了之后,才能继续下一个 then,这样才能完成正确的依赖关系
      // 所以我们还得判断 onResolved 函数的返回结果
      self.onResolveCallback.push(function (value) {
        var x = onResolved(value);
        if (x instanceof Promise) {
          // 如果 onResolved 返回的仍是一个 promise,那就等等到这个 promise 的状态改变了之后,才能改变 promise1 的状态,然后继续下一个 then
          x.then(resolve, reject)
        } else {
          // 否则直接改变 promise1 的状态,把 promise1 的执行结果 x 传入到下一个 then 中的回调
          resolve(x);
        }

      })

      // 注册失败回调
      self.onRejectCallback.push(function(reason) {
        var x = onRejected(reason);
        if (x instanceof Promise) {
          x.then(resolve, reject)
        } else {
          reject(reason)
        }
      })
    })
    
    
  }
  if (self.status === 'resolved') {
    // 如果 new Promise(executor) 的时候,传入的 executor 内并没有执行异步操作,而是直接调用了 resolve,那么走到 then 的时候,Promise 已经是 resolved 状态了
    // 为了能够链式调用我们还是要返回一个 promise
    return new Promise(function(resolve, reject) {
      // 由于状态已经是 resolved 了,就不需要把 onResolved、onRejected push 到队列中了
      // 直接把 executor 的执行直接塞进 onResolved 就行了
      var x = onResolved(self.value)
      if (x instanceof Promise) {
        x.then(resolve, reject)
      } else {
        resolve(x)
      }
    })
  }
  if (self.status === 'rejected') {
    // 逻辑和上面差不多,只是最后一步应该执行 reject(x)
  }
}

all

做一个计数,等所有 promise 都完成了,执行 resolve 改变状态就行了

Promise.all = function(arr) {
  return new Promise(function (resolve, reject) {
    var count = 0;  // 记录已完成了 promise
    var list = []
    // 数组 arr 中每一项都是一个 promis 对象
    arr.forEach((p,index) => {
      p.then(res => {
        count ++;
        list[index] = res;
        if (count === arr.length) {
          // 所有的 promise 都成功了
          resolve(list)
        }
      }).catch(e => {
        reject(e)
      })
    })
  })
}

race

做一个标识,第一个完成的 promise 执行 resolve 改变下状态,其他的就不管了

Promise.race = function(arr) {
  return new Promise(function (resolve, reject) {
    // 做一个标识,如果有一个完成了执行 resolve,等其他的 promise 完成了就不管了
    var done = false;
    // 数组 arr 中每一项都是一个 promis 对象
    arr.forEach((p,index) => {
      p.then(res => {
        if (!done) {
          resolve(res)
        }
      }).catch(e => {
        if (!done) {
          reject(e)
        }
      })
    })
  })
}

 重要的就这么几个函数,好了,Promise 的使用和实现到此结束。

然而技术总是在不断的更新,程序员在追求代码优雅的路上也在不断的探索。

二、Generator

2.1 简介

Generator 是 ES6 提出的一种异步编程解决方案,语法上可以将它理解为一个状态机,封装了多个内部状态。同时 Generator 也是一个遍历器生成函数,执行它可以生成一个遍历器,用于遍历 Generator 函数内部的每个状态。

2.2 基本用法

function* test() {
  let a = 1 + 2;
  yield 2;
  yield 3;
}
let b = test();
console.log(b.next()); // >  { value: 2, done: false }
console.log(b.next()); // >  { value: 3, done: false }
console.log(b.next()); // >  { value: undefined, done: true }

执行 test() 函数时,其内部的代码并没有开始执行,而是生成了一个遍历器 b,执行 b.next() 时才开始执行 test 内部的代码。每次遇到 yield 标志就会暂停,如同一个指针一步一步往下执行,直到遇到 return 或者 没有代码为止。

2.3 原理

function test () {
  var a;
  return generator (function (_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          a = 1 + 2;
          _context.next = 4;  // 标记下一个需要执行的代码块
          return 2; // 返回 yield 后的内容
        case 4:
          _context.next = 6;
          return 3;
        case 6:
        case 'end':
          return _context.stop();
      }
    }
  })
}

function generator (cb) {
  return (function () {
    var obj = {
      next: 0,
      stop: function () {}
    }
    return {
      next: function () {
        var ret = cb(obj);
        if (ret === undefined)
          return { value: undefined, done: true }
        return { value: ret, done: false }

      }
    }
  })()
}

 这是基本用法中的代码被 babel 转译后的样子,babel 会根据 yield 将代码分割三块,然后给每一块代码加一个编号,通过 switch、case 传入的参数值来决定执行哪一块代码。

  • 第一次 b.next(),传入到 test 中的代码块编号是 0,在这个代码块中,会标记出下一个要执行的代码块编号,并返回 yield 后的内容。
  • 第二次 b.next(),obj 里已经知道下一个要执行的代码块的编号是 4
  • 以此类推

三、Async/Await

3.1 简介

async 函数在 ES2017 纳入了标准,它是 Generator 函数的语法糖,使得异步操作变得更加简单。

Generator 函数需要手动去执行 next() 函数,而 async 函数内置了执行器,且返回值是一个 pending 状态的 Promise,方便是用 then 函数进行链式调用。

3.2 基本用法

async 函数的返回值是一个 promise,可以使用 then 注册回调函数。它和 await 搭配使用,当 async 函数执行到 await 的时候,就会等待 await 后的函数执行完之后再执行后面的内容。比如文章开头的三个函数就可以写成这样

async function start () {
  var loginData = await login().catch(...);
  var userData = await getUser();
  var menuData = await getMenu();
  return menuData;
}
start.then(...)

看起来是不是很简单、很优雅,我们可以像写同步代码一样去操作异步函数。而且比起 Promise 我们可以更加容易的去中断链路,在任意一行 return 就行了。

这里需要注意几点

  • async 必须紧跟着 function,像这种写法是错误的 var async start = function () {}
  • await 必须在 async 申明的函数内部使用,不然会报错
  • await 后面需要是一个 promise 对象,才能达到“等待”的效果。其他对象是达不到这个效果的,如 await setTimeout()

3.3 原理

async 的实现原理,就是将 Generator 函数和自动执行器包装在一个函数里

async function (args) {
  // ...
}
// 等同于
function fn (args) {
  return spawn (function* () {
    // ...
  })
}

所有的 async 函数都可以写成上面第二种形式,其中 spawn 函数就是自动执行器。那么现在基本用法中的例子就可以写成下面这个样子

function start () {
  function* genF () {
    var loginData = yield login().catch(...);
    var userData = yield getUser();
    var menuData = yield getMenu();
    return menuData;
  }

  return spawn(genF)
}

下面我们来看一下 spawn 的实现方式

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

首先 async 函数返回的是一个 pending 状态 Promise,那么 spawn 首先要返回一个 Promise

然后在 Promise 内我们执行 genF() 生成了一个遍历器 gen

第一次执行遍历器得到的 next.value,是例子中 login() 返回的 promise。我们将这这个 promise 对象封装在了 Promise.resolve(next.value)

等到 login 异步执行成功了,也就是next.value(promise)的状态也变了,这时候就可以执行 Promise.resolve(next.value).then 内的内容,也就是执行下一个 next()(也就是getUser())

以此类推

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值