1

异步(Asynchronous)指同一时间不止一个事件发生,或者说是多个相关事件不等待前一事件完成就发生。异步处理不用阻塞当前线程来等待处理完成,而是允许后续操作,直至其它线程将处理完成,并回调通知此线程。

现在与将来

一个完整的javascript程序,几乎一定是由多个块构成的。这些块中只有一个是现在执行,其余的则会在将来执行。最常见的块单位是函数。

举个例子

function now() {
 return 21;
}
function later() {
 answer = answer * 2;
 console.log( "Meaning of life:", answer );
}
var answer = now();
setTimeout( later, 1000 );

上面的例子两个块:现在执行的部分,以及将来执行的部分。

现在执行部分

function now() {
 return 21;
}
function later() { .. }
var answer = now();
setTimeout( later, 1000 );

将来执行部分

answer = answer * 2;
console.log( "Meaning of life:", answer );
任何时候,只要把一段代码包装成一个函数,并指定它在响应某个事件(定时器、鼠标点击、Ajax 响应等)时执行,你就是在代码中创建了一个将来执行的块,也由此在这个程序中引入了异步机制。程序中现在运行的部分和将来运行的部分之间的关系就是异步编程的核心。

事件循环

异步与事件循环密切相关,在了解解决方案前,建议先看下并发模型与事件循环

异步编程解决方案

  • 回调
  • 事件监听
  • 发布订阅
  • Promise
  • Generator
  • async/await

注意:Promise、Generator、async/await 在IE浏览器都不支持,需要做兼容处理。

下面介绍常用的解决方案。

1.回调

JavaScript 语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。

缺点:大量的嵌套回调会造成回调地狱,难以维护。

// 如果下一请求依赖上一个请求的返回值,就需要不断嵌套
$.ajax({ 
  url: "url-1", 
  success: function(){
    $.ajax({ 
      url: "url-2", 
      success: function(){
        $.ajax({ 
          url: "url-3", 
          success: function(){
            // ...
          }
        });
      }
    });
  }
});

2.promise(ES6)

介绍

  • promise是一个代表了异步操作最终完成或者失败的对象。
  • 本质上,promise是一个函数返回的对象, 它可以绑定回调函数
  • Promise对象是一个构造函数,用来生成Promise实例。
  • 使用promise最直接的好处就是能够使用then进行链式调用

创建

Promise 对象是由关键字 new 及其构造函数来创建的。

var p = new Promise((resolve, reject) => {
    // 一系列异步操作
    // resolve()
    // reject()
});
console.log(p) // Promise

想要某个函数拥有 promise 功能,只需让其返回一个promise即可。

function myAsyncFunction(url) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open("GET", url);
        xhr.onload = () => resolve(xhr.responseText);
        xhr.onerror = () => reject(xhr.statusText);
        xhr.send();
    });
  };

状态

一个 Promise有以下几种状态:

  • pending: 初始状态,既不是成功,也不是失败状态。
  • fulfilled: 意味着操作成功完成。状态:pengding=>fulfilled
  • rejected: 意味着操作失败。状态:pending=>rejected
一个 promise 对象处在 fulfilled 或 rejected 状态而不是 pending 状态,那么它也可以被称为 settled 状态。你可能也会听到一个术语 resolved ,它表示 promise 对象处于 settled 状态。但是在平时大家都习惯将 resolved 特指 fulfilled 状态
var p1 = new Promise((resolve, reject) => {
});
console.log(p1); // pending

var p2 = new Promise((resolve, reject) => {
    resolve('成功');
});
console.log(p2); // fulfilled

var p3 = new Promise((resolve, reject) => {
    reject('失败');
});
console.log(p3); // reject

注意:promise状态是不可逆的。

promise的状态 只有从pengding =》fulfilled 或者 pending =》 rejected,而不会反过来。并且已经resolve的数据,后面无论如何修改,都不会改变then中接受到的数据。

new Promise((resolve, reject) => {
    var num = 100;
    resolve(num);
    num = 999;
    resolve(num); // resolve 也不会改变已传出去的num 100
    console.log(num) // 999
}).then(result => { 
    console.log(result) // 100
});

属性

  • Promise.length:length属性,值总是为1
  • Promise.prototype:构造器原型

方法

iterable:一个可迭代对象,如 Array 或 String。

方法名功能返回结果
Promise.all(iterable)所有传入的 promise 成功才触发成功,只要有一个失败就会触发失败包含所有 promise 值的数组
Promise.allSettled(iterable)所有传入的promise都完成(成功/失败)后完成包含所有 promise 值的数组
Promise.any(iterable)当第一个成功的 promise 成功时返回第一个成功的promise的值
Promise.race(iterable)当第一个 promise 成功/失败返回第一个完成的promise的值
Promise.reject(reason)返回一个状态为失败的 Promise 对象状态为失败的Promise
Promise.resolve(value)返回一个状态由给定value决定的 Promise对象状态为成功的Promise
注意:Promise.any() 方法尚未被所有的浏览器完全支持。它当前处于 TC39 第四阶段草案(Stage 4)

原型属性

  • Promise.prototype.constructor:返回被创建的实例函数. 默认为 Promise 函数.

原型方法

方法名功能返回结果说明
Promise.prototype.then(resolFun, rejecFun)添加 fulfilled 和 rejected 回调到当前 promise返回新的 promise当回调函数被调用,新 promise 都将以它的返回值来resolve
Promise.prototype.catch(Fun)添加一个 rejection 回调到当前 promise返回新的 promise返回的 promise 会以 rejection 的返回值 resolve
Promise.prototype.finally(Fun)当其中的一个 promise 成功时返回返回新的 promise无论是fulfilled还是rejected,都会执行

promise/A+规范

Promise 规范有很多,如Promise/A,Promise/B,Promise/D 以及 Promise/A 的升级版 Promise/A+。

ES6 中采用了 Promise/A+ 规范

Promises/A+规范总结:

  1. 一个 promise 的当前状态只能是 pending、fulfilled 和 rejected 三种之一。状态改变只能是 pending 到 fulfilled 或者 pending 到rejected。状态改变不可逆。
  2. promise 的 then 方法接收两个可选参数,表示该 promise 状态改变时的回调。then 方法必须返回一个 promise。then 方法可以被同一个 promise 调用多次。

3.Generator(ES6)

介绍

  • Generator 函数是一个状态机,封装了多个内部状态
  • 执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态
  • Generator 函数只有调用 next() 方法才会遍历下一个内部状态

关键标识和关键字

  • function* : 关键标识
  • yield:暂停执行
  • yield* :语法糖,在 Generator 函数中执行另一个 Generator 函数

方法

方法名功能备注
Generator.prototype.next()返回一个由 yield表达式生成的值
Generator.prototype.return()返回给定的值并结束生成器
Generator.prototype.throw()向生成器抛出一个错误

下面对关键字和各个方法做详细介绍

yield 表达式

  • yield 表示暂停执行
  • yield 表达式后面的表达式,只有当调用 next()、内部指针指向该语句时才会执行
  • yield 表达式的值会作为返回的对象的 value 属性值
  • 调用 next() 之前,yield 前面的语句不会执行
function* helloWorldGenerator() {
  console.log('aaa')
  yield 'hello';
  console.log('bbb')
  yield 'world';
  console.log('ccc')
  return 'ending';
}

var hw = helloWorldGenerator();
console.log(hw); // helloWorldGenerator {<suspended>}  状态:suspended

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

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

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

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

console.log(hw); // helloWorldGenerator {<closed>}   状态:closed
关于 号。虽然ES6 没有规定,function关键字与函数名之间的星号必须连在一起,但我们还是根据标准的写法。即 function funName(){}

使用注意:

  • yield 只能在 Generator 函数中使用,在其他地方使用会报错。就算是在Generator 函数内,但处于一个普通函数内,也会报错
var arr = [1, [[2, 3], 4], [5, 6]];

var flat = function* (a) {
  a.forEach(function (item) {
    if (typeof item !== 'number') {
      yield* flat(item);
    } else {
      yield item;
    }
  });
};

for (var f of flat(arr)){
  console.log(f);
}
// Uncaught SyntaxError: Unexpected identifier

// 改造下
var arr = [1, [[2, 3], 4], [5, 6]];

var flat = function* (a) {
  var length = a.length;
  for (var i = 0; i < length; i++) {
    var item = a[i];
    if (typeof item !== 'number') {
      yield* flat(item);
    } else {
      yield item;
    }
  }
};

for (var f of flat(arr)) {
  console.log(f);
}
// 1, 2, 3, 4, 5, 6
  • yield表达式如果用在另一个表达式之中,必须放在圆括号里面
function* demo() {
  console.log('Hello' + yield); // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError

  console.log('Hello' + (yield)); // OK
  console.log('Hello' + (yield 123)); // OK
}
  • yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号
function* demo() {
  foo(yield 'a', yield 'b'); // OK
  let input = yield; // OK
}

next()方法

  • next() 表示恢复执行
  • next() 可接受参数,并且该参数表示上一个yield表达式的返回值
  • 第一次调用 next() 时,传递参数无效。(可封装函数,先执行一次无参 next())
function* G() {
    const a = yield 100
    console.log('a', a)
    const b = yield 200
    console.log('b', b)
    const c = yield 300
    console.log('c', c)
}
var g = G()

g.next(); // {value: 100, done: false}
g.next(); // a undefined  {value: 200, done: false}
g.next(); // b undefined  {value: 300, done: false}
g.next(); // c undefined  {value: undefined, done: true}

g.next();      // {value: 100, done: false}
g.next('aaa'); // a aaa   {value: 200, done: false} 
g.next('bbb'); // b bbb   {value: 300, done: false}
g.next('ccc'); // c ccc   {value: undefined, done: true}

throw()方法

throw() 方法用来向生成器抛出异常,并恢复生成器的执行,返回带有 done 及 value 两个属性的对象。

使用

  • throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获
  • 如果 Generator 函数内部没有部署try...catch代码块,那么throw方法抛出的错误,将被外部try...catch代码块捕获
  • 如果 Generator 函数内部和外部,都没有部署try...catch代码块,那么程序将报错,直接中断执行
  • throw 方法可以接受一个参数,该参数会被catch语句接收
  • catch 语句只能捕获到第一个 throw 方法
  • throw 方法不会中断程序执行,并且会自动执行下一次 next()

优点

多个yield表达式,可以只用一个try...catch代码块来捕获错误

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log('内部捕获', e); 
  }
};

var i = g();
i.next();

try {
  var err = i.throw('参数 aaa');
  console.log(err)
  i.throw('b');
} catch (e) {
  console.log('外部捕获', e);
}
// 内部捕获 参数 aaa
// {value: undefined, done: true}
// 外部捕获 b

如果 Generator 函数内部没有部署try...catch代码块,那么throw方法抛出的错误,将被外部try...catch代码块捕获

var g = function* () {
  yield;
  console.log('内部捕获', e);
};

var i = g();
i.next();

try {
  i.throw('参数 aaa');
  i.throw('b');
} catch (e) {
  console.log('外部捕获', e); // 外部捕获 参数 aaa
}

如果 Generator 函数内部和外部,都没有部署try...catch代码块,那么程序将报错,直接中断执行。

var g = function* () {
  yield;
  console.log('内部捕获', e);
};

var i = g();
i.next();
i.throw();
// VM9627:2 Uncaught undefined

throw 方法不会中断程序执行,并且会自动执行下一次 next()

var gen = function* gen(){
  try {
    yield console.log('a');
  } catch (e) {
    console.log('error')
  }
  yield console.log('b');
  yield console.log('c');
}

var g = gen();
g.next() // a
g.throw() // error b
g.next() // c

return()方法

return方法可以返回给定的值,并且终结遍历 Generator 函数。

  • 如果 return 方法调用时,不提供参数,则返回值的 value 属性值为 undefined
  • 如果 Generator 函数内部有try...finally代码块,且正在执行try代码块,那么return方法会立刻进入finally代码块,执行完以后,整个函数才会结束。
function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();

g.next()        // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true } =》后面的 done 始终为 true
g.next()        // { value: undefined, done: true }

Generator 函数内部有try...finally代码块

function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally { // return后依然执行
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }

yield* 表达式

yield* 相当于一个语法糖(给后面的可迭代对象部署一个for...of循环),用于执行 Generator 函数内的 Generator 函数

  • yield* 等同于在 Generator 函数内部,部署一个for...of循环
  • yield* 返回一个遍历器对象
  • 任何可迭代对象都可被 yield* 遍历
// 执行 Generator 函数内的 Generator 函数
function* foo() {
  yield 2;
  yield 3;
  return "foo";
}
function* bar() {
  yield 1;
  var v = yield* foo();
  console.log("v: " + v);
  yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}


// 任何可迭代对象都可被 yield* 遍历
let read = (function* () {
  yield* 'he';
  yield* [1, 2, 3];
})();

read.next().value // "h"
read.next().value // "e"
read.next().value // 1
read.next().value // 2
read.next().value // 3

应用

待更新...

4.async/await(ES7)

介绍

  • async 函数是 Generator 函数的语法糖,它的写法更趋向于同步。
  • async 函数返回一个 promise 对象
  • async 函数可以包含0个或者多个 await 指令
  • await 会暂停异步函数的执行,并等待Promise执行,然后继续执行异步函数,并返回结果
  • await 表达之后的代码可以被认为是存在在链式调用的 then 回调方法中
async/await的目的是简化使用多个 promise 时的同步行为,并对一组 Promises执行某些操作。正如Promises类似于结构化回调,async/await更像结合了generators和 promises。

async对Generator的改进

改进点asyncGenerator
内置执行器调用即执行调用 next() 方法才执行
更好的语义async(内部有异步操作)、await(等待异步操作完成后)Generator(生成器)、yield(产出)
更广的适用性await 后可以是Promise 对象和原始数据类型(会被自动转成Promise.resolve()的值)yield命令后面只能是 Thunk 函数或 Promise 对象
返回值是 Promise返回 Promise 对象返回 Iterator 对象

基本使用

// 异步请求
function resolveAfter1Seconds() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('1s resolved');
    }, 1000);
  });
}
function resolveAfter3Seconds() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('3s resolved');
    }, 3000);
  });
}


async function asyncCall() {
  console.log('calling');
  const result1 = await resolveAfter3Seconds();
  console.log(result1)
  const result2 = await resolveAfter1Seconds();
  console.log(result2);
  return 'ending'
}

asyncCall().then(res => {
  console.log(res)
});
// calling
// 3s resolved => 执行3秒后输出
// 1s resolved => 执行4秒后输出
// ending

await命令

  • await 命令后是一个 Promise 对象,返回该对象的结果,如果不是则直接返回对应值
  • await 命令后的代码可以被认为是存在在链式调用的then回调方法中
async function foo() {
   await 1 // 原始数据类型会被自动转成Promise.resolve()的值
}
// 等价于
function foo() {
   return Promise.resolve(1).then(() => undefined)
}

错误处理

  • await 命令后代码执行失败,并且有 try...catch 捕捉错误,则不会影响接下来的代码执行。
function step(val){
  if (val === 2) {
    return Promise.reject('出错了')
  } else {
    return  val
  }
}

async function main() {
  try {
    const val1 = await step(1);
    const val2 = await step(2);
    const val3 = await step(3);
    console.log('Final: ', val1, val2, val3);
  } catch (err) {
    console.error('error', err);
  }
  console.log('继续执行')
}
main();
// error 出错了
// 继续执行
  • await 命令没有 try...catch, async 函数返回的 Promise 对象会被reject,程序中断
function step(val){
  if (val === 2) {
    return Promise.reject('出错了')
  } else {
    return  val
  }
}

async function main() {
  const val1 = await step(1);
  const val2 = await step(2);
  const val3 = await step(3);
  console.log('继续执行')
}

main().then().catch(err => {
  console.log('error', err)
})
// error 出错了

async 函数的实现原理

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

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

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); });
  });
}

总结

  1. JS 异步编程进化史:callback -> promise -> generator -> async + await
  2. async/await 的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。
  3. async/await 相对于Promise,优势体现在:

    • 处理 then 的调用链,能够更清晰准确的写出代码并且也能优雅地解决回调地狱问题。
  4. async/await 对 Generator 函数的改进,体现在:

    • 内置执行器
    • 更好的语义
    • 更广的适用性
    • 返回值是 Promise 对象
  5. async/await 的缺点:

    • 如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。代码没有依赖性的话,可以使用 Promise.all 的方式替代。

YanniLi
56 声望4 粉丝