重学JavaScript Day7

122 阅读7分钟

Ch 11 期约与异步函数

11.2 期约

期约是对尚不存在结果的一个替身。

11.2.1 Promise/A+规范

时间线:

  • 2010 年,CommonJS 实现的 Promises/A 规范日益流行起来
  • Q 和 Bluebird 等第三方 JavaScript 期约库也越来越得到社区认可
  • 为弥合现有实现之间的差异,2012 年 Promises/A+制定了 Promises/A+规范
  • Promises/A+规范最终成为ES6规范实现的范本

11.2.2 期约基础

ES6引入了Promise类型,通过new操作符来实例化。接受一个执行器函数作为参数。

const promise = new Promise(()=>{});

期约状态机

一个Promise它有三种状态:Pending(等待),Fulfilled(兑现),Rejected(拒绝)

一个Promise最初状态为Pending,然后可以转为Fulfilled或者Rejected,一但状态进行了改变,那么这个改变是不可逆的,改变后的状态也不能再改变。

通过执行函数来控制期约状态

我们知道了期约有三种状态,那么我们怎么手动改变期约状态呢?期约的状态是私有的,我们只能在期约内部修改状态,而修改状态使用过调用执行器中的函数来实现的,看如下代码:

const p1 = new Promise((resolve, reject)=>{
    // 这里面的代码是同步执行的
    ...
    resolve();          // pending => fulfilled
})
const p2 = new Promise((resolve, reject)=>{
    reject();           // pending => rejected
})

Promise.resolve和Promise.reject

一个Promise实例的初始状态并不一定是Pending,通过 Promise.resolve 和 Promise.reject 两个方法可以实例化一个解决或者是拒绝的期约。并且可以把解决或者拒绝的期约值作为参数传入函数,只能传一个参数,多余的参数会被忽略。如果传入的参数本身就是一个Promise,那么实际上就相当于没有执行任何操作。

let p1 = Promise.resolve(5);
console.log(p1);        // Promise <fulfilled> : 5
let p2 = Promise.reject(8,5,1);
console.log(p2);        // Promise <rejected> : 8
let p3 = new Promise(()=>{});
let p4 = Promise.resolve(p3);
console.log(p3 === p4); // true

值得注意的是,在同步代码的线程中,并不能通过try/ catch块来捕捉异步代码抛出的错误。

try {
    setTimeout(()=>{
        console.log('1')
        throw new Error('0')
    },0)
} catch(e){
    console.log(e)
}
// Uncaught Error:0

11.2.3 期约的实例方法

1. 实现 Thenable 接口

在ECMAScript中,异步结构中的任何对象都有一个 then()方法,这个方法被认为是是实现了 Thenable 接口。

2. Promise.prototype.then()

这个方法主要是为期约实例添加处理程序的的方法。then 方法最多接收两个参数:

第一个参数是期约状态变为fulfilled时的回调函数,

第二个参数时期约状态变为rejected时的回调函数。

function onResolved(id) { 
 setTimeout(console.log, 0, id, 'resolved'); 
} 
function onRejected(id) { 
 setTimeout(console.log, 0, id, 'rejected'); 
} 
let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000)); 
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000)); 
p1.then(() => onResolved('p1'), 
 () => onRejected('p1')); 
p2.then(() => onResolved('p2'), 
 () => onRejected('p2')); 
//(3 秒后)
// p1 resolved 
// p2 rejected 

then 方法的返回值是一个新的期约实例。这个新期约的实例是基于fulfilled状态下的回调函数的返回值进行构建。即返回 Promise.resolve(fulfilled状态处理程序的返回值)。如果没有显式的返回语句,则 Promise.resolve()会包装默认的返回值 undefined。rejected状态也是同理。

3. Promise.prototype.catch

Promise.prototype.catch()方法用于给期约添加拒绝处理程序。这个方法只接收一个参数: onRejected 处理程序。事实上,这个方法就是一个语法糖,调用它就相当于调用 Promise.prototype. then(null, onRejected)。

4. Promise.prototype.finally

Promise.prototype.finally()方法用于给期约添加 onFinally 处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行。这个方法可以避免 onResolved 和 onRejected 处理程序中出现冗余代码。

Promise.prototype.finally()方法返回一个新的期约实例,这个新期约实例不同于 then()或 catch()方式返回的实例。因为 onFinally 被设计为一个状态无关的方法,所以在大多数情况下它将表现为父期约的传递。对于已解决状态和被拒绝状态都是如此。

11.2.4 期约连锁与合成

1. 期约连锁

把期约逐个地串联起来是一种非常有用的编程模式。之所以可以这样做,是因为每个期约实例的方法(then()、catch()和 finally())都会返回一个新的期约对象,而这个新期约又有自己的实例方法。

let p = new Promise((res, rej)=>{
    console.log('first');
    res();
})
​
p.then(()=> console.log('second'))
 .then(()=> console.log('third'))
 .then(()=> console.log('fourth'))
​
// first -> second -> third -> fourth
3. Promise.all() 和 Promise.race()

Promise 类提供两个将多个期约实例组合成一个期约的静态方法:Promise.all()和 Promise.race()。 而合成后期约的行为取决于内部期约的行为。

Promise.all: 在一组期约全部解决之后再解决。这个静态方法接收一个可迭代对象(通常是数组),返回一个新期约。如果至少有一个包含的期约待定,则合成的期约也会待定。如果有一个包含的期约拒绝,则合成的期约也会拒绝。

let p1 = Promise.all([ 
 Promise.resolve(), 
 Promise.resolve() 
]); 
// 可迭代对象中的元素会通过 Promise.resolve()转换为期约
let p2 = Promise.all([3, 4]); 
// 空的可迭代对象等价于 Promise.resolve() 
let p3 = Promise.all([]);

Promise.race: 接收一个可迭代对象,返回一个新期约,是一组集合中最先解决或拒绝的期约的镜像

// 解决先发生,超时后的拒绝被忽略
let p1 = Promise.race([ 
 Promise.resolve(3), 
 new Promise((resolve, reject) => setTimeout(reject, 1000)) 
]); 
setTimeout(console.log, 0, p1); // Promise <resolved>: 3 

11.2.5 期约扩展

1. 期约取消

期约正在处理过程中,程序却不再需要其结果。在这个情况下,我们没有原生的api来解决这个问题,但是可以通过cancel token(取消令牌)这个方法来实现取消期约的功能。

  <button id="start">Start</button>
  <button id="cancel">Cancel</button>
  <script>
    class CancelToken {
      constructor(cancelFn) {
        this.promise = new Promise((resolve, reject) => {
          cancelFn(() => {
            setTimeout(console.log, 0, 'delay cancelled');
            resolve();
          });
        });
      }
    }
    const startButton = document.querySelector('#start');
    const cancelButton = document.querySelector('#cancel');
    
    function cancellableDelayedResolve(delay) {
      setTimeout(console.log, 0, 'set delay');
      return new Promise((resolve, reject) => {
        const id = setTimeout(() => {
          setTimeout(console.log, 0, 'delayed resolve');
          resolve();
        }, delay);
​
        const cancelToken = new CancelToken(cancelCallback =>
          cancelButton.addEventListener('click', cancelCallback)
        );
        cancelToken.promise.then(() => clearTimeout(id));
      });
    }
    startButton.addEventListener('click', () =>
      cancellableDelayedResolve(1000)
    );
  </script>

每次单击“Start”按钮都会开始计时,并实例化一个新的 CancelToken 的实例。此时,“Cancel” 按钮一旦被点击,就会触发令牌实例中的期约解决。而解决之后,单击“Start”按钮设置的超时也会被取消。

11.3 异步函数

异步函数,也称为“async/await”(语法关键字),是 ES6 期约模式在 ECMAScript 函数中的应用。 async/await 是 ES8 规范新增的。使得以同步方式写的代码能够异步执行。

11.3.1 异步函数

1. async

async 关键字用于声明异步函数。这个关键字可以用在函数声明、函数表达式、箭头函数和方法上,使用 async 关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。

async function foo() { 
 console.log(1); 
} 
foo(); 
console.log(2); 
// 1 
// 2 

异步函数如果使用 return 关键字返回了值,这个值会被 Promise.resolve() 包装成一个期约对象。异步函数始终返回期约对象

与在期约处理程序中一样,在异步函数中抛出错误会返回拒绝的期约, 不过,拒绝期约的错误不会被异步函数捕获:

async function foo() { 
 console.log(1); 
 throw 3; 
} 
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log);
console.log(2); 
// 1 
// 2 
// 3 
​
async function foo() { 
 console.log(1); 
 Promise.reject(3); 
} 
// Attach a rejected handler to the returned promise 
foo().catch(console.log); 
console.log(2); 
// 1 
// 2 
// Uncaught (in promise): 3 
2. await

因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力。使用 await 关键字可以暂停异步函数代码的执行,等待期约解决。await 关键字会暂停执行异步函数后面的代码,让出 JavaScript 运行时的执行线程

async function foo() { 
    let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3)); 
    let res = await p;
    console.log(res); 
} 
foo(); 
// 3 

等待会抛出错误的同步操作,会返回拒绝的期约,对拒绝的期约使用 await 则会释放错误值(将拒绝期约返回)

async function foo() { 
 console.log(1); 
 await Promise.reject(3); 
 console.log(4); // 这行代码不会执行
} 
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log); 
console.log(2); 
// 1 
// 2 
// 3 
3. await 限制

await 关键字必须在异步函数中使用,不能在顶级上下文中使用。

11.3.2 理解await

JavaScript 运行时在碰 到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了(个人理解为 Promise 的状态落定了),JavaScript 运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。

11.3.3 异步函数策略

1. 实现sleep
async function sleep(time) {
    return new Promise((res) => setTimeout(res, time));
}
async function foo(){
    ...
    await sleep(1500);
    ...
}
2. 串行执行期约
async function addTwo(x) {return x + 2;} 
async function addThree(x) {return x + 3;} 
async function addFive(x) {return x + 5;} 
async function addTen(x) { 
 for (const fn of [addTwo, addThree, addFive]) { 
    x = await fn(x); 
 } 
 return x; 
} 
addTen(9).then(console.log); // 19