什么是异步

所谓”异步”,简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。比如,有一个任务是读取文件进行处理,异步的执行过程就是下面这样。

上图中,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。

这种不连续的执行,就叫做异步。相应地,连续的执行,就叫做同步。

上图就是同步的执行方式。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。


JS诸多异步方法其实都是为了实现异步,并把异步同步化(这里的同步化指的是代码层面上

什么是iterator(迭代器/遍历器)

为某种数据结构提供的一种统一的遍历机制

Iterator的遍历过程如下:

  1. 创建一个指针对象,指向当前数据结构的起始位置。遍历器对象本质上是一个指针对象
  2. 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员
  3. 不断调用next方法,直至指向数据结构的结束位置

每次调用next方法,返回一个包含数据结构当前成员信息的对象,指针移动

数据结构当前成员信息的对象有两个属性:valuedone,value是当前成员的值,done是布尔型值,用于标识遍历是否结束

模拟next方法例子:

var it = makeIterator(['a', 'b']);

it.next() // { value: 'a', done: false }
it.next() // { value: 'b', done: false }
it.next() // { value: undefined, done: true }

function makeIterator(array) {
    var nextIndex = 0;
    return {
        next: function() {
            return nextIndex < array.length ?
             { value: array[ nextIndex++ ], done: false } :
             { value: undefined, done: true };
        }
    }
}

那For…of循环过程中做了什么?

For…of循环实际上是自动寻找被遍历数据结构的Iterator接口,循环内部调用的是数据结构的Symbol.iterator方法

部分数据结构原生具有Iterator接口,如Array, Map, Set, String, TypedArray, 函数的arguements对象, NodeList对象,而有的数据结构没有原生的Iterator接口,如普通Object

但无论哪种数据结构,ES6规定数据结构默认的Iterator接口部署在数据结构的Symbol.iterator属性,调用Symbol.iterator方法会得到当前数据结构默认的遍历器生成函数

const arr = [];
arr[Symbol.iterator]();  //Array Iterator {}

const obj = {}
obj[Symbol.iterator]() 
//Uncaught TypeError: obj[Symbol.iterator] is not a function

generator实现机制——协程

传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做”协程”(coroutine),意思是多个线程互相协作,完成异步任务。

协程是具有以下功能的函数:

  • 可以暂停执行(暂停的表达式称为暂停点);
  • 可以从挂起点恢复 
(保留其原始参数和局部变量)

协程有点像函数,又有点像线程。它的运行流程大致如下。

第一步,协程A开始执行。

第二步,协程A执行到一半,进入暂停,执行权转移到协程B。

第三步,(一段时间后)协程B交还执行权。

第四步,协程A恢复执行。

上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。

举个具体的例子:

function* A() {
  console.log("我是A");
  yield B(); // A停住,在这里转交线程执行权给B
  console.log("结束了");
}
function B() {
  console.log("我是B");
  return 100;// 返回,并且将线程执行权还给A
}
let gen = A();
gen.next();
gen.next();

// 我是A
// 我是B
// 结束了

在这个过程中,A 将执行权交给 B,也就是 A 启动 B,我们也称 A 是 B 的父协程。因此 B 当中最后return 100其实是将 100 传给了父协程。

需要强调的是,对于协程来说,它并不受操作系统的控制,完全由用户自定义切换,因此并没有进程/线程上下文切换的开销,这是高性能的重要原因。

它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。

Generator函数和yield是什么

直译的话就是生成器函数,这么起名当然是有他的道理,他生成的是什么?是一个装有异步任务的容器

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)

function* test(x){
  var y = yield x + 2;
  return y;
}

上面代码就是一个 Generator 函数。它不同于普通函数,是可以暂停执行的,所以函数名之前要加星号,以示区别。

整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。

Generator函数不会像普通函数一样直接返回return值,而是返回一个内部指针对象(我个人不是很喜欢这个说法,这个说法源于阮一峰的异步入门博客)

const z = test(1)  //这里把赋值执行后返回的指针对象赋给z(方便表达)
z.next()     //{value: 3, done: false}
z.next()     //{value: undefined, done: true}

需要调用指针对象的next属性去返回当前阶段的信息( value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。

我理解为:next返回的是这一个yield关键字所紧接的表达式所返回的状态(交换执行权的后果),value表示这个状态处理后的值,done表示在value为undefined之前还是否有下一个yield关键字(停顿)。

还有个骚操作:next 方法返回值的 value 属性,是 Generator 函数向外输出数据;next 方法还可以接受参数,这是向 Generator 函数体内输入数据。

const z = test(1)  //这里把赋值执行后返回的指针对象赋给z(方便表达)
z.next()     //{value: 3, done: false}
z.next(4)     //{value: 4, done: true}  ---> y === 4

骚操作2.0:Generator函数内部部署错误处理代码,还可以捕获函数外部的错误

function* gen(x){
  try {
    var y = yield x + 2;
  } catch (e){ 
    console.log(e);
  }
  return y;
}

var g = gen(1);
g.next();     //{value: 3, done: false}
g.throw('出错了');   //抛出错误
// 出错了  ---> 通过内部的console.log指令输出,表示内部捕获到函数外部抛出的错误

上面代码的最后一行,Generator 函数体外,使用指针对象的 throw 方法抛出的错误,可以被函数体内的 try … catch 代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。

async/await是什么?

JavaScript 中的 async/await 是 AsyncFunction 特性 中的关键字。其中async是一个声明异步函数的关键字,令被作用的函数成为异步函数,返回Promise对象,如果你返回的是一个直接量(比如下面的字符串),async会把这个直接量通过Promise.resolve()封装成Promise对象

async function test(){
    return "Hello async"
}
const res = test();
console.log(res)   //Promise {<fulfilled>: "Hello async"}

你会发现上面返回的promise对象有一个[[PromiseState]] === "fulfilled",后面才是紧接着[[PromiseResult]] === "Hello async"

async/await和generator/yield的关系是什么

前者是后者的体现,后者是前者的原理(async/await可以用generator/yield去实现)

虽然是这么说,但是他们可以说是两个东西

前者的关键是异步的自动执行——>await的特性,当await关键字后面的表达式执行完毕之后代码会继续向下执行,并且返回的是Promise对象

后者的关键是执行权的交换(暂停执行)——>函数(异步的体现)需要手动执行,不论yield的节点是否出现在Generator函数中,函数不会像普通函数一样执行,而是需要手动next去执行,从而实现异步操作

实际上async/await是对generator/yield进行了多一层的迭代处理和增加了一个错误的捕获步骤还有加了个Promise对象的return,详情可以查看babel的对比

Promise对象又是什么

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise对象有以下两个特点。

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:

1、pending(进行中)

2、fulfilled(已成功)

3、rejected(已失败)

只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

注意,为了行文方便,本章后面的resolved统一只指fulfilled状态,不包含rejected状态。

用法

ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。

下面代码创造了一个Promise实例。

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);  //异步操作成功调用value函数
  } else {
    reject(error);  //异步操作不成功调用Error函数
  }
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

then是什么

要了解then是什么先要知道他为什么要存在,then是使用在Promise对象当中的,Promise对象只能针对resolvereject分别设置一个回调函数,那问题来了,那我怎么能在Promise对象执行中执行多个回调函数呢

这就是then的作用——对Promise实现链式调用。

then就相当于隐式的去new Promise,使得Promise对象可以链式调用(重复执行下去跟个二叉树一样…)

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。

Promise的缺点

1、无法取消,新建即执行,无法中途取消

2、如果不设置回调函数,Promise内部的错误不会抛到外部

3、当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。