JavaScript系列 -- Promise、Generator、async及await

669 阅读21分钟

前言

同步和异步的最大区别在于:

  • 同步会阻塞,上面的代码执行完成后下面的代码才会执行
  • 异步不会阻塞,上面的代码执行未完成下面的代码就已经开始执行了 我们知道 JS 是单线程语言,所以如果存在运行时间比较长的任务(如:定时器、DOM事件、异步请求等),如果把这些任务视为同步任务,那将会造成很严重的“阻塞现象”,所以浏览器把这些任务归为异步任务,JS 引擎线程遇到这些异步任务时,会通知其他线程(如:定时器线程、事件处理线程、异步请求线程等)去做,最后只需要把最终结果反馈 JS 引擎线程就行

好,那这个通知反馈 怎么做呢?

于是就有了JavaScript的异步编程方式:Promise、Generator、async / await

我们来看看这些方式是怎么完成这项工作的:

Promise

Promise 的基本思想

回调地狱: D716245785261F43A6C111F2C590DED0.png 利用 Promise 解决回调地狱: 12F19B75F08C8284471A4B0AD64EC570.png

Promise 是什么?

在控制台打印出 Promise ,结果可以看到是 [native code] ,说明是官方封装的函数,代码是本地的

image.png 在控制台输出 Promise.prototype ,结果可以看到有 then 、catch 、finally 等方法

image.png 综上,我们把 Promise 当做是构造函数,使用 new 关键字创建 Promise 的一个最简单的实例对象 p:

var p = new Promise((resolve,reject)=>{
    if(1) resolve("异步任务完成")
    else reject("异步任务失败")
})

在控制台输出实例对象 p

image.png 我们在实例对象 p 的原型__proto__属性里面找到了 Promise.prototype,所以我们才能让实例对象 p 使用 then / catch / finally 等方法

Promise 的通知与反馈过程

通知

var p = new Promise((resolve,reject)=>{
    if(1) resolve("异步任务完成")
    else reject("异步任务失败")
})

以上就是 Promise 的通知过程,Promise 里面是一个箭头函数,传入参数是 resolve 和 reject,{ }里面运行的代码是 resolve() 和 reject() 两个调用其他函数的操作。官方规定:写在前面的参数表示“任务完成”,写在第二个参数表示“任务失败”。所以由此可见:这两个参数的名称是可以自定的,只不过我们为了更加语义化才采用 resolve 和 reject。

状态

  1. 我们在控制台输出实例对象 p,显示的是一个 Promise 对象,对象的属性里发现除了原型还有 [[PromiseState]][[PromiseResult]]两个属性,顾名思义分别表示状态结果,其中的状态的值是fulfilled,而结果的值就是我们刚刚 Promise 里面箭头函数里写的 resolve() 括号的实参"异步任务完成"

image.png 2. 我们修改一下 Promise 函数,控制其任务失败:

var p = new Promise((resolve,reject)=>{
    if(0) resolve("异步任务完成")
    else reject("异步任务失败")
})

image.png 3. 其中[[PromiseResult]]就是我们刚刚 Promise 里面箭头函数里写的 reject() 括号的实参"异步任务失败",而[[PromiseState]]变成了rejected

这说明了:我们的通知过程中 Promise 所做的背后工作(原理)就是:执行 Promise 里面的箭头函数时会把该实例对象 p (一个Promise对象)里的[[PromiseState]]属性的值进行改变:

  • 若是执行第一个参数(resolve)对应的函数(resolve())的话,就把这个属性的值改为 fulfilled;(源码就是这个道理)
  • 若是执行第二个参数(reject)对应的函数(reject())的话,就把这个属性的值改为 rejected
  1. 那这个属性原来的值是什么,我们用 setTimeout() 作一下延时,才能看得到:

image.png

  1. 可以看到这个属性原来的值是 pending

反馈

  1. 我们对实例对象 p 使用其继承过来的 then 方法:(针对的是任务完成的例子)

image.png 可以看到调用 p.then() 后还是返回一个 Promise 对象,并且这个对象的状态变为fulfilled结果变为undefined(因为我没有调用其他函数并传参)

  1. 我们针对任务失败的例子使用 then() 方法:

image.png

  1. 可以发现状态还是 rejected,也没有打印 res,说明 p.then() 并没有执行,我们在对这个返回的 Promise 对象使用 catch() 方法:

image.png 9. 可以看到调用 p.then().catch() 后还是返回一个 Promise 对象,并且这个对象的状态变为fulfilled结果变为undefined,还把 err 给打印出来了

  1. 这就是实例对象 p 可以采用链式调用的本质原因,也证明了为什么任务失败只会执行catch()方法,而任务完成执行了then()方法后还会继续执行链上下一个then()方法,一直走到catch()不执行退出的原因
  • then() 方法的入口钥匙[[PromiseState]]属性的值是fulfilled
  • catch() 方法的入口钥匙[[PromiseState]]属性的值是rejected。(源码就是这个道理)

image.png (任务完成的 p)

image.png (任务失败的 p)

总结

  • 使用 Promise 解决异步问题的步骤:
  1. 利用构造函数 Promise 创建实例对象;
  2. 在 构造函数里面写个箭头函数,传入两个参数,一个代表任务完成,一个代表任务失败;
  3. 在箭头函数里执行异步任务,执行完成后调用以这两个为名称的函数,可把执行结果做实参传过去;
  4. Promise()里的箭头函数的异步任务执行完毕后会返回一个 Promise 对象,这个对象已经赋值给了实例对象 p;p 可以使用从 Promise.prototype 继承过来的 then() / catch() 等方法
  5. p 执行这些方法前会看[[PromiseState]]这个属性的值看是否进入当前方法,进去之后出来[[PromiseState]][[PromiseResult]]两个属性的值都可能发生改变;
  6. 每次执行完后都还是返回一个 Promise 对象

明白了这些本质之后,对于 Promise 的源码的基本框架就大概知道是什么了(虽然现在还可能很难看懂hhh),以及后面的各种用法、面对各种场景也都比较有把握了。

上面说了 Promise 的本质性的东西,接下来我们研究一下 Promise 的源码,是如何实现“通知” 和 “反馈” 的,想直接跳过的同学 点击这里

Promise((resolve,reject)=>{}) 的实现原理

  • 核心:
    • 返回 Promise 对象
    • 怎么实现:任务成功,状态置为"fulfilled",任务失败,状态置为"rejected",并把 value 值返回
    • 怎么实现:异步任务完成后不会里面反馈结果,而是放置一个微任务队列,等调用 then() 方法才会执行

来自 神三元同学的做法

class MyPromise {
  //传一个异步函数进来
  constructor(excutorCallBack){
    this.status = 'pending';
    this.value = undefined;
    this.fulfillAry = [];
    this.rejectedAry = [];
    //=>执行Excutor
    let resolveFn = result => {
      if(this.status !== 'pending') return;
      let timer = setTimeout(() => {
        this.status = 'fulfilled';
        this.value = result;
        this.fulfillAry.forEach(item => item(this.value));
      }, 0);
    };
    let rejectFn = reason => {
      if(this.status !== 'pending')return;
      let timer = setTimeout(() => {
        this.status = 'rejected';
        this.value = reason;
        this.rejectedAry.forEach(item => item(this.value))
      }, 0)
    };
    try{
      //执行这个异步函数
      excutorCallBack(resolveFn, rejectFn);
    } catch(err) {
      //=>有异常信息按照rejected状态处理
      rejectFn(err);
    }
  }
  then(fulfilledCallBack, rejectedCallBack) {
    //resolve和reject函数其实一个作为微任务,因此他们不是立即执行,而是等then调用完成后执行
    this.fulfillAry.push(fulfilledCallBack);
    this.rejectedAry.push(rejectedCallBack);
    //一顿push过后他们被执行
  }
}

Promise.then().catch() 的实现原理

  • 核心:
    • 怎么实现:任务成功的结果会被then方法捕捉到,错误只被catch捕捉到
    • 怎么实现:链式调用 把上面的 MyPromise 类里的 then() 方法进行修改:
  //then传进两个函数
  then(fulfilledCallBack, rejectedCallBack) {
    //保证两者为函数
    typeof fulfilledCallBack !== 'function' ? fulfilledCallBack = result => result:null;
    typeof rejectedCallBack !== 'function' ? rejectedCallBack = reason => {
      throw new Error(reason instanceof Error? reason.message:reason);
    } : null
    //返回新的Promise对象,后面称它为“新Promise”
    return new Promise((resolve, reject) => {
      //注意这个this指向目前的Promise对象,而不是新的Promise
      //再强调一遍,很重要:
      //目前的Promise(不是这里return的新Promise)的resolve和reject函数其实一个作为微任务
      //因此他们不是立即执行,而是等then调用完成后执行
      this.fulfillAry.push(() => {
        try {
          //把then里面的方法拿过来执行
          //执行的目的已经达到
          let x = fulfilledCallBack(this.value);
          //下面执行之后的下一步,也就是记录执行的状态,决定新Promise如何表现
          //如果返回值x是一个Promise对象,就执行then操作
          //如果不是Promise,直接调用新Promise的resolve函数,
          //新Promise的fulfilAry现在为空,在新Promise的then操作后.新Promise的resolve执行
          x instanceof Promise ? x.then(resolve, reject):resolve(x);
        }catch(err){
          reject(err)
        }
      });
      //以下同理
      this.rejectedAry.push(() => {
        try {
          let x = rejectedCallBack(this.value);
          x instanceof Promise ? x.then(resolve, reject):resolve(x);
        }catch(err){
          reject(err)
        }
      })
    }) ;
  }

有了then方法,catch自然而然调用即可:

  catch(rejectedCallBack) {
    return this.then(null, rejectedCallBack);
  }

测试如下:

var p = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    let num = Math.random()
    console.log(num)
    num < 0.5 ? resolve(100) : reject(-100);
  }, 1000)
})
console.log(p)
p.then(res=>{
  console.log(p)
  console.log(res)
}).catch(err=>{
  console.log(p)
  console.log(err)
})

(异步任务成功)

image.png (异步任务失败)

image.png

Promise().then().catch() 处理多个串行异步任务

比如有个需求 —— 想要获取一个地级市拥有多少个辖区:

  1. 我们现在得先调用【获取当地省份】的接口
  2. 拿到省份id后才能够调用【获取地级市】的接口
  3. 拿到对应地级市的id后才能调用【获取地级市拥有多少个辖区】的接口

其伪代码如下:

ajax('url_1',data1);
ajax('url_2',data2); // 执行之前需要拿到 ajax('url_1') 的结果
ajax('url_3',data3); // 执行之前需要拿到 ajax('url_2') 的结果

按照之前回调函数的写法就是:

ajax('url_1', data1, function (err, result) {
    if (err) {
        return handle(err);
    }
    ajax('url_2', data2, function (err, result) {
        if (err) {
            return handle(err);
        }
        ajax('url_3', data3, function (err, result) {
            if (err) {
                return handle(err);
            }
            return success(result);
        });
    });
});

先不说它的可读性差,如果中间某一环节出现错误,修改起来很麻烦。所以我们用 Promise 来解决这个问题,好处就是 Promise 实现了将多个异步任务串行执行写成同步任务执行顺序

let promise = fn('url_1',data1)
promise.then(data2 => { // 注意这里 promise 不要加括号,它是执行 new Promise(...) 后返回的一个对象
    fn('url_2',data2)
}).then(data3 => {
    fn('url_3',data3)
}).then(res =>{
    console.log(res) // 串行完毕可以得到你想要的结果了
}).catch(err => {
    console.log(err)
})

function fn(url,data){
     return new Promise((resovle,reject)=>{
        ajax(url,data).success(function(res){
            resolve(res)
        })
    })
}

这样代码可读性就好很多了,清晰明了。(其实后面还有更加简洁的方法)

说完串行了,那么并行怎么办? 当有多个异步事件,之间并无联系而且没有先后顺序,只需要全部完成就可以开始工作了。

串行会把每一个异步事件的等待时间进行一个相加,明显会对完成进行一个阻塞。那么并行的话该怎么确定全部完成呢?

Promise.all 与 Promise.race 解决并行问题

Promise.all([promise1,promise2,promise3,...])

  • 接收一个数组,数组的每一项都是一个 promise 对象
  • 当数组中所有的 promise 的状态都达到 resolved 的时候,Promise.all的状态就会变成 resolved
  • 如果其中有一个 promise 的状态变成了 rejected,那么 Promise.all 的状态就会变成 rejected
  • 调用then方法时的结果成功的时候是回调函数的参数也是一个数组按顺序保存着每一个promise对象resolve执行时的值
let promise1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve(1);
    },4000)
});
let promise2 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve(2);
    },3000)
});
let promise3 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve(3);
    },5000)
});
let promiseAll = Promise.all([promise1,promise2,promise3])
promiseAll.then(res=>{
    console.log(res); // [1,2,3] 
}).catch(err => {
    console.log(err);
})

控制台 5s 后输出结果:(执行时间最长的 promise 完成才算完成)

image.png 顺序是[1,2,3],证明与哪个 promise 的状态先变成 resolved 无关

let promise1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        reject(1);
    },4000)
});
let promise2 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve(2);
    },3000)
});
let promise3 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve(3);
    },5000)
});
let promiseAll = Promise.all([promise1,promise2,promise3]);
promiseAll.then(res => {
    console.log(res);
}).catch(err => {
    console.log("任务" + err + "失败了") // 1 说明是 promise1 里的异步任务执行失败了
})

控制台 4s 后输出结果:(其中一个 promise 一失败就退出)

image.png

Promise.all() 的实现原理:

Promise.all = function (promises) {
    let index = 0
    let result = [] // 存放异步任务执行成功的结果
    if (promises.length === 0) resolve(result)
    return new Promise((resolve, reject) => {
        for (let i = 0; i < promises.length; i++) {
            Promise.resolve(promises[i]).then((data) => {
                processValue(i, data) // 拿取任务完成的结果
            }, (err) => {
                reject(err) // 一旦发现错误,立马抛出
                return
            })
        }
        function processValue(i, data) {
            result[i] = data // 把任务完成的结果存入数组中
            if (++index === promise.length) { 
            // 每成功一次计数器就会加1,直到所有任务成功时会与values长度一致,则认定为都成功了
                resolve(result)
            }
        }
    })
}

总结:

  1. 对数组中的每一个 promise 对象调用 then()方法和 catch()方法进行结果检验,一旦发现错误就抛出
  2. 如果当前遍历的 promise 对象的结果是成功的,则放入一个 results 数组
  3. 当 results 数组的长度和原来的 promises 数组长度一致时返回 results 数组

Promise.race([promise1,promise2,promise3,...]) 竞速模式

  • 接受一个每一项都是promise的数组。
  • 但是与all不同的是,第一个promise对象状态变成resolved时自身的状态变成了resolved,第一个promise变成rejected自身状态就会变成rejected。第一个变成resolved的promsie的值就会被使用
let promise1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve(1);
    },4000)
});
let promise2 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve(2);
    },3000)
});
let promise3 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve(3);
    },5000)
});
let promiseRace = Promise.race([promise1,promise2,promise3])
promiseRace.then(res => {
    console.log(res); // 打印出2 因为 promise2 最先完成,其余的就忽略了
}).catch(err => {
    console.log("任务" + err + "失败了");
})

但是在控制台输出 promise1 和 promise3 都显示“任务完成”的状态,也就是说“竞赛”输了的选手也还是会完成这场比赛

Promise.race() 的实现原理

Promise.race = function (promises) {
    let index = 0
    let result = [] // 存放异步任务执行成功的结果
    if (promises.length === 0)  resolve(result)
    return new Promise((resolve, reject) => {
        for (let i = 0; i < promises.length; i++) {
            Promise.resolve(promises[i]).then((data) => {
                resolve(data) // 其中一个任务一有任务完成的结果,立马抛出
            }, (err) => {
                reject(err) // 一旦发现错误,立马抛出
                return
            })
        }
    })
}

总结:

  1. 对数组中的每一个 promise 对象调用 then()方法和 catch()方法进行结果检验
  2. 一旦发现其中一个 promise 对象是任务完成的状态,立马抛出
  3. 一旦发现其中一个 promise 对象是任务失败的状态,立马抛出

Promise.resolve() 与 Promise.reject()

Promise.resolve() 用法

Promise.resolve()的作用是将现有对象转换成状态为 fulfilled 的 Promise 对象

下面分四种不同参数的情况讨论其使用效果:

1. Promise.resolve(Promsie实例)

参数是一个 Promise 实例,这种情况Promise.resolve()什么都不做

2. Promise.resolve(thenable对象)

参数是具有then方法的对象(thenable对象),Promise.resolve方法会将这个对象转为Promise对象,然后就立即执行thenable对象的then方法。例如:

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
  console.log(value);  // 42
});

上面代码中,thenable对象的then方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then方法指定的回调函数,输出 42

3. Promise.resolve(普通对象或原始值)

参数不是具有then方法的对象,或根本就不是对象,Promise.resolve返回一个状态为resolved的对象

const p = Promise.resolve('Hello');
// 因为p的状态为resolved所以.then()会立即执行
p.then(function (s){
 console.log(s)
});
// Hello

4. Promise.resolve() 不带任何参数

这种情况直接返回状态为resolved的Promise对象。如果希望得到一个 Promise 对象,比较方便的方法就是直接调用。 立即resolve的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。示例:

setTimeout(function () {
  console.log('three');
}, 0);

Promise.resolve().then(function () {
  console.log('two');
});

console.log('one');
// one
// two
// three

上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log(‘one’)则是立即执行,因此最先输出

Promise.resolve() 实现原理

核心在于:将现有对象转换成状态为 fulfilled 的 Promise 对象,如果已经是 Promise 对象则直接返回

Promise.resolve = function (value) {
    if (value instanceof Promise) return value
    return new Promise(resolve => resolve(value))
}

Promise.reject() 用法

Promise.reject() 的作用是将现有对象转换成状态为 rejected 的 Promise 对象,参数情况也是四种,和上面的 Promise.resolve() 相似

Promise.reject() 实现原理

核心在于:将现有对象转换成状态为 rejected 的 Promise 对象

Promise.reject = function (value) {
    return new Promise((resolve, reject) => reject(value))
}

Generator

Generator 是什么

Generator(生成器)是ES6标准引入的新的数据类型。一个 generator 看上去像一个函数,但可以返回多次

function* fn(max) {
    yield ...;
    return;
}

generator 生成器长得像函数,与函数不同的是用function*定义的

yield 关键字

关键字 yield,有点像 return,yield 和 return 的区别在于:

  • return 只在函数调用之后返回值,return 语句之后不允许你执行任何其他操作
  • yield 相当于暂停函数执行而返回一次值,下次调用 next() 时,它将执行到下一个 yield 语句那里

return 返回的值

image.png 在控制台输出 return 的值(如果没有return就执行到函数结束)会得到一个 generator 对象,其属性[[GeneratorState]]的值是suspended表示“暂停状态”

Generator.prototype.next() 方法

image.png 在控制台输出 generator 对象的原型会看到有 next、return、throw等方法,我们重点看一下 next 方法:

image.png 使用 g.next() 方法后会得到一个对象 {value: 1,done: false},此时输出 g,可观察到其状态还是suspended,当执行第三次 g.next() 后将会得到 {value: 1,done: false}对象,此时输出 g 的状态为 "closed"。告知外界该 generator 对象已经执行完毕

返回的 value 就是 yield 的返回值,done 表示这个 generator 是否已经执行结束了。如果done为true,则value 就是 return的返回值。当执行到done为true时,这个generator对象就已经全部执行完毕,不要再继续调用next()了。

Generator.prototype.next() 方法 传参

next() 方法也可以传参

function* gen(x){
  var y = yield x + 2;
  return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }

第一次 g.next() 输出 { value: 3, done: false } 的原因是 gen(1) 传入参数 1,然后 yield 返回 1 + 2,所以得到 value 值为 3,而第二次 g.next() 传入参数 2,相当于把整个 yield x + 2 换成 2,所以 return 返回 y 的值为 2,执行结束,done 值为 true

Generator.prototype.return() 方法

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

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}
var g = gen();
g.next()        // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next()        // { value: undefined, done: true }

Generator.prototype.throw() 方法

Generator 函数返回的遍历器对象,都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获

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

var g = gen(1);
g.next();
g.throw('出错了');
// 出错了

相当于把整个 yield x + 2语句换成 throw("出错了"),然后 try...catch...语句捕获到了,于是输出 e

Generator 的使用

  • 输出斐波那契数列的每一项 以一个著名的斐波那契数列为例子引入:
0 1 1 2 3 5 8 13 21 34 ...

要编写一个产生斐波那契数列的函数,可以这么写:

function fib(max) {
    var
        t,
        a = 0,
        b = 1,
        arr = [0, 1];
    while (arr.length < max) {
        [a, b] = [b, a + b];
        arr.push(b);
    }
    return arr;
}
// 测试:
fib(5); // [0, 1, 1, 2, 3]
fib(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

普通函数只能返回一次,所以必须返回一个Array。但有时我们需要每次拿其中一个返回值,比如做一个与用户交互的进度条啊之类的,但普通函数只能返回一次,所以不能完成这项需求,但是,如果换成 generator,就可以一次返回一个数不断返回多次

function* fib(max) {
    var
        t,
        a = 0,
        b = 1,
        n = 0;
    while (n < max) {
        yield a;
        [a, b] = [b, a + b];
        n ++;
    }
    return;
}

直接调用试试:

fib(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}

直接调用一个 generator 和调用函数不一样,fib(5)仅仅是创建了一个 generator 对象,这个对象可以想象成隐藏了结果的数组,需要我们一次次的去触发它,它才会按顺序地一次次的冒出数组元素

var f = fib(5);
f.next(); // {value: 0, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: false}
f.next(); // {value: undefined, done: true}
f.next(); // {value: undefined, done: true}

next()方法会执行generator的代码,然后,每次遇到yield x;就返回一个对象{value: x, done: true/false},然后“暂停”【所以相比每次执行普通函数去获取斐波那契数列的第n项的性能要好很多】

或者直接用for ... of循环迭代generator对象,这种方式不需要我们每次去 next() 和判断 done:

for (var x of fib(5)) {
    console.log(x); // 依次输出0, 1, 1, 2, 3
}
  • 解决回调地狱:(还是上面那个 求一个地级市有多少个辖区 的例子)
ajax('url_1', data1, function (err, result) {
    if (err) {
        return handle(err);
    }
    ajax('url_2', data2, function (err, result) {
        if (err) {
            return handle(err);
        }
        ajax('url_3', data3, function (err, result) {
            if (err) {
                return handle(err);
            }
            return success(result);
        });
    });
});

有了generator,用AJAX时可以这么写:

try {
    r1 = yield ajax('url_1', data1);
    r2 = yield ajax('url_2', data2); // 可以使用ajax('first')的结果,也就是 r1
    r3 = yield ajax('url_3', data3); // 可以使用ajax('second')的结果,也就是 r2
    success(r3);
}
catch (err) {
    handle(err); 
}

这里使用 try...catch... 语句,所以在 try{ } 区域内不用对结果 r1、r2、r3 进行错误处理,哪个地方出错了自然会被 catch 语句捕获到,并且输出错误参数 err

  • 更高级的应用是在 Lazy-loading (未完成)

Generator

Generator 的实现原理

Generator 的实现原理:我关于它的实现原理的简单理解就是,有点像把函数的结果存到一个对象(数组)里面,然后返回,然后我们在对这个对象(数组)一个一个地遍历,得到每一次我们想想要的结果。只不过区别在于:使用 Generator 代替函数不会造成空间上的浪费,因为是在执行 Generator.next() 的时候才会去运行 Generator 函数

Yield

Generator 函数内部对代码的运行处理方式是:

  • 第一次是从 函数起点 到 第一个yield 语句;
  • 最后一次是从 最后一个 yield 语句 到 函数终点 或 return;
  • 其余的每一次都是从 yield 语句 执行到 下一个yield 语句 后停下。

所以这就好比把一个函数分割成很多很多子块函数,每一块子函数的起点都是 yield 语句,终点是下一个 yield 语句(第一块和最后一块除外),自然就能起到“暂停”的效果【从而实现“同步阻塞”的效果】,而且每一小块函数的返回值都会存在一个叫“迭代器”的容器里面

最终,Generator 函数执行结束后会返回一个迭代器

Generator.next() —— 迭代器模式

定义:迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示 简单理解就是:在不暴露对象的内部表示的情况下,能够遍历出所有元素 下面我们就来实现一个简单的迭代器:

// 在数据获取的时候没有选择深拷贝内容,
// 对于引用类型进行处理会有问题
// 这里只是演示简化了一点
function Iterdtor(arr){
    let data = [];
    if(!Array.isArray(arr)){
        data = [arr];
    }else{
        data = arr;
    }
    let length = data.length;
    let index = 0;
    // 迭代器的核心next
    // 当调用next的时候会开始输出内部对象的下一项
    this.next = function(){
    	let result = {};
    	result.value = data[index];
    	result.done = (index === length - 1 ? true : false;)
    	if(index !== length){
            index++;
            return result;
    	}
    	// 当内容已经没有了的时候返回一个字符串提示
    	return 'data is all done'
    };
}
let arr = [1,2,3,4];
// 生成一个迭代器对象。
let iterdtor = new Iterdtor(arr);

控制台输出:

image.png

async 和 await

async 函数是 Generator 函数的语法糖。使用 async 关键字代替 Generator 函数的星号 *await 关键字代替 yield

相较于Generator函数,async函数改进了以下四点:

  • 内置执行器 Generator 函数的执行必须靠执行器,所以才有了 co 模块,而 async 函数自带执行器。
  • 更好的语义 async 和 await,比起 * 和 yield,语义更清楚。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
  • 更广的适用性 co 模块约定,yield 命令后面只能是 Thunk 函数Promise 对象,而async 函数的await 命令后面,可以是Promise对象原始类型的值
  • 返回值 async 函数的返回值是Promise对象,这比Generator 函数的返回值是 Iterator对象方便多了。你可以用 then 方法指定下一步的操作。

async 和 await 的使用

  • 普通函数用 function 关键字开头
  • async 函数就用 async function 关键字开头
  • await 就是等待后面那一项的完成,如果后面那一项没完成绝不会往下走

还是上面那个 求一个地级市有多少个辖区 的例子:

ajax('url_1',data1);
ajax('url_2',data2); // 执行之前需要拿到 ajax('url_1',data1) 的结果
ajax('url_3',data3); // 执行之前需要拿到 ajax('url_2',data2) 的结果

使用 async / await:

function fn(url,data) {
    return new Promise((resolve, reject) => {
        ajax(url, data, function (err, res) {
            if (err) reject(err)
            resolve(res)
        })
    })
}
async function asyncFn() {
    let p1 = await fn('url_1',data1)
    let p2 = await fn('url_2',data2)
    let p3 = await fn('url_3',data3)
    return p3
}
asyncFn().then(result => {
    console.log(result)
}).catch(error => {
    console.log(error)
})

注意事项

  1. 凡是在前面添加了 async 的函数在执行后都会自动返回一个 Promise 对象
  2. await 必须在 async 函数里使用,不能单独使用
  3. await 后面如果跟的是Promise 对象,则会等待该对象里面的函数执行完成;如果跟的不是 Promise 对象则会执行其后面的代码 / 等于后面的值
// await 后面是 Promise 对象
let promiseDemo = new Promise((resolve, reject) => {
    setTimeout(()=>{
        resolve('success')
        console.log(3)
    },0)
})
async function test() {
    let result = await promiseDemo // 3
    return result
}
// await 后面不是 Promise 对象
let promiseDemo = fn(() => {
    console.log(3)
})
async function test() {
    let result = await promiseDemo // 3
    return result
}

async 和 await 的执行顺序

这道题跟 async / await 的应用有关,还和 JavaScript系列 -- event loop 事件轮询 有关

console.log(1) // 1
let promiseDemo = new Promise((resolve, reject) => {
    console.log(2) // 1 2
    setTimeout(() => {
        let random = Math.random()
        if (random >= 0) {
            resolve('success')
            console.log(3) // 1 2 4 6 3
        } else {
            reject('failed')
            console.log(3)
        }
    }, 1000)
})
async function test() {
    console.log(4) // 1 2 4
    let result = await promiseDemo // 注意这里不用加括号,它是执行 new Promise(...) 后返回的一个对象
    return result
}
test().then(result => {
    console.log(5) // 1 2 4 6 3 5
}).catch((result) => {
    console.log(5)
})
console.log(6) // 1 2 4 6

值得注意的是:

  • 我们在最开始 new Promise 的时候就已经执行 Promise 里面的箭头函数的代码了
  • 而 async 函数 test() 只有在 test().then().catch() 的时候才被调用,test() 函数里面的代码才开始被执行

解析:

  • 1 2 4 6 是同步任务,由主线程来办,所以在前半部分输出
  • 进入 async 函数 test() 里面后面执行到await promiseDemo语句,就会等待定时器完成计时,输出 3
  • 然后会返回一个 Promise 对象并赋值给变量 result,async 函数再把这个 Promise 对象返回到函数体外
  • 函数体外对这个 Promise 对象使用 then() 方法和 catch() 方法,从而输出 5

参考文章