Javascript关于异步编程的发展

Afterward

前言

PS:
2018/08/08 转简体
2018/08/09 重新排版布局,代码全面使用ES6并且直接附上输出结果,补充细节
2018/08/13 补充Async..await内容,增加例子说明
2018/08/20 新增Async..await和Promise.all在遍历情况下怎么使用

上文讲了关于Javascript执行机制--单线程,同异步任务,事件循环的知识点,我们知道Javascript在某一时刻内只能执行特定的一个任务,并且会阻塞其它任务执行,为了解决这个问题,Javascript语言将任务的执行模式分成两种:

  • 同步(Synchronous):一定要等任务执行完了,得到结果,才执行下一个任务;
  • 异步(Asynchronous):不等任务执行完,直接执行下一个任务,等到后面再继续执行未完成的任务;

现在我们就讲讲关于异步编程的发展
更多细节请看阮一峰大神的《ECMAScript 6 入门》

总结图

如果看不清晰右键图片新标签页打开
图片描述

回调函数

百度百科的解释是:

回调函数就是一个通过函数指针调用的函数.如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数.回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应.

机制:

⑴定义一个回调函数;
⑵提供函数实现的一方在初始化的时候,将回调函数的函数指针注册给调用者;
⑶当特定的事件或条件发生的时候,调用者使用函数指针调用回调函数对事件进行处理.

例如:

//定义一个回调函数
function callback() {
  console.log('I am a callback!');
}

//函数实现
function trigger(fn, time) {
  setTimeout(function() {
    fn && fn();
  }, time || 1000);
}

//耐心等两秒哦
trigger(callback, 2000);

//输出
//I am a callback!

优劣点

缺点:

  • 线性理解能力缺失,代码高度耦合,不利于维护阅读,嵌套层级深的情况流程混乱,陷入“回调地狱”;
  • 因为回调本身错误无法被外层代码捕捉,需要在实现内部里实现额外的容错代码(返回报错,多次执行,不执行等);

tips:因为异步任务在执行机制里处理方式不同的问题,try/catch语句只能捕捉执行栈上的错误,详情请回顾Javascript执行机制--单线程,同异步任务,事件循环

事件监听

也叫观察者模式,这是一种常见的编程方式,一个对象(目标对象和观察者对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知.执行事件由触发事件调用与顺序无关,易用低耦合并且不依赖函数调用.

DOM监听事件

<!DOCTYPE html>
<html>

  <head>
    <meta charset="utf-8">
    <title></title>
  </head>

  <body>
    <ul id="list">
      <li id="item1">item1</li>
      <li id="item2">item2</li>
      <li id="item3">item3</li>
    </ul>
    <script type="text/javascript">
      var list = document.getElementById("list");

      document.addEventListener("click", function (event) {
        var target = event.target;
        if (target.nodeName == "LI") {
          alert(target.innerHTML);
        }
      })
    </script>
  </body>

</html>

优劣点

优点:

  • 低耦合,目标对象和观察者对象建立了一个抽象的关系;
  • 一对多的映射关系,目标对象的状态发生改变,所有的观察者对象都将得到通知,进行各自的回调操作;
  • 任务的执行不取决于代码的顺序,而取决于某个事件是否发生;

缺点:

  1. 尽管松耦合的通信方式优于对象之间的硬编码,代码流程依然是硬伤,因为你可能不知道在哪里定义了什么回调函数触发了什么事件发生;

发布/订阅

这是一种类似事件监听但是却更强大的设计模式(很多人也把两者视为同一种模式,个人觉得还是有点区别的),例如:

  • 前者由目标对象负责调度,后者由一个调度中心负责调度;
  • 前者低耦合,后者不耦合;

例如我们可以自己封装一个方法以实现更多监听事件

var listener = (function() {
  //订阅队列
  var list = [],
    //监听事件
    on = function(type, fn) {
      if (!list[type]) list[type] = [];
      list[type].push(fn);
    },
    //取消监听事件
    off = function(type, fn) {
      list[type] = [];
    },
    //触发事件
    trigger = function() {
      //取出监听类型
      var type = Array.prototype.shift.call(arguments),
        queue = list[type],
        i = 0,
        len = queue.length;

      for (; i < len; i++) {
        //带参数发布
        queue[i].apply(this, arguments);
      }
    };

  return {
    on: on,
    off: off,
    trigger: trigger,
  };
})();

listener.on('log', function() {
  console.log('I trigger a log!');
});
listener.trigger('log');

//取消之后不触发
listener.off('log');
listener.trigger('log');

//输出
//I trigger a log!

优劣点

优点:

  • 调度中心统一管理目标对象和观察者对象相关信息,之间互不依赖;

Promise

上面的方法不管怎么变化,始终避不开回调地狱的困境,直到后来被社区提出和实现并且已经被放置到ES6统一标准化的---Promise出现.
简单来说就两个特性:

  • 初始化时候的pending(进行中)状态只能通过异步操作的结果更改成fulfilled/resolved(已成功)或者rejected(已失败)的状态,即其他任何手段都不能影响到;
  • 状态一旦更改是不可逆,即使之后再添加回调函数也只是返回同个结果,这个特点恰恰能解决上面说的回调函数无法被外层代码捕捉错误的缺点;
const promise = new Promise((resolve, reject) => {
  true ? resolve('success') : reject('error');
});
//Promise实例
console.log(promise);

promise.then(
  function resolved(res) {
    console.log(res);
  },
  function rejected(err) {
    console.log(err);
  }
);

//输出
//Promise {  }
//success

Promise构造函数接受一个函数作为参数,JavaScript 引擎提供两个参数分别是:

参数名 用途
resolve 将Promise对象的状态从pending(进行中)变为resolved(已成功)
reject 将Promise对象的状态从pending(进行中)变为rejected(已失败)

其中rejected(已失败)的状态包括程序的错误也能捕捉并且不会中断程序运行,并把错误原因当成参数传递下去

const promise = new Promise(function(resolve, reject) {
  //未定义变量
  return num;
});

promise.then(null, function rejected(err) {
  console.log(`rejected:${err}`);
});

//输出
//rejected: ReferenceError: num is not defined

then

Promise 实例能访问定义在原型对象Promise.prototype上的then方法.它的作用是为 Promise 实例添加状态改变时的回调函数,提供两个参数分别是

参数名 用途
resolved resolved状态的回调函数
rejected(可选) 是rejected状态的回调函数,如果发生错误但没有处理函数会程序报错而不是状态失败报错
const promise = new Promise(function(resolve, reject) {
  throw 'error';
});

//有rejected处理函数
promise.then(
  function resolved(res) {
    console.log(res);
  },
  function rejected(err) {
    console.log(`捕捉错误:${err}`);
  }
);

//没有rejected处理函数
promise.then(function resolved(res) {
  console.log(res);
});

//捕捉错误:error
//系统报错

因为then返回的是一个新的Promise实例,所以可以链式调用,如果需要把当前结果作为下一个then方法的参数将它作为返回值return.

const promise = new Promise((resolve, reject) => {
  true ? resolve('success') : reject('error');
});

promise
  .then(function resolved(res) {
    console.log(res);
    //这次不返回结果了
  })
  .then(function resolved(res) {
    console.log(res);
    //这次返回其他
    return '成功';
  })
  .then(
    function resolved(res) {
      console.log(`成功信息:${res}`);
    },
    function rejected(err) {
      console.log(`失败信息:${err}`);
    }
  );

//输出
//success
//undefined
//成功信息:成功

Promise代码量看著多只是为了让读者更直观,采用ES6箭头函数跟缩写名称可以压缩成下面这样

const promise = new Promise ((resolve, reject) => {
  true ? resolve ('success') : reject ('error');
});

promise.then (res => res, err => err);

return返回值

then方法会隐性返回一个Promise或者开发者显性返回任何值,而Promise有自己内部机制处理:

Promise状态改变前

  • 如果回调函数中返回一个值或者不返回:

    • then返回resolved状态Promise,并且将返回的值或者undefined作为参数值.
  • 如果回调函数抛出错误:

    • then返回rejected状态Promise,并且将抛出的错误作为参数值.
  • 如果回调函数返回一个Promise:

    • Promise是pending状态,then返回pending状态Promise,但是两者的终态还会相同的,并且将回调函数Promise的参数值作为它的参数值;
    • Promise是resolved状态,then返回resolved状态Promise,并且将回调函数Promise的参数值作为它的参数值;
    • Promise是rejected状态,then返回rejected状态Promise,并且将回调函数Promise的参数值作为它的参数值;

Promise状态改变后

前面说过状态一旦更改是不可逆,即使之后再添加回调函数也只是返回同个结果

resolved状态

即使后面再抛出错误then也是返回resolved状态Promise;

const promise = new Promise ((resolve, reject) => {
  resolve ();
  throw 'error';
});

promise.then (
  res => {
    console.log (`resloved: ${res}`);
  },
  err => {
    console.log (`rejected: ${err}`);
  }
);

//输出
//resloved: undefined

rejected状态

即使后面再显性返回resolved状态Promise也是返回rejected状态Promise;

const promise = new Promise ((resolve, reject) => {
  reject ();
  return Promise.resolve ();
});

promise.then (
  res => {
    console.log (`resloved: ${res}`);
  },
  err => {
    console.log (`rejected: ${err}`);
  }
);

//输出
//rejected: undefined

例子

基本条件如下:

  1. p1设定三秒后改变状态
  2. p2设定一秒后改变状态
  3. p1作为p2的返回参数传递

p1返回resolved状态,p2返回resolved状态.

//开始执行时间
console.log (`开始运行时间: ${new Date ().toTimeString ()}`);
const p1 = new Promise ((resolve, reject) => {
  //3秒后执行
  setTimeout (resolve, 3000);
}),
  p2 = new Promise ((resolve, reject) => {
    //1秒后执行
    setTimeout (() => resolve (p1), 1000);
  });

//处理时间
p2.then (
  res => {
    console.log (`p2返回成功状态时间: ${new Date ().toTimeString ()}`);
  },
  err => {
    console.log (`p2返回失败状态时间: ${new Date ().toTimeString ()}`);
  }
);

//输出
//开始运行时间: 09:27:44 GMT+0800 (中国标准时间)
//p2返回成功状态时间: 09:27:47 GMT+0800 (中国标准时间)

p1返回rejectd状态,p2返回resolved状态.

//开始执行时间
console.log(`开始运行时间: ${new Date().toTimeString()}`);
const p1 = new Promise((resolve, reject) => {
    //3秒后执行
    setTimeout(reject, 3000);
  }),
  p2 = new Promise((resolve, reject) => {
    //1秒后执行
    setTimeout(() => resolve(p1), 1000);
  });

//处理时间
p2.then(
  res => {
    console.log(`p2返回成功状态时间: ${new Date().toTimeString()}`);
  },
  err => {
    console.log(`p2返回失败状态时间: ${new Date().toTimeString()}`);
  }
);

//输出
//开始运行时间: 09:30:50 GMT+0800 (中国标准时间)
//p2返回失败状态时间: 09:30:53 GMT+0800 (中国标准时间)

p1返回resolved状态,p2返回rejectd状态.

//开始执行时间
console.log (`开始运行时间: ${new Date ().toTimeString ()}`);
const p1 = new Promise ((resolve, reject) => {
  //3秒后执行
  setTimeout (resolve, 3000);
}),
  p2 = new Promise ((resolve, reject) => {
    //1秒后执行
    setTimeout (() => reject (p1), 1000);
  });

//处理时间
p2.then (
  res => {
    console.log (`p2返回成功状态时间: ${new Date ().toTimeString ()}`);
  },
  err => {
    console.log (`p2返回失败状态时间: ${new Date ().toTimeString ()}`);
  }
);

//输出
//开始运行时间: 09:31:33 GMT+0800 (中国标准时间)
//p2返回失败状态时间: 09:31:34 GMT+0800 (中国标准时间)

看完三个例子之后还有一个省略代码可以得出结论:

p1 p2 then 间隔(s)
resolved resolved resolved 3
rejectd resolved rejectd 3
resolved rejectd rejectd 1
rejectd rejectd rejectd 1

结论:then回调函数的Promise状态首先取决于调用函数(p2)状态,当它resolved(已成功)情况下才取决于返回函数Promise(p1)的状态.

catch

如果Promise状态变为rejected或者then方法运行中抛出错误,都可以用catch捕捉并且返回一个新的Promise实例,建议then方法省略错误处理,最底层添加一个catch处理机制;

const promise = new Promise((resolve, reject) => {
  throw 'error';
});

//省略then错误处理
promise.then().catch(res => {
  console.log(res);
});

//输出
//error

注意:如果决定使用catch处理的话前面就不能用reject做错误处理了,因为被拦截之后是不会再经过catch了

const promise = new Promise((resolve, reject) => {
  throw 'error';
});

promise
  .then(null, err => {
    console.log(`then: ${err}`);
    return err;
  })
  .catch(err => {
    //跳过
    console.log(`catch: ${err}`);
  })
  .then(
    res => {
      console.log(`resloved: ${res}`);
    },
    err => {
      console.log(`rejected: ${err}`);
    }
  );

//输出
//then: error
//resloved: error

从结果可以看到第一个then有了错误处理函数之后会跳过catch方法,然后第二个then会在成功处理函数里打印?说好的状态一旦更改是不可逆呢???
这里面又涉及到return值的问题了.

第一层then已经做了错误处理,返回一个resloved状态Promise;
因为catch不会处理resloved状态Promise所以跳过;
第二层then接收并在resloved处理函数处理;

如果记性好的人应该记得上面讲解例子有个类似的写法但是却依然报错,知道原因么?

const promise = new Promise(function(resolve, reject) {
  throw 'error';
});

//有rejected处理函数
promise.then(
  function resolved(res) {
    console.log(res);
  },
  function rejected(err) {
    console.log(`捕捉错误:${err}`);
  }
);

//没有rejected处理函数
promise.then(function resolved(res) {
  console.log(res);
});

//捕捉错误:error
//系统报错

再重复一遍,Promise每次都会返回一个新的Promise实例,所以不能用链式调用后的结果来看待变量promise.

静态方法

Promise还提供多个方法控制流程:

方法 作用
resolve 快速返回一个新的 Promise 对象,状态为resolved的实例
reject 快速返回一个新的 Promise 对象,状态为rejected的实例
finally 不管 Promise 对象最后状态如何,都会执行的操作,回调函数不接受任何参数这表明finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。该方法是 ES2018 引入标准的,就不说了

all

The Promise.all(iterable) method returns a single Promise that resolves when all of the promises in the iterable argument have resolved or when the iterable argument contains no promises. It rejects with the reason of the first promise that rejects.

大概意思就是当迭代器参数里的所有Promise都返回成功状态或者没有Promise入参的时候返回一个成功状态Promise,否则返回第一个失败状态的Promise抛出的错误.
如果迭代器参数不是Promise也会被隐性调用Promise.resolve方法转成Promis实例.(Promise.all方法的参数必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例).

//成功
const p1 = new Promise((resolve, reject) => {
    resolve('suc1');
  }),
  //成功
  p2 = new Promise((resolve, reject) => {
    resolve('suc2');
  }),
  //失败
  p3 = new Promise((resolve, reject) => {
    reject('err');
  });

Promise.all([p1, p2]).then(res => {
  console.log(`全部成功:${res}`);
});

Promise.all([p1, p3]).then(null, err => console.log(`第一个失败:${err}`));

Promise.all([]).then(
  res => console.log('空数组返回成功Promise'),
  err => console.log('空数组返回失败Promise')
);

//输出
//空数组返回成功Promise
//全部成功:  (2) ["suc1", "suc2"]
//第一个失败: err

注意: 如果空数组情况下立马返回成功状态,尽管放在最后位置却第一个打印结果!

race

跟all类似,区别在于两点:

  • 它只返回第一个改变Promise状态的结果,不管成功失败都只返回一个结果;
  • 空数组情况下永远不会执行;
//成功
const p1 = new Promise((resolve, reject) => {
    resolve('suc1');
  }),
  //成功
  p2 = new Promise((resolve, reject) => {
    resolve('suc2');
  }),
  //失败
  p3 = new Promise((resolve, reject) => {
    reject('err');
  });

Promise.race([p1, p2]).then(res => {
  console.log(`第一个返回成功:${res}`);
});

Promise.race([p3, p2]).then(null, err => console.log(`第一个返回失败:${err}`));

Promise.race([]).then(
  res => console.log('空数组返回成功Promise'),
  err => console.log('空数组返回失败Promise')
);

//输出
//第一个返回成功:suc1
//第一个返回失败:err

使用要点

因为Promise的then方法会把函数结果放置到微任务队列(micro tasks),也就是当次事件循环的最后执行.如果你使用的是同步函数实际上是被无端延迟执行了.如果不清楚这方面内容可以再看一下我之前写得Javascript执行机制--单线程,同异步任务,事件循环

function sync() {
  console.log('sync');
}

function async() {
  setTimeout(() => console.log('async'), 1000);
}

Promise.resolve().then(sync);
Promise.resolve().then(async);
console.log('end');

//输出
//end
//sync
//async

实际上有两种方法可以实现让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API.

匿名函数立即执行new Promise()

function sync() {
  console.log('sync');
}

function async() {
  setTimeout(() => console.log('async'), 1000);
}

(() => new Promise(resolve => resolve(sync())))();

(() => new Promise(resolve => resolve(async())))();

console.log('end');

// 输出
// sync
// end
// async

async,如果不知道没关系,下面有讲到

function sync() {
  console.log('sync');
}

function async() {
  setTimeout(() => console.log('async'), 1000);
}
(async () => sync())();
(async () => async())();
console.log('end');

// 输出
// sync
// end
// async

优劣点

优点:

  • 异步流程能使用线性写法链式调用,摆脱“回调地狱”;
  • 多种方式的错误处理机制;
  • 能将当前结果往下层层传递;
  • promise是一种规约,它在回调调用和错误处理规范化的基础上给异步编程提供了更多的可能性;

缺点:

  • 一旦开始无法停止取消;
  • 尽管可以用catch做统一错误处理,但是被中间层拦截了之后可能会造成干扰;
  • 只能知道Promise在实例pending(进行中)、fulfilled(已成功)和rejected(已失败)状态中的一种,没有更加详细的进度信息;
  • 尽管比回调函数稍好,但是写法也十分冗余,过多的then让代码语义化不明;
  • 每次都是返回新的Promise实例,牺牲性能代价略高;

Generator

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

原理

语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态.执行 Generator 函数并不执行里面程序而是返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态.yield表达式是暂停执行的标记,而next方法可以恢复执行.
每一次调用next方法,都会返回数据结构的当前成员的信息.具体来说,就是返回一个包含value和done两个属性的对象.其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束.

特征

  • function关键字与函数名之间有一个星号就行,哪个位置都能通过;
  • 函数体内部使用yield表达式定义不同的内部状态;
function* Generator() {
  yield '1';
  return '2';
}

const example = Generator();
console.log(example);
console.log(example.next());
console.log(example.next());

// 输出
// Object [Generator] {}
// { value: '1', done: false }
// { value: '2', done: true }

next表达式运行逻辑

yield表达式本身没有返回值,或者说总是返回undefined.next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值.否则上一个yield表达式的返回值维持原状不变,这个特性相当重要,我们就依赖它从外部向内部注入不同的值,从而调整函数行为.调用next的时候,内部指针会往下运行直到遇到下列情况:

  • 遇到yield表达式,它后面的表达式只有内部指针指向该语句时才会执行,并将它的值作为返回的vaule,然后暂停执行;
  • 遇到return表达式,将它的值作为返回的vaule,遍历结束;
  • 运行到函数结束,将undefined作为返回的vaule,遍历结束;
  • 2和3之后仍然可以继续调用next,结果都是{value: undefined, done: true},即使return后面还有yield表达式也不会改变结果;

然后我们根据规则再去解读那一段代码的运行结果

function* foo() {
  let x = yield 1;
  console.log(`函数内部x: ${x}`);

  let y = x * (yield x + 2);
  console.log(`函数内部xy: ${x} ${y}`);

  return x + y;
}

const it = foo();
console.log(it.next(2));
console.log(it.next(3));
console.log(it.next(4));

// 输出
// 函数内部x: undefined
// Object { value: 1, done: false }
// 函数内部x: 3
// Object { value: 5, done: false }
// 函数内部xy: 3 12
// Object { value: 15, done: true }
  • 第一次next: 执行var x = yield 1后停止,因为没有上一个yield表达式,所以入参2不起效,输出1;
  • 第二次next: 执行yield x + 2后停止,x等于入参3,输出5;
  • 第三次next: 执行return x + y后停止,上一个yield表达式var y = 3 * 4(x+2被入参4取代了),输出15;

概念其实很清晰,过程有点复杂,大家谨记next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值.否则上一个yield表达式的返回值维持原状不变,然后根据这个规则来算就明白了.

yield* 表达式运行逻辑

Generator函数内部,调用另一个Generator函数,默认情况下是没有效果的,需要用到yield表达式,带*会返回值,不带*返回遍历器对象.

function* foo() {
  bar(1);
  //内部调用Generator函数
  yield bar(2);
  yield* bar(3);
  yield 1;
}

function* bar(num) {
  console.log(num);
  yield 2;
}

const it = foo();
console.log(it.next());
console.log(it.next());
console.log(it.next());

// 输出
// { value: Object [Generator] {}, done: false }
// 3
// { value: 2, done: false }
// { value: 1, done: false }

从输出结果可知只有yield * bar(3)生效.
如果被代理的 Generator 函数有return语句,那么就可以向代理它的Generator函数返回数据.

function* genFuncWithReturn() {
  yield 'a';
  yield 'b';
  return 'c';
}

function* logReturned(genObj) {
  const result = yield* genObj;
  //返回值
  console.log(result);
}

const it = logReturned(genFuncWithReturn());
console.log(it.next());
console.log(it.next());
console.log(it.next());

// 输出
// { value: 'a', done: false }
// { value: 'b', done: false }
// c
// { value: undefined, done: true }

易错点

Generator函数不用yield表达式就只剩一个暂缓执行的作用;

function* foo() {
  console.log(1);
  console.log(2);
  return;
}

const it = foo();
console.log(3);
it.next();

// 输出
// 3
// 1
// 2

非Generator函数中使用yield表达式会句法错误;

function foo() {
    yield 1;
}

const it = foo();

// 输出
// Unexpected number

yield表达式如果用在另一个表达式之中,必须放在圆括号里面,除非用作函数参数或放在赋值表达式的右边

function* foo() {
  console.log('函数内部x: ' + (yield));
  yield 1;
}

const it = foo();
console.log(it.next());
console.log(it.next(3));

// 输出
// { value: undefined, done: false }
// 函数内部x: 3
// { value: 1, done: false }

原型方法

throw

Generator函数返回的遍历器对象的throw方法(不是全局的throw方法)可以在函数体外抛出错误,这意味著出错的代码与处理错误的代码,实现了时间和空间上的分离.然后在Generator函数体内被捕获后,会附带执行下一条yield表达式.

const err = function*() {
  try {
    yield;
  } catch (e) {
    console.log(`内部捕获: ${e}`);
  }
};
//执行
const it = err();
console.log(it.next());

//外部捕获错误
try {
  //两次抛出错误
  console.log(it.throw('err1'));
  console.log(it.throw('err2'));
} catch (e) {
  console.log(`外部捕获: ${e}`);
}

// 输出
// { value: undefined, done: false }
// 内部捕获 err1
// { value: undefined, done: true }
// 外部捕获 err2

所以这里实际执行顺序it.throw('err1') ⇨ it.next() ⇨ it.throw('err2')
同时,Generator函数体内抛出的错误,也可以被函数体外的catch捕获;

const err = function*() {
  yield '1';
  throw 'err';
  yield;
  yield '2';
};
//执行
const it = err();
console.log(it.next());

try {
  console.log(it.next());
} catch (e) {
  console.log('外部捕获', e);
}
//捕捉错误之后继续执行
console.log(it.next());

// 输出
// { value: '1', done: false }
// 外部捕获 err
// { value: undefined, done: true }

注意最后一次执行next函数返回{value: undefined, done: true},因为抛出错误后没有做内部捕获,JS引擎会认为这个Generator函数已经遍历结束.

return

返回给定的值或者不传默认undefined,并且终结遍历 Generator 函数.

function* Generator() {
  yield '1';
  yield '2';
  yield '3';
  return '4';
}

const it = Generator();
console.log(it.return(2));
console.log(it.next(3));

// 输出
// { value: 2, done: true }
// { value: undefined, done: true }

实践写法

//模拟ajax请求
function ajax() {
  return new Promise((resolve, reject) => {
    setTimeout(
      () =>
        resolve({
          abc: 123,
        }),
      1000
    );
  });
}

function* foo() {
  try {
    yield;
  } catch (err) {
    console.log(`内部捕获: ${err}`);
  }
  yield console.log('再次执行');
  yield ajax();
  return res;
}

const it = foo();
it.next();
it.throw('somethings happend');
it.next().value.then(res => {
  console.log(res);
});

// 输出
// 内部捕获: somethings happend
// 再次执行
// { abc: 123 }

暂停执行和恢复执行,这是Generator能封装异步任务的根本原因,强大的错误捕捉能力可以在写法上更加自由.
我们可以在上面的代码扩展出更多步骤,例如这种异步依赖的代码可以以同步顺序写出来,或者外层操作res1之后再传到ajax2等.

function* foo() {
  var res1 = yield ajax1(),
    res2 = yield ajax2(res1);

  return res;
}

尽管Generator异步流程管理非常简洁,但是操作流程不算方便,需要开发者决定什么时候执行下一步,甚至你会发现在整个异步流程会充斥著多个next的身影.

自动遍历方法

for...of循环可以自动遍历 Generator 函数时生成的Iterator对象,一旦next方法的返回对象的done属性为true,循环就会中止,且不包含该返回对象,所以return语句会执行,但不会显示输出.

function* Generator() {
  yield '1';
  yield '2';
  yield '3';
  return '4';
}

for (let v of Generator()) {
  console.log(v);
}

// 输出
// 1
// 2
// 3

也可以用while实现,区别在于for...of循环返回值,while返回数据结构

function* Generator() {
  yield '1';
  yield '2';
  yield '3';
  return '4';
}

let it = Generator(),
  result = it.next();

while (!result.done) {
  console.log(result);
  result = it.next(result);
}

// 输出
// { value: '1', done: false }
// { value: '2', done: false }
// { value: '3', done: false }

异步衍生库co

上面方法不适用于异步操作,如果之间需要依赖关系的话可能会导致后续执行失败,为了实现这种效果有些很出名的衍生库co等,关键源码很短也比较简单,直接贴出来了.

/**
 * Execute the generator function or a generator
 * and return a promise.
 *
 * @param {Function} fn
 * @return {Promise}
 * @api public
 */

function co(gen) {
  var ctx = this;
  var args = slice.call(arguments, 1);

  // we wrap everything in a promise to avoid promise chaining,
  // which leads to memory leak errors.
  // see https://github.com/tj/co/issues/180
  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);

    onFulfilled();

    /**
     * @param {Mixed} res
     * @return {Promise}
     * @api private
     */

    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);
      return null;
    }

    /**
     * @param {Error} err
     * @return {Promise}
     * @api private
     */

    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    /**
     * Get the next value in the generator,
     * return a promise.
     *
     * @param {Object} ret
     * @return {Promise}
     * @api private
     */

    function next(ret) {
      if (ret.done) return resolve(ret.value);
      var value = toPromise.call(ctx, ret.value);
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      return onRejected(
        new TypeError(
          'You may only yield a function, promise, generator, array, or object, ' +
            'but the following object was passed: "' +
            String(ret.value) +
            '"'
        )
      );
    }
  });
}

完整代码请看co,具体用法就不说了

优劣点

优点

  • 一种新的异步编程概念,ES6对协程的不完全实现,使用同步的方式处理异步操作,改写回调函数;
  • 出色的控制流管理,Generator内部可以使用yield表达式定义不同的内部状态并且惰性执行;
  • yield表达式具备记忆功能,next可以从上一次暂停点开始执行;
  • 通过next入参和返回值可以交换数据影响执行函数;
  • Generator函数体外抛出的错误,可以在函数体内捕获;反过来,Generator函数体内抛出的错误,也可以被函数体外的catch捕获;

缺点

  • 相对复杂,需要了解更多的知识点,怎么正确使用Generator有很多注意地方,具体详情自行查看文档;
  • 流程管理不够灵活,衍生的co库等都有些不足;
  • 牺牲性能代价比较高;

Async Await

ES2017 标准引入了 async 函数,一句话,它就是 Generator 函数的语法糖.
如果说 Promise 主要解决的是异步回调问题,那么 async + await 主要解决的就是将异步问题同步化,降低异步编程的认知负担.

特征

将Generator的*换成async,yeild换成await;

内置处理器,自动执行

一旦遇到await就会先返回,等到异步操作完成,再接著执行函数体内后面的语句

function p1() {
  return Promise.resolve().then(res => console.log('p1'));
}

function p2() {
  return Promise.resolve().then(res => console.log('p2'));
}

async function Async() {
  console.log(1);
  await p1();
  console.log(2);
  await p2();
}

const it = Async().then(res => console.log(123));

// 输出
// 1
// p1
// 2
// p2
// 123

如果await命令后面是非Promise对象会被强制转换成promise对象

async function Async() {
  return await 1;
}
Async().then(res => console.log(res));

// 输出
// 1

如果await命令后面的Promise对象为reject状态,就会中断整个Async函数

async function Async() {
  await Promise.reject('err');
  console.log('不执行了');
  await Promise.resolve();
};
Async().catch(err => console.log(err));

// 输出
// err

使用要点

联合Promise.all在遍历使用

//阻塞主线程
const delazy = (time = 2000) =>
  new Promise(resolve => {
    setTimeout(() => {
      resolve();
    }, time);
  });

//模拟请求
async function quest() {
  await delazy();
  return Promise.resolve({
    str: '123',
  });
}

//统一获取
async function doSomethings() {
  const ary = await Promise.all([quest(), quest()]);
  console.log(ary.map(e => e.str));
}

doSomethings();

为了保证不因为某个环节失败中断整个方法,建议都放在try..catch中

async function Async() {
  try {
    await Promise.reject('err').then(null, err => console.log(err));
    console.log('继续执行');
    await Promise.resolve('suc').then(res => console.log(res));
  } catch (err) {}
}
Async();

// 输出
// 继续执行
// suc

没有依赖关系的方法不要使用await

function p1() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000);
  });
}

function p2() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(2), 1000);
  });
}

async function sum() {
  const [a, b] = await Promise.all([p1(), p2()]);
  console.log(a, b);
}
==================== OR ============================
async function sum() {
  const a = p1(),
    b = p2(),
    c = await a,
    d = await b;

  console.log(c, d);
}

sum();

// 输出
// 1 2

总结:

  • 如果想要跳过错误继续执行的解决办法可以使用try..catch或者catch;
  • 没有依赖关系的异步操作可以直接用await后面接Promise.all()提高性能;

实现原理(全部代码源自阮一峰大神的博客)

跟上面说的co类似,将Generator函数和自动执行器,包装在一个函数里.

async function fn(args) {}
//等价于
function fn(args) {
  return spawn(function*() {});
}

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函数的await命令后面,可以是Promise对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作);
  • async函数的返回值是Promise;

缺点

  • 只要一个await语句后面的Promise变为reject,那么整个async函数都会中断执行;
  • 错误处理机制复杂;
  • 牺牲性能代价更高;
阅读 853

努力去做,对的坚持,静待结果

504 声望
42 粉丝
0 条评论

努力去做,对的坚持,静待结果

504 声望
42 粉丝
文章目录
宣传栏