《JavaScript 高级程序设计》第十一章 期约与异步函数 学习记录

1、异步编程

  • 异步行为是为了优化因计算量大而时间长的操作。
  • 在等待其他操作完成的同时,即使运行其他指令,系统也能保存稳定。
  • 异步操作不一定要计算量大或者时间长,不想为等待某个操作而阻塞线程执行,那么任何时候都可以使用。

1、同步与异步

  • 同步行为对应内存中顺序执行的处理器指令。

  • 异步行为类似于系统中断,即当前进程外部的实体可以出发代码执行。

  • 异步操作是必要的,因为强制等待一个长时间操作不可行。

    let x = 3
    setTimeout(()=> x = x + 4, 1000)
    复制代码
    • 什么时候x会更新?如何通知?

2、以往的异步编程模式

  • 以往只支持定义回调函数表明异步操作完成。
  • 需要深度嵌套回调函数,因此产生「回调地狱

1、异步返回值

  • 如何把异步操作setTimeout 的返回值传给需要的地方?

  • 以往需要一个回调,回调包含要使用异步返回值的代码(作为回调参数)

    function double(value, callback) {
      setTimeout(()=> callback(value*2), 1000)
    }
    double(3,(x)=> console.log('I was given: ${x}'))
    // I was given 6 (1000ms后)
    复制代码

2、失败处理

  • 成功回调和失败回调
function double(value, success, failure) {
  setTimeout(() => {
    try {
      if (typeof value !== 'number') {
        throw 'Must provide number as first argument';
      }
      success(2 * value);
    } catch (e) {
      failure(e);
    }
  }, 1000);
}
const successCallback = (x) => console.log(`Success: ${x}`);
const failureCallback = (e) => console.log(`Failure: ${e}`);
double(3, successCallback, failureCallback);
double('b', successCallback, failureCallback);
// Success: 6(大约 1000 毫秒之后)
// Failure: Must provide number as first argument(大约 1000 毫秒之后)
复制代码
  • 不可取,必须在初始化异步操作时定义回调,异步函数的返回值只在短时间内存在,只有预备好将这个短时间存在的值作为参数的回调才能接收到它。

3、嵌套异步函数

  • 噩梦一般的 嵌套?

2、期约

  • 期约,承诺(promise)

1、Promise/A+ 规范

  • ES6起完善支持

2、期约基础

  • 引用类型Promise,可以用new操作符来实例化
  • 创建时需要传入执行器函数作为参数

1、期约状态机

  • 把一个Promise实例传给console.log() 时,控制台输出表明该实例处于**待定(pending)**状态
  • 三种状态
    • 待定(pending)
      • 最初始状态,待定状态下可以落定(settled)为代表成功兑现状态,或者失败的拒绝状态
      • 状态修改不可逆
      • 不能保证期约必然会脱离待定状态
    • 兑现(fulfilled,有时也称解决,resolved)
    • 拒绝(rejected)
  • 期约的状态是私有的,不能外部检测到,为了避免读取到期约状态,用同步的方式处理期约对象。
  • 期约的状态是不能被外部修改。

2、解决值、拒绝理由及期约用例

  • 期约用途
    • 抽象的表示一个异步操作,期约状态代表期约是否完成,这个状态就是有用的信息。
    • 期约封装的异步操作会实际生成某个值,程序期待期约状态改变时可以访问这个值。
  • 为了满足这两种用例,每个期约只要状态切换为兑现,就会有一个私有的内部(value)。每个期约只要切换为拒绝,就会有一个私有的内部理由(reason)。默认是undefined

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

  • resolve() 会把状态切换为兑现,reject() 会切换为拒绝

    let p1 = new Promise((resolve, reject) => resolve());
    setTimeout(console.log, 0, p1); // Promise <fulfilled>
    let p2 = new Promise((resolve, reject) => reject());
    setTimeout(console.log, 0, p2); // Promise <fulfilled>
    // Uncaught error (in promise)
    复制代码
  • 无论resolve() 和 reject() 哪个状态被调用了,状态转换都不可撤销,修改状态会静默失败

  • 为了避免卡在约定状态,可以定时退出。

4、Promise.resolve()

  • 期约并非一开始必须处于待定状态,可以通过Promise.resolve()静态方法,实例化一个解决的期约。

    let p1 = new Promise((resolve, reject) => resolve())
    let p2 = Promise.resolve()
    复制代码
  • 这个解决的期约的值对应着传给Promise.resolve() 的第一个参数

    setTimeout(console.log, 0, Promise.resolve());
    // Promise <fulfilled>: undefined
    
    setTimeout(console.log, 0, Promise.resolve(3));
    // Promise <fulfilled>: 3
    
    // 多余的参数会忽略
    setTimeout(console.log, 0, Promise.resolve(4, 5, 6));
    // Promise <fulfilled>: 4 
    复制代码
  • 如果传入的参数本身是一个期约,那么它的行为类似空包装,可以说是个幂等方法

    let p = Promise.resolve(7)
    p === Promise.resolve(p)
    p === Promise.resolve(Promise.resolve(p)       
    复制代码
  • 这个幂等性会保留传入期约的状态

    let p = new Promise(() => {})
    setTimeout(console.log, 0, p) 
    // Promise <pending>
    setTimeout(console.log, 0, Promise.resolve(p))
    // Promise <pending>
    setTimeout(console.log,0,p === Promise.resolve(p)) 
    // true
    复制代码
  • 可以包装任何非期约值,包含错误对象,并将其转换为解决的期约。

    let p = Promise.resolve(new Error('foo'));
    setTimeout(console.log, 0, p);
    // Promise <fulfilled>: Error: foo
    复制代码

5、Promise.reject()

  • 与Promise.resolve() 类似,Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误(无法通过try/catch捕获,只能通过拒绝处理程序捕获)

    let p1 = new Promise((resolve, reject)=> reject())
    let p2 = Promise.reject()
    复制代码
  • 拒绝的期约理由就是传给Promise.reject() 的第一个参数

    let p = Promise.reject(3);
    setTimeout(console.log, 0, p); // Promise <rejected>: 3
    p.then(null, (e) => setTimeout(console.log, 0, e)); // 3
    复制代码
  • 如果传入期约对象,这个期约会成为它返回的拒绝的理由

    setTimeout(console.log, 0, Promise.reject(Promise.resolve()));
    // Promise <rejected>: Promise <resolved>
    复制代码
    • Promise.resolve() 会保留传入状态
    • Promise.reject() 无论传入什么都会返回失败

6、同步/异步执行的二元性

try {
  throw new Error('foo');
} catch (e) {
  console.log(e); // Error: foo
}

try {
  Promise.reject(new Error('bar'));
} catch (e) {
  console.log(e);
}
// Uncaught (in promise) Error: bar 
复制代码
  • try/catch同步代码之所以没有捕获期约抛出的错误,是因为没有通过异步模式捕获错误。
  • 期约的真正特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式的媒介

3、期约的实例方法

  • 期约实例方法可以访问异步操作返回的数据,处理期约成功和失败的结果,连续对期约求值,或者添加只有期约进入终止状态才会执行的代码。

1、实现Thenable接口

  • 异步结构,任何对象都有then() 方法,实现了Thenable接口

    class MyThenable {
      then() {}
    }
    复制代码
  • Promise类型实现了Thenable接口。这个简化的接口跟TypeScript获取他包里的接口或类型定义不同,都设定了Thenable接口更具体的形式。

2、Promise.prototype.then()

  • 为期约实例添加处理程序的主要方法

  • then() 方法

    • 最多接收两个参数 onResolved和onRejected处理程序
    • 都是可选的,如果提供的话则会在期约分别进入”兑现”和“拒绝”状态时执行。
    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'))
    // 3s后
    // p1 resolved 
    // p2 rejected 
    复制代码
    • 期约如果只提供onRejected处理程序,第一个参数要传null,这样有助于避免在内存中创建多余对象。
    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('gobbeltygook');
    // 不传 onResolved 处理程序的规范写法
    p2.then(null, () => onRejected('p2'));
    // p2 rejected(3 秒后)
    复制代码
    • Promise.prototype.then()方法返回一个新的期约实例
    let p1 = new Promise(() => {})
    let p2 = p1.then()
    setTimeout(console.log, 0, p1)
    //Promise <pending>
    setTimeout(console.log, 0, p1)
    //Promise <pending>
    setTimeout(console.log, 0, p1===p2)
    // false
    复制代码
    • 这个新期约实例基于onResolved 处理程序的返回值构建。
    • 该处理程序的返回值会通过Promise.resolve() 包装来生成新期约。
    • 如果没有提供这个处理程序,则Promise.resolve()就会包装上一个期约解决之后的值。
    • 如果没有显式返回语句,则Promise.resolve()会包装默认的返回值undefined
    let p1 = Promise.resolve('foo')
    
    // 若调用then()时不传处理程序,则原样向后传
    let p2 = p1.then()
    setTimeout(console.log, 0, p2)
    // Promise <fulfilled>: foo
    
    // 没有显式返回语句都一样 返回undefined
    let p3 = p1.then(()=> undefined)
    let p4 = p1.then(()=> {})
    let p5 = p1.then(()=> Promise.resolve())
    // ...Promise <fulfilled>: undefined
    
    // 如果有显式返回值,就会包装这个值
    let p6 = p1.then(()=> 'bar')
    let p7 = p1.then(()=> Promise.resolve('bar'))
    // ...Promise <fulfilled>: bar
    
    // Promise.resolve() 保留返回的期约
    let p8 = p1.then(()=> new Promise(()=>{}))
    let p9 = p1.then(()=> Promise.reject())
    setTimeout(console.log, 0, p8)
    // Promise <pending>
    setTimeout(console.log, 0, p9)
    // Promise <rejected>: undefined
    
    // 抛出异常会返回拒绝的期约
    let p10 = p1.then(()=> {throw 'baz'})
    // Uncaught (in promise) baz
    setTimeout(console.log, 0, p10)
    // Promise <rejected>: baz
    
    // 返回错误值不会触发上面拒绝行为而是会包装在一个解决期约中
    let p11 = p1.then(()=> Error('qux'))
    setTimeout(console.log, 0, p11)
    // Promise <fulfilled>: Error: qux
    复制代码
    • onRejected处理程序类似。onRejected处理程序返回的值也会被Promise.resolve() 包装。
    • onRejected处理程序任务就是捕获异步错误,使用不抛出异常是符合期约的行为,应该返回一个解决期约。
    let p1 = Promise.reject('foo')
    
    // 调用then() 不传处理程序则原样向后传
    let p2 = p1.then()
    //  Uncaught (in promise) foo
    setTimeout(console.log, 0, p2)
    // Promise <rejected>: foo
    
    // 没有显式返回语句都一样 返回undefined
    let p3 = p1.then(null, ()=> undefined)
    let p4 = p1.then(null, ()=> {})
    let p5 = p1.then(null, ()=> Promise.resolve())
    // ...Promise <fulfilled>: undefined
    
    // 如果有显式返回值,就会包装这个值
    let p6 = p1.then(null,()=> 'bar')
    let p7 = p1.then(null,()=> Promise.resolve('bar'))
    // ...Promise <fulfilled>: bar
    
    // Promise.resolve() 保留返回的期约
    let p8 = p1.then(null, ()=> new Promise(()=>{}))
    let p9 = p1.then(null, ()=> Promise.reject())
    setTimeout(console.log, 0, p8)
    // Promise <pending>
    setTimeout(console.log, 0, p9)
    // Promise <rejected>: undefined
    
    // 抛出异常会返回拒绝的期约
    let p10 = p1.then(null, ()=> {throw 'baz'})
    // Uncaught (in promise) baz
    setTimeout(console.log, 0, p10)
    // Promise <rejected>: baz
    
    // 返回错误值不会触发上面拒绝行为而是会包装在一个解决期约中
    let p11 = p1.then(null, ()=> Error('qux'))
    setTimeout(console.log, 0, p11)
    // Promise <fulfilled>: Error: qux
    复制代码

3、Promise.prototype.catch()

  • 用于给期约添加拒绝处理程序,直接收一个参数onRejected处理程序。

  • 语法糖,相当于调用Promise.prototype.then(null, onRejected)

    let p = Promise.reject()
    let onRejected = function(e) {
      setTimeout(console.log, 0, 'rejected')
    }
    p.then(null, onRejected) // rejected
    p.catch(onRejected) // rejected
    复制代码
  • Promise.prototype.catch() 返回了一个新期约

    let p1 = new Promise(()=> {})
    let p2 = p1.catch()
    setTimeout(console.log, 0, p1)
    //Promise <pending>
    setTimeout(console.log, 0, p1)
    //Promise <pending>
    setTimeout(console.log, 0, p1===p2)
    //false
    复制代码
  • 新期约的行为与Promise.prototype.then() 的onRejected处理程序一样。

4、Promise.prototype.finally()

  • 用于给期约添加onFinally处理程序,这个处理程序在期约传换为解决或拒绝状态都会执行。

  • 可以避免onResolved和onRejected处理程序出现冗余代码

  • onFinally处理程序无法知道期约状态是解决还是拒绝,所以主要用于清理代码。

    let p1 = Promise.resolve()
    let p2 = Promise.resolve()
    let onFinally = function(e) {
      setTimeout(console.log, 0, 'Finally')
    }
    
    p1.finally(onFinally) // Finally
    p2.finally(onFinally) // Finally
    复制代码
  • Promise.prototype.finally() 返回了一个新期约

    let p1 = new Promise(()=> {})
    let p2 = p1.finally()
    setTimeout(console.log, 0, p1)
    //Promise <pending>
    setTimeout(console.log, 0, p1)
    //Promise <pending>
    setTimeout(console.log, 0, p1===p2)
    //false
    复制代码
  • 这个新期约实例不同于then() 或 catch() 方式返回的实例。是一个与状态无关的方法,指表现为父期约的传递。

    let p1 = Promise.resolve('foo')
    
    // 原样往后传
    let p2 = p1.finally()
    let p3 = p1.finally(()=> undefined)
    let p4 = p1.finally(()=> {})
    let p5 = p1.finally(()=> Promise.resolve())
    let p6 = p1.finally(()=> 'bar')
    let p7 = p1.finally(()=> Promise.resolve('bar'))
    let p8 = p1.finally(()=> Error('qux'))
    
    //... Promise <fulfilled>: foo
    复制代码
  • 如果返回一个待定期约,或者onFinally处理程序抛出了错误,则会返回相应期约(待定或拒绝)

    // Promise.resolve() 保留返回的期约
    let p9 = p1.finally(()=> new Promise(()=> {}))
    let p10 = p1.finally(()=> Promise.reject())
    // Uncaught (in promise): undefined
    setTimeout(console.log, 0, p9)
    // Promise <pending>
    setTimeout(console.log, 0, p10)
    // Promise <rejected>: undefined
    
    // 抛出异常会返回拒绝的期约
    let p11 = p1.finally(()=> {throw 'baz'})
    // Uncaught (in promise) baz
    setTimeout(console.log, 0, p11)
    // Promise <rejected>: baz
    复制代码
  • 返回待定期约的情形不常见,因为只要期约一解决,新期约仍然会原样后传初始的期约。

    let p1 = Promise.resolve('foo')
    
    let p2 = p1.finally(()=> {
      new Promise((resolve, reject)=> {
        setTimeout(()=> resolve('bar'), 100)
      })
    })
    
    setTimeout(console.log, 0, p2) // Promise <pending>
    setTimtout(()=> setTimeout(console.log, 0, p2), 200)
    // 200ms后
    // Promise <fulfilled>: foo
    复制代码

5、非重入期约方法

  • 期约进入落定状态时,该状态相关的处理程序仅仅会被排期,而不是立即执行。之后的同步代码一定会在处理程序之前先执行。

  • 即使期约一开始就与附加处理程序关联,也是这样,称为“非重入”

    // 创建解决的期约
    let p = Promise.resolve()
    
    // 添加解决处理程序
    p.then(() => console.log('onResolved handler'))
    
    console.log('then() returns')
    
    // then() returns
    // onResolved handler
    复制代码
    let synchronousResolve;
    
    let p = new Promise((resolve)=> {
      synchronousResolve = function() {
        console.log('1: invoking resolve()')
        resolve()
        console.log('2: resolve() returns')
      }
    })
    
    p.then(() => console.log('4: then() handler executes'))
    
    synchronousResolve()
    
    console.log('3 synchronousResolve() returns')
    //1234
    复制代码
  • 非重入适用于onResolved/onRejected处理程序、catch()处理程序和finally() 处理程序。

6、邻近处理程序的执行顺序

  • 如果添加了多个处理程序,期约变化时,相关处理程序会按照添加顺序依次执行。无论then() catch() 还是 finally()

    let p1 = Promise.resolve()
    let p2 = Promise.reject()
    
    p1.then(()=> setTimeout(console.log, 0, 1))
    p1.then(()=> setTimeout(console.log, 0, 2))
    // 1
    // 2
    
    p2.then(null, ()=> setTimeout(console.log, 0, 3))
    p2.then(null, ()=> setTimeout(console.log, 0, 4))
    // 3
    // 4
    
    p2.catch(()=> setTimeout(console.log, 0, 5))
    p2.catch(()=> setTimeout(console.log, 0, 6))
    // 5
    // 6
    
    p1.finally(()=> setTimeout(console.log, 0, 7))
    p1.finally(()=> setTimeout(console.log, 0, 8))
    // 7
    // 8
    复制代码

7、传递解决值和拒绝理由

  • 落定状态,期约会提供解决值或拒绝理由,进行进一步操作。

  • 解决的值和拒绝的理由分别作为resolve() 和 reject() 的第一个参数

    let p1 = new Promise((resolve, reject) => resolve('foo'));
    p1.then((value) => console.log(value)); // foo
    
    let p2 = new Promise((resolve, reject) => reject('bar'));
    p2.catch((reason) => console.log(reason)); // bar
    复制代码
  • Promise.resolve() 和 Promise.reject() 在被调用时就会接收解决值和拒绝理由。也会传给处理程序

    let p1 = Promise.resolve('foo');
    p1.then((value) => console.log(value)); // foo
    
    let p2 = Promise.reject('bar');
    p2.catch((reason) => console.log(reason)); // bar
    复制代码

8、拒绝期约与拒绝错误处理

  • 类似throw() 表示中断,处理函数中抛出错误会导致拒绝。

    let p1 = new Promise((resolve, reject) => reject(Error('foo')));
    let p2 = new Promise((resolve, reject) => { throw Error('foo'); });
    let p3 = Promise.resolve().then(() => { throw Error('foo'); });
    let p4 = Promise.reject(Error('foo'));
    setTimeout(console.log, 0, p1); 
    // Promise <rejected>: Error: foo
    setTimeout(console.log, 0, p2); 
    // Promise <rejected>: Error: foo
    setTimeout(console.log, 0, p3); 
    // Promise <rejected>: Error: foo
    setTimeout(console.log, 0, p4); 
    // Promise <rejected>: Error: foo
    
    // 也有四个未捕获错误
    复制代码
  • 理由可以任意,最好统一使用错误对象。

  • Promise.resolve().then() 的错误最后抛出,因为要创建另一个期约

  • 同步中 throw() 抛出错误后,不会继续执行后面语句。

  • 异步中 throw() 抛出错误后,并不会阻止运行时继续执行同步指令

  • 异步错误只能通过异步的onRejected处理程序捕获

    // 正确	
    Promise.reject(Error('foo')).catch((e)=> {})
    
    // 错误
    try{
      Promise.reject(Error('foo'))
    }.catch(e){}
    复制代码
  • 在解决或拒绝之前的捕获执行函数里,仍可以使用try/catch在执行函数中捕获错误。

    let p = new Promise((resolve, reject) => {
      try {
        throw Error('foo')
      }catch(e) {}
      resolve('bar')
    })
    setTimeout(console.log, 0, p);
    // promise <fulfilled>: bar
    复制代码
  • then() 和 catch() 的 onRejected 处理程序在语义上相当于try/catch

    • 出发点都是为了捕获错误后隔离,不影响正常逻辑
    • 因此onRejected处理程序的任务应该是在捕获异步错误之后返回一个解决的期约
    console.log('begin synchronous execution');
    try {
      throw Error('foo');
    } catch (e) {
      console.log('caught error', e);
    }
    console.log('continue synchronous execution');
    // begin synchronous execution
    // caught error Error: foo
    // continue synchronous execution
    
    new Promise((resolve, reject) => {
      console.log('begin asynchronous execution');
      reject(Error('bar'));
    }).catch((e) => {
      console.log('caught error', e);
    }).then(() => {
      console.log('continue asynchronous execution');
    });
    // begin asynchronous execution
    // caught error Error: bar
    // continue asynchronous execution
    
    复制代码

4、期约连锁与期约合成

  • 期约连锁就是一个期约接一个期约拼接。
  • 期约合成是将多个期约组合成为一个期约

1、期约连锁

  • 因为每个期约实例方法(then()、catch()、finally()) 都会返回一个新的期约对象,新的期约对象又有自己的实例方法。

    let p = new Promise((resolve, reject) => {
      console.log('first');
      resolve()
    })
    p.then(()=>console.log('second'))
     .then(()=>console.log('third'))
     .then(()=>console.log('fourth'))
    // first second third fourth
    复制代码
    • 最终执行了一连串的同步任务,相当于同步函数。
  • 真正执行异步任务,可以让每个执行器都返回期约实例,可以让每个后续期约都等待之前的期约。

    let p1 = new Promise((resolve, reject) => {
      console.log('p1')
      setTimeout(resolve, 1000)
    })
    p1.then(() => new Promise(resolve, reject)=> {
      console.log('p2')
      setTimeout(resolve, 1000)
    }).then(() => new Promise(resolve, reject)=> {
      console.log('p3')
      setTimeout(resolve, 1000)
    }).then(() => new Promise(resolve, reject)=> {
      console.log('p4')
      setTimeout(resolve, 1000)
    })
    // p1 1s后
    // p2 2s后
    // p3 3s后
    // p4 4s后
    复制代码
  • 提前到工厂函数

    function delayedResolve(str) {
      return new Promise((resolve, reject)=> {
        console.log(str)
        setTimeout(resolve, 1000)
      })
    }
    delayedResolve('p1')
     .then(()=> delayedResolve('p2'))
     .then(()=> delayedResolve('p3'))
     .then(()=> delayedResolve('p4'))
    复制代码
  • then()、catch()、finally() 都返回期约

    let p = new Promise((resolve, reject) => {
      console.log('initial promise rejects');
      reject();
    });
    p.catch(() => console.log('reject handler'))
      .then(() => console.log('resolve handler'))
      .finally(() => console.log('finally handler'));
     // initial promise rejects
     // reject handler
     // resolve handler
     // finally handler 
    复制代码

2、期约图

  • 一个期约可以有任意多个处理程序,可以构建有向非循环图的结构。

  • 每个期约都是图中的一个节点,使用实例方法添加的处理程序是有向节点。图的方向就是期约的解决或拒绝顺序。

    //     A
    //    /  \
    //   B    C
    //  / \  / \
    // D  E  F   G
    let A = new Promise((resolve, reject) => {
      console.log('A');
      resolve();
     });
     let B = A.then(() => console.log('B'));
     let C = A.then(() => console.log('C'));
     B.then(() => console.log('D'));
     B.then(() => console.log('E'));
     C.then(() => console.log('F'));
     C.then(() => console.log('G')); 
    复制代码
  • 输出语句是对二叉树的层序遍历。是按照他们添加的顺序执行的。

3、Promise.all() 和 Promise().race()

1、Promise.all()
  • Promise.all() 静态方法创建的期约会在一组期约全部解决之后再解决。接收一个可迭代对象返回一个新期约。

    let p1 = Promise.all([
      Promise.resolve(),
      Promise.resolve()
    ])
    // 可迭代对象中的元素会通过Promise.resolve() 转换为期约
    let p2 = Promise.all([3, 4])
    
    // 空的可迭代对象等价于Promise.resolve()
    let p3 = Promise.all([])
    
    // 无效
    let p4 = Promise.all()
    // TypeError: ...
    复制代码
  • 合成期约只会在每个包含的期约都解决后才解决

    let p = Promise.all([
      Promise.resolve(),
      new Promise((resolve, reject) => setTimeout(resolve, 1000))
    ]);
    setTimeout(console.log, 0, p); // Promise <pending>
    p.then(() => setTimeout(console.log, 0, 'all() resolved!'));
     // all() resolved!(大约 1 秒后)
    复制代码
  • 如果至少有一个包含的期约待定,合成期约也待定

  • 如果有一个包含的期约拒绝,合成期约也拒绝

    // 永远待定
    let p1 = Promise.all([new Promise(() => {})]);
    setTimeout(console.log, 0, p1); 
    // Promise <pending>
    
    // 一次拒绝会导致最终期约拒绝
    let p2 = Promise.all([
     Promise.resolve(),
     Promise.reject(),
     Promise.resolve()
    ]);
    setTimeout(console.log, 0, p2); 
    // Promise <rejected>
    // Uncaught (in promise) undefined
    复制代码
  • 如果都解决,合成期约的解决值就是所有包含期约解决值的数组,按迭代器顺序。

    let p = Promise.all([
      Promise.resolve(3),
      Promise.resolve(),
      Promise.resolve(4)
    ])
    p.then((values)=> setTimeout(console.log,0,values))
    // [3, undefined, 4]
    复制代码
  • 如果有期约拒绝,则第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由,之后拒绝不会影响最后期约拒绝理由

  • 不会影响所有包含期约正常的拒绝操作,合成的期约会静默处理所有包含期约的拒绝操作。

    // 虽然只有第一个期约的拒绝理由会进入
    // 拒绝处理程序,第二个期约的拒绝也
    // 会被静默处理,不会有错误跑掉
    let p = Promise.all([
      Promise.reject(3),
      new Promise((resolve, reject) => setTimeout(reject, 1000))
    ]);
    p.catch((reason) => setTimeout(console.log, 0, reason)); // 3
     // 没有未处理的错误
    复制代码
2、Promise.race()
  • Promise.race() 静态方法返回一个包装期约,是一组集合中最先解决或拒绝期约的镜像。

    let p1 = Promise.race([
      Promise.resolve(),
      Promise.resolve()
    ])
    // 可迭代对象中的元素会通过Promise.resolve() 转换为期约
    let p2 = Promise.race([3, 4])
    
    // 空的可迭代对象等价于new Promise(()=> {})
    let p3 = Promise.race([])
    
    // 无效
    let p4 = Promise.race()
    // TypeError: ...
    复制代码
  • 不会对解决或拒绝的期约区别对待,无论解决还是拒绝,第一个落定的期约,Promise.race() 就会包装其解决值或拒绝理由并返回新期约。

    // 解决先发生,超时后的拒绝被忽略
    let p1 = Promise.race([
      Promise.resolve(3),
      new Promise((resolve, reject) => setTimeout(reject, 1000))
    ]);
    setTimeout(console.log, 0, p1); 
    // Promise <fulfilled>: 3
    
    // 拒绝先发生,超时后的解决被忽略
    let p2 = Promise.race([
      Promise.reject(4),
      new Promise((resolve, reject) => setTimeout(resolve, 1000))
    ]);
    setTimeout(console.log, 0, p2); 
    // Promise <rejected>: 4
    
    // 迭代顺序决定了落定顺序
    let p3 = Promise.race([
      Promise.resolve(5),
      Promise.resolve(6),
      Promise.resolve(7)
    ]);
    setTimeout(console.log, 0, p3); 
    // Promise <fulfilled>: 5
    复制代码
  • 如果有一个期约先拒绝,只要他是第一个落定的,就会成为拒绝理由。之后在拒绝不会影响最终的拒绝理由。但不会影响正常的拒绝操作,合成的期约会静默处理所有包含期约的拒绝操作。

4、串行期约合成

  • 类似函数合成

    function addTwo(x) { return x + 2; }
    function addThree(x) { return x + 3; }
    function addFive(x) { return x + 5; }
    function addTen(x) {
      return Promise.resolve(x)
        .then(addTwo)
        .then(addThree)
        .then(addFive);
    }
    addTen(8).then(console.log); // 18
    
    // Array.prototype.reduce()
    function addTwo(x) { return x + 2; }
    function addThree(x) { return x + 3; }
    function addFive(x) { return x + 5; }
    function addTen(x) {
      return [addTwo, addThree, addFive]
       .reduce((promise, fn)=> promise.then(fn), Promise.resolve(x))
    }
    addTen(8).then(console.log); // 18
    
    // 提出公共函数
    function addTwo(x) { return x + 2; }
    function addThree(x) { return x + 3; }
    function addFive(x) { return x + 5; }
    
    function compose(...fns) {
      return (x)=> fns.reduces((promise, fn)=> promise.then(fn), Promise.resolve(x))
    }
    
    let addTen = compose(addTwo, addThree, addFive)
    
    addTen(8).then(console.log); // 18
    复制代码

5、期约扩展

  • 第三方期约库不具备 期约取消和进度追踪

1、期约取消

  • 通过”取消令牌”, 其实就是一层临时封装,触发取消。

    class CancelToken {
    	constructor(cancelFn) {
        this.promise = new Promise((resolve, reject)=> {
          cancelFn(resolve)
        })
      }
    }
    function cancelableDelayResolve() {
      
    }
    const id = setTimeout(()=> {
      resolve()
    }, delay)
    const cancelToken = new CancelToken((cancelCb)=> {
    	xxxx.addEventListener('click', cancelCb)
    }) 
    cancelToken.promise.then(()=>clearTimeout(id))
    复制代码

2、期约进度通知

  • 通过扩展监控期约执行进度。

  • 扩展Promise类,为它添加notify()方法

    class TrackablePromise extends Promise {
      constructor(executor) {
        const notifyHandlers = []
        
        super((resolve, reject)=> {
          return executor(resolve, reject, (status) => {
            notifyHandlers.map((handler) => handler(status))
          })
        })
        this.notifyHandlers = notifyHandlers
      }
      
      notify(notifyHandler) {
        this.notifyHandlers.push(notifyHandler)
        return this
      }
    }
    
    // 这样就可以执行函数时使用notify了。
    let p = new TrackablePromise((resolve, reject, notify)=> {
      function countdown(x) {
        if(x > 0) {
          notify(`${20 * x}% remaining`)
          setTimeout(() => countdown(x - 1), 1000)
        }else {
          resolve()
        }
      }
      countdown(5)
    })
    
    p.notify((x)=> setTimeout(console.log, 0, 'progress:', x))
    p.then(()=> setTimeout(console.log, 0 , 'completed'))
    // (约 1 秒后)80% remaining
    // (约 2 秒后)60% remaining
    // (约 3 秒后)40% remaining
    // (约 4 秒后)20% remaining
    // (约 5 秒后)completed 
    复制代码

3、异步函数

  • async/await,ES8 新增,让同步方式写的代码能够异步执行。

    // 以往
    let p = new Promise((resolve, reject)=>setTimeout(resolve, 1000, 3))
    
    p.then((x)=>console.log(x))
    复制代码

1、异步函数

1、async

  • async 关键词用于声明异步函数,可用在函数声明、函数表达式、箭头函数和方法上。

    async function foo() {}
    
    let bar = async function() {}
    
    let baz = async () => {}
    
    class Qux {
      async qux() {}
    }
    复制代码
  • 使用async可让函数具有异步特征,但总体上仍然是同步求值的

  • 在参数和闭包方面,异步函数仍然就有普通JavaScript函数的正常行为

    async function foo() {
      console.log(1)
    }
    foo()
    console.log(2)
    // 1
    // 2
    复制代码
  • 异步函数通过return 返回的值(没有则为undefined),这个值会被Promise.resolve()包装成一个期约对象。异步函数始终返回期约对象。在函数外部调用这个函数可以得到它返回的期约。

    async function foo() {
      console.log(1)
      return 3
    }
    foo().then(console.log)
    console.log(2)
    // 1 2 3
    
    // 直接返回一个期约对象
    async function foo() {
      console.log(1)
      return Promise.resolve(3)
    }
    foo().then(console.log)
    console.log(2)
    // 1 2 3
    复制代码
  • 异步函数返回值期待一个实现(不要求)thenable接口的对象,常规值也可以。如果返回是实现thenable接口的对象,则可以由提供给then()的处理程序“解包”。 否则就当做已解决期约。

    // 返回一个原始值
    async function foo() {
      return 'foo';
    }
    foo().then(console.log);
    // foo
    
    // 返回一个没有实现 thenable 接口的对象
    async function bar() {
      return ['bar'];
    }
    bar().then(console.log);
    // ['bar']
    
    // 返回一个实现了 thenable 接口的非期约对象
    async function baz() {
      const thenable = {
        then(callback) {
          callback('baz');
        }
      };
      return thenable;
    }
    baz().then(console.log);
    // baz
    
    // 返回一个期约
    async function qux() {
      return Promise.resolve('qux');
    }
    qux().then(console.log);
    // qux
    
    复制代码
  • 与在期约处理程序中一样, 在异步函数中抛出错误会返回拒绝的期约

    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 可以暂停异步函数代码的执行,等待期约解决

    let p = new Promise((resolve, reject)=>setTimeout(resolve, 1000, 3))
    
    p.then((x)=> console.log(x)) // 3
    
    // ---->
    async function foo() {
      let p = new Promise((resolve,reject)=>setTimeout(resolve, 1000, 3))
      console.log(await p)
    }
    foo()
    // 3
    复制代码
  • await 这个行为与 yield 一样。同样尝试“解包”对象的值。

  • await可以单独使用也可以在表达式使用。

    // 异步打印 foo
    async function foo() {
      console.log(await Promise.resolve('foo'))
    }
    foo()
    // foo
    
    // 异步打印 bar
    async function bar() {
      return await Promise.resolve('bar')
    }
    bar().then(console.log)
    // bar
    
    // 1000ms 后异步打印 baz
    async function baz() {
      await new Promise((resolve, reject) => {
      return setTimeout(resolve, 1000)
      })
      console.log('baz')
    }
    baz()
    // baz (1000ms 后)
    复制代码
  • await期待(不要求)一个实现thenable接口对象。如果是thenable接口对象则进行“解包”,否则当作已解决值

    // 等待一个原始值
    async function foo() {
      console.log(await 'foo');
    }
    foo();
    // foo
    
    // 等待一个没有实现 thenable 接口的对象
    async function bar() {
      console.log(await ['bar']);
    }
    bar();
    // ['bar']
    
    // 等待一个实现了 thenable 接口的非期约对象
    async function baz() {
      const thenable = {
        then(callback) { callback('baz'); }
      };
      console.log(await thenable);
    }
    baz();
    // baz
    
    // 等待一个期约
    async function qux() {
      console.log(await Promise.resolve('qux'));
    }
    qux();
     // qux
    复制代码
  • 等待会抛出错误的同步操作,会返回拒绝期约

    async function foo() {
      console.log(1)
      await (()=> {throw 3})()
      console.log(4) // 不会执行
    }
    
    foo().catch(console.log)
    console.log(2)
    // 1
    // 2
    // 3
    复制代码
  • 单独的Promise.reject()不会被异步函数捕获,而会抛出未捕获错误。对拒绝的期约使用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只能直接出现在异步函数的定义中

2、停止和恢复执行

  • 顺序问题

    async function foo() {
      console.log(await Promise.resolve('foo'));
    }
    async function bar() {
      console.log(await 'bar');
    }
    async function baz() {
      console.log('baz');
    }
    foo();
    bar();
    baz();
    // baz
    // foo 
    // bar
    // baz bar foo (旧)
    复制代码
    • 异步函数里不包含await关键字,和普通函数没区别

    • 在运行时遇到await 时,会记录在哪里暂停执行,等到await右边的值可用了,会向消息队列中推送任务,这个任务会恢复异步函数执行

    • 即使await跟着立即可用的值,函数其他部分也是会异步求值

      async function foo() {
        console.log(2)
        await null
        console.log(4)
      } 
      console.log(1)
      foo()
      console.log(3)
      // 1 2 3 4
      复制代码
      1. 打印1
      2. 调用异步函数foo()
      3. 在foo() 中 打印 2
      4. 在foo() 中 await关键字暂停执行,为立即可用值null向消息队列添加任务
      5. foo() 退出
      6. 打印3
      7. 同步线程的代码执行完毕
      8. JavaScript运行时从消息队列中取出任务,恢复异步函数执行
      9. 在foo() 中恢复执行,await 取得null值 (没有使用)
      10. 在foo() 中 打印4
      11. foo() 返回
    • 若后面是一个期约,则为了执行异步函数,实际会有两个任务被添加到消息队列并被异步求值(新版浏览器只会生成一个异步任务)

      async function foo() {
        console.log(2)
        // 旧版会多一步添加恢复执行foo()函数的任务
        console.log(await Promise.resolve(8))
        console.log(9)
      }
      async function bar() {
        console.log(4)
        console.log(await 6)
        console.log(7)
      }
      console.log(1)
      foo()
      console.log(3)
      bar()
      console.log(5)
      // 123456789  旧
      // 123458967  新
      复制代码

3、异步函数策略

1、实现sleep()

async function sleep(delay) {
  return new Promise((resolve)=> setTimeout(resolve,delay))
}
async function foo() {
  const t0 = Date.now()
  await sleep(1500)
  console.log(Date.now() - t0)
}
foo()
// 1502
复制代码

2、利用平行执行

  • 平行加速的机会

    async function randomDelay(id) {
      // 延迟 0~1000 毫秒
      const delay = Math.random() * 1000;
      return new Promise((resolve) => setTimeout(() => {
        console.log(`${id} finished`);
        resolve();
      }, delay));
    }
    async function foo() {
      const t0 = Date.now();
      await randomDelay(0);
      await randomDelay(1);
      await randomDelay(2);
      await randomDelay(3);
      await randomDelay(4);
      console.log(`${Date.now() - t0}ms elapsed`);
    }
    foo();
     // 0 finished
     // 1 finished
     // 2 finished
     // 3 finished
     // 4 finished
     // 877ms elapsed 
    
    // for 循环
    async function foo() {
      const t0 = Date.now();
      for(let i = 0; i < 5; ++i) {
        await randomDelay(i)
      }
      console.log(`${Date.now() - t0}ms elapsed`);
    }
    复制代码
  • 如果期约之间没有依赖,异步函数也会依次暂停保证执行顺序,但总时间也变长。如果无需保证顺序 可以一次性初始化所有契约,分别等待结果。

    async function randomDelay(id) {
      // 延迟 0~1000 毫秒
      const delay = Math.random() * 1000;
      return new Promise((resolve) => setTimeout(() => {
        setTimout(console.log, 0, `${id} finished`);
        resolve();
      }, delay)); 
    }
    
    async function foo() {
      const t0 = Date.now()
      const p0 = randomDelay(0)
      const p1 = randomDelay(1)
      const p2 = randomDelay(2)
      const p3 = randomDelay(3)
      const p4 = randomDelay(4)
      await p0
      await p1
      await p2
      await p3
      await p4
      setTimeout(console.log, 0 ,`${Date.now() - t0}ms elapsed`);
    }
    foo()
    // 1 finished
    // 4 finished
    // 3 finished
    // 0 finished
    // 2 finished
    // 877ms elapsed 
    
    // for 循环
    async function foo() {
      const t0 = Date.now()
      const promises = Array(5).full(null).map((_, i)=>randomDelay(i))
      
      for(const p of promises) {
        await p
      }
      setTimeout(console.log, 0 ,`${Date.now() - t0}ms elapsed`);
    }
    复制代码
  • 虽然期约没有按照顺序执行,但await按顺序收到了每个期约的值

    async function randomDelay(id) {
      // 延迟 0~1000 毫秒
      const delay = Math.random() * 1000;
      return new Promise((resolve) => setTimeout(() => {
        console.log(`${id} finished`);
        resolve(id);
      }, delay));
    }
    async function foo() {
      const t0 = Date.now();
      const promises = Array(5).fill(null).map((_, i) => randomDelay(i));
      for (const p of promises) {
        console.log(`awaited ${await p}`);
      }
      console.log(`${Date.now() - t0}ms elapsed`);
    }
    foo();
    复制代码

3、串行执行期约

function addTow(x) {return x + 2}
function addThree(x) {return x + 2}
function addFive(x) {return x + 5}

async function addTen(x) {
  for(const fn of [addTow, addThree, addFive]) {
    x = await fn(x)
  }
  return x
}
addTen(9).then(console.log) // 19
复制代码

4、栈追踪与内存管理

  • 期约和异步函数在内存中的差别很大。

    function fooPromiseExecutor(resolve, reject) {
      setTimeout(reject, 1000, 'bar')
    }
    
    function foo() {
      new Promise(fooPromiseExecutor)
    }
    foo()
    // Uncaught (in promise) bar
    // setTimeout (async)  
    // fooPromiseExecutor
    // foo
    复制代码
    • 有些已经返回的函数,还是出现了。有计算和存储成本
    function fooPromiseExecutor(resolve, reject) {
      setTimeout(reject, 1000, 'bar');
    }
    async function foo() {
      await new Promise(fooPromiseExecutor);
    }
    foo();
    // Uncaught (in promise) bar
    // foo
    // async function (async)  
    // foo
    复制代码
    • JavaScript 运行时可以简单的在嵌套韩素中存储指向包含函数的指针,就和对待同步函数调用栈一样。
    • 这个指针实际上存储在内存中,可用于再出错事生成栈追踪信息。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享