JavaScript高级程序设计笔记11 期约与异步函数(Promise & Async Function)

期约与异步函数

ES6新增Promise引用类型,支持优雅地定义和组织异步逻辑。

ES8增加了使用async和await关键字定义异步函数的机制。

异步编程

JavaScript这种单线程事件循环模型

异步行为是为了优化因计算量大而时间长的操作。(在等待其他操作完成的同时,即时运行其他指令,系统也能保持稳定)

只要你不想为等待某个操作而阻塞线程执行,那么任何时候都可以使用(异步操作)。

同步与异步

同步:这样的执行流程容易分析程序在执行到代码任意位置时的状态。在程序执行的每一步,都可以推断出程序的状态。

let x = 3;
x = x + 4;

这两行代码大致对应的低级指令:1)操作系统在栈内存上分配一个存储浮点数值的空间;2)针对这个值做一次数学计算;3)把计算结果写回之前分配的内存中。

异步:类似于系统中断,即当前进程外部的实体可以触发代码执行。(场景:访问一些高延迟的资源)

let x = 3;
setTimeout(() => x = x + 4, 1000);

执行线程不知道x值何时会改变,这取决于回调何时从消息队列出列并执行。由系统计时器触发,这会生成一个入队执行的中断,什么时候会触发入队中断,对JavaScript运行时来说是一个黑盒,无法预知。

异步代码不容易推断。为了让后续代码能够使用x,异步执行的函数需要在更新x的值以后通知其他代码。(如果程序不需要这个值,可以不必等待结果)

以往的异步编程模式

在早期的JavaScript中,只支持定义回调函数来表明异步操作完成。串联多个异步操作是一个常见的问题,通常需要深度嵌套的回调函数(”回调地狱“)来解决。

setTimeout可以定义一个在指定时间之后会被调度执行的回调函数。

function double(value) {
  setTimeout(() => setTimeout(console.log, 0, value * 2), 1000);
}
double(3);

在运行到setTimeout时,JavaScript运行时开始工作,发现需要设置系统计时器,等到1000毫秒之后,触发执行入队中断,JavaScript运行时把回调函数推到其消息队列上等待执行。(回调什么时候出列被执行对JavaScript代码完全不可见)。double()函数在setTimeout成功调度异步操作(触发JavaScript运行时工作?)之后会立即退出。

  1. 异步返回值

    假设setTimeout的异步操作会提供一个有用的值。通常是给异步操作提供一个回调函数,这个回调函数中包含要使用异步返回值的代码(异步返回值作为回调函数的参数)。

    function double(value, callback) {
      setTimeout(() => callback(value * 2), 1000);
    }
    double(3, x => console.log(`I was given: ${x}`));
    

    这个函数会由运行时负责异步调度执行。位于函数闭包中的回调函数及其参数在异步执行时仍然是可用的。

  2. 失败处理

    异步操作的失败处理。成功回调和失败回调。

    这种模式已经不可取了,因为必须在初始化异步操作时定义回调

     function double(value, success, failure) {
     	setTimeout(()=> {
     		try {
     			if(typeof value !== 'number') {
     				throw 'Must provide number as first argument';
     			}
     			success(value * 2);
     		} catch(e) {
     			failure(e);
     		}
     	}, 1000);
     }
     const successCallback = (x) => console.log(`Success: ${x}`);
     const failureCallback = (e) => console.log(`Failure: ${e}`);
    
     double(3, successCallback, failureCallback);
     double('p', successCallback, failureCallback);
     // Success: 6
     // Failure: Must provide number as first argument
    

    异步函数的返回值只在短时间内存在,只有预备好将这个短时间内存在的值作为参数的回调函数才能接收到它。

  3. 嵌套异步回调

    如果异步返回值又依赖另一个异步返回值,那么回调的情况还会进一步变复杂。在实际的代码中,这就要求嵌套回调。

    不具有扩展性

期约(Promise)

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

一种异步程序执行的机制。

Promises/A+规范

早期的期约机制在jQuery和Dojo中是以Deferred API的形式出现的。为弥合现有实现之间的差异,2012年Promise/A+组织fork了CommonJS的Promise/A+建议,并以相同的名字制定了Promise/A+规范。这个规范最终成为了ECMAScript6规范实现的范本。

ES6增加了对Promise/A+规范的完善支持,即Promise类型。异步编程机制。很多其他浏览器API(如fetch()和电池API)也以期约为基础。

期约基本知识

可通过new操作符来实例化。创建新期约时,需要传入执行器函数(executor)作为参数。(如果不提供executor,会抛出SyntaxError)。

  1. 期约状态机(状态)

    期约是一个有状态的对象,可能处于3种状态之一

    • 待定(pending):最初始状态
    • 兑现(fulfilled,有时也称为”解决“,resolved)
    • 拒绝(rejected)

    在待定状态下,期约可以落定(settled)为代表成功的兑现(fulfilled)状态,或者代表失败的拒绝(rejected)状态。无论落定为哪种状态,都不可逆。也不能保证期约必然会脱离待定状态

    期约的状态是私有的,不能直接通过JavaScript检测到。(避免根据读取到的期约状态,以同步方式处理期约对象)。期约的状态也不能被外部JavaScript代码修改。

    期约将异步行为封装起来,从而隔绝外部的同步代码。

    let p1 = new Promise((resolve, reject) => resolve());
    setTimeout(console.log, 0, p1);
    // Promise {<fulfilled>: undefined}     Chrome控制台
    // Promise { undefined }       node运行
    let p2 = new Promise((resolve, reject) => reject());
    setTimeout(console.log, 0, p2);
    // Uncaught (in promise) undefined    Chrome控制台
    // Promise {<rejected>: undefined}
    // UnhandledPromiseRejectionWarning: undefined    node运行
    // Promise { <rejected> undefined }
    
  2. 解决值、拒绝理由及期约用例(用途)

    期约主要有两大用途:

    • 抽象地表示一个异步操作。期约的状态代表期约是否完成,兑现还是拒绝。

      某些情况下,这个状态机就是期约可以提供的最有用的信息。(知道一段异步代码已经完成)

    • 期约封装的异步操作会实际生成某个值,而程序期待期约状态改变时可以访问这个值,或是期约被拒绝时,期待期约状态改变时可以拿到拒绝的理由

    为了支持这两种用例,每个期约只要状态切换为兑现,就会有一个私有的内部值(value);每个期约只要状态切换为拒绝,就会有一个私有的内部理由(reason)。无论值还是理由,都是包含原始值或者对象的不可修改的引用。两者都为可选,且默认值为undefined。

    在期约到达某个落定状态时执行的异步代码会收到这个值或理由。

  3. 通过执行器函数控制期约状态(状态转换)

    期约的状态是私有的,所以只能在内部进行操作。

    内部操作在期约的执行器函数中完成。

    执行器函数主要有两项职责初始化期约的异步行为和控制状态的最终转换。控制期约状态的转换是通过调用它的两个函数参数实现的。这两个函数参数通常命名为resolve()和reject()。调用reject()会抛出错误

    执行器函数是同步执行的;因为执行器函数是期约的初始化程序。

    无论resolve()和reject()中的哪个被调用,状态转换都不可撤销。继续修改状态会静默失败

    为避免期约卡在待定状态,可以添加一个定时退出功能。如,通过setTimeout设置一个10秒后无论如何都会拒绝期约的回调。(超时处理)

    /* executor函数在其他同步代码执行之后会同步执行 */
    new Promise(()=> setTimeout(console.log, 0, 'executor'));
    setTimeout(console.log, 0, 'promise initialized');
    // executor
    // promise initialized
    
    /*
    let p = new Promise((resolve, reject) => setTimeout(resolve, 1000));
    setTimeout(console.log, 0, p); // Promise { <pending> }*/
    
    /* 落定的状态不可撤销
    let p = new Promise((resolve, reject) => {
    	resolve();
    	reject();
    });
    setTimeout(console.log, 0, p); // Promise {<fulfilled>: undefined}*/
    
    /* 定时退出,避免期约卡在待定状态
    let p = new Promise((resolve, reject) => {
    	setTimeout(reject, 10000);
    });
    setTimeout(console.log, 0, p);
    setTimeout(console.log, 11000, p);
    // Promise {<pending>}
    // (10秒后)
    // Uncaught (in promise) undefined
    // (1秒后)
    // Promise {<rejected>: undefined}*/
    
  4. 同步/异步执行的二元性(交互)

    两种模式下抛出错误的处理。通过try/catch无法捕获reject的错误。没有通过异步模式捕获错误。

    期约真正的异步特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式的媒介。

    拒绝期约的错误并没有抛到执行同步代码的线程里,而是通过浏览器异步消息队列来处理的。代码一旦开始以异步模式执行,唯一与之交互的方式就是使用异步结构——期约的方法

期约的静态方法

  • Promise.resolve()

    可以实例化一个解决的期约。

    这个解决的期约的值对应着传给Promise.resolve()的第一个参数,多余的参数会被忽略。实际上,这个静态方法可以把任何值都转换为一个期约。

    • 参数是一个期约:相当于透传,得到的期约为参数本身
    • 参数是非期约值(包括错误对象):将参数转换为解决的期约
    /*
    // Promise.resolve
    // 参数为期约值时,等于透传=>幂等
    // 参数为非期约值时,将其转换为解决的期约(实例化一个解决的期约)
    let p1 = new Promise((resolve, reject) => resolve());
    let p2 = Promise.resolve();
    setTimeout(console.log, 0, p1);
    // Promise {<fulfilled>: undefined}
    // 	__proto__: Promise
    // 	[[PromiseState]]: "fulfilled"
    // 	[[PromiseResult]]: undefined
    setTimeout(console.log, 0, Promise.resolve(3));
    // Promise {<fulfilled>: 3}
    // 	__proto__: Promise
    // 	[[PromiseState]]: "fulfilled"
    // 	[[PromiseResult]]: 3
    
    console.log(p1 === Promise.resolve(p1)); // true
    console.log(Promise.resolve(9)); // Promise {<fulfilled>: 9}*/
    
  • Promise.reject()

    可以实例化一个拒绝的期约并抛出一个异步错误(不能通过try/catch捕获)。

    这个拒绝的期约的理由就是传给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
    
    p = Promise.reject(Promise.resolve());
    // Promise {<rejected>: Promise}
    // Uncaught (in promise) Promise {<fulfilled>: undefined}
    setTimeout(console.log, 0, p); // Promise { <rejected> Promise { undefined } }
    // Promise {<rejected>: Promise}
    p.then(null, (e)=>setTimeout(console.log, 0, e)); // Promise {<fulfilled>: undefined}
    

期约的实例方法

期约实例的方法是连接外部同步代码与内部异步代码之间的桥梁

这些方法可以访问异步操作返回的数据,处理期约成功和失败的输出(onResolved或onRejected),连续对期约求值(连续调用then、catch或finally),或者添加只有期约进入终止状态时才会执行的代码(onFinally)。

  1. Thenable接口

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

    ECMAScript的Promise类型实现了Thenable接口。

  2. Promise.prototype.then()

    为期约实例添加处理程序的主要方法接收最多两个参数:onResolved处理程序和onRejected处理程序。都是可选的,如果提供的话,则会在期约分别进入”兑现“和”拒绝“状态时执行。

    因为期约只能转换为最终状态一次,所以这两个操作一定是互斥的。传给then()的任何非函数类型的参数都会被静默忽略。

    如果想只提供onRejected参数,就要在onResolved参数的位置上传入null。=>有助于避免在内存中创建多余的对象,对期待函数参数的类型系统也是一个交代。

    返回一个新的期约实例

    • 若期约还没有落定,就是一个pending中的期约实例。

    • 若期约实例的状态落定为resolve

      1. 没有onResolved处理程序,返回期约实例的副本期约(不相等,但状态同步)。

      2. 有就使用onResolved处理程序的返回值,通过Promise.resolve()包装来生成新期约(非期约值包装成解决的期约,期约值包装成它的副本期约)。

        如果没有显式的返回值,就包装默认的返回值undefined。

        如果onResolved处理程序抛出异常(throw),就返回一个拒绝的期约实例。

    • 若期约实例的状态落定为reject,就使用onRejected处理程序的返回值,通过Promise.resolve()包装来生成新期约。

      1. 没有onRejected处理程序,返回期约实例的副本期约(不相等,但状态同步)。

      2. 有就使用onRejected处理程序的返回值,通过Promise.resolve()包装来生成新期约(非期约值包装成解决的期约,期约值包装成它的副本期约)。

        如果没有显式的返回值,就包装默认的返回值undefined。

        如果onRejected处理程序抛出异常(throw),就返回一个拒绝的期约实例。

    /*
    Promise.prototype.then()
    返回一个新的期约实例。
    若期约还没有落定,就是一个pending中的期约实例。
    
    若期约实例的状态落定为resolve,就是被onResolved处理程序的返回值 通过Promise.resolve()包装来生成新期约。
    如果onResolve处理程序抛出异常(throw),就返回拒绝的期约
    
    若期约实例的状态落定为reject,就是被onRejected处理程序的返回值 通过Promise.resolve()包装来生成新期约。
     */
    // 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, 1000));
    // let p2 = new Promise((resolve, reject) => setTimeout(reject, 1000));
    // /*p1.then('not function test');*/ p1.then(() => onResolved('p1'), () => onRejected('p1')); // p1 resolved
    // p2.then(null, () => onRejected('p2')); // p2 rejected
    
    /*
    let p1 = Promise.resolve('foo');
    // 没有onResolve处理程序,通过Promise.resolve()包装期约实例来生成新期约。
    let p2 = p1.then();
    setTimeout(console.log, 0, p1); // Promise { 'foo' }
    setTimeout(console.log, 0, p2); // Promise { 'foo' }
    console.log(p1 === p2); // false
    // onResolve处理程序没有显式返回,就把默认的返回值undefined包装成期约
    // 否则,就把返回的值包装成期约(非期约值包装成解决的期约,期约值包装成它的副本期约)
    // 如果处理程序抛出异常(throw),就返回拒绝的期约
    let p3 = p1.then(() => undefined);
    let p4 = p1.then(() => {}); // 无返回值
    let p5 = p1.then(() => Promise.resolve());
    setTimeout(console.log, 0, p3); // Promise { undefined } => Promise.resolve(undefined)
    setTimeout(console.log, 0, p4); // Promise { undefined } => Promise.resolve(undefined)
    setTimeout(console.log, 0, p5); // Promise { undefined } => Promise.resolve(Promise.resolve())
    let p6 = p1.then(() => 'bar');
    let p7 = p1.then(() => Promise.resolve('bar'));
    setTimeout(console.log, 0, p6); // Promise { 'bar' } => Promise.resolve('bar')
    setTimeout(console.log, 0, p7); // Promise { 'bar' } => Promise.resolve(Promise.resolve('bar'))
    let p8 = p1.then(() => new Promise(() => {}));
    setTimeout(console.log, 0, p8); // Promise { <pending> }
    let p9 = p1.then(() => Promise.reject());
    setTimeout(console.log, 0, p9); // Promise { <rejected> undefined }
    let p10 = p1.then(() => { throw 'baz'});
    setTimeout(console.log, 0, p10); // Promise { <rejected> 'baz' }
    let p11 = p1.then(() => Error('qux'));
    setTimeout(console.log, 0, p11);
    // Promise {
    // 	Error: qux
    // 	at .../c11-promise+async.js:155:25
    // 	at processTicksAndRejections (internal/process/task_queues.js:93:5)
    // }
    */
    
    /*
    let p1 = Promise.reject('foo');
    // 没有onRejected处理程序,p2就是p1的副本期约
    let p2 = p1.then();
    // Uncaught (in promise) foo
    setTimeout(console.log, 0, p2); // Promise {<rejected>: "foo"}
    let p3 = p1.then(null, () => undefined);
    let p4 = p1.then(null, () => {});
    let p5 = p1.then(null, () => Promise.resolve());
    setTimeout(console.log, 0, p3); // Promise {<fulfilled>: undefined}
    setTimeout(console.log, 0, p4); // Promise {<fulfilled>: undefined}
    setTimeout(console.log, 0, p5); // Promise {<fulfilled>: undefined}
    let p6 = p1.then(null, () => 'bar');
    let p7 = p1.then(null, () => Promise.resolve('bar'));
    setTimeout(console.log, 0, p6); // Promise {<fulfilled>: "bar"}
    setTimeout(console.log, 0, p7); // Promise {<fulfilled>: "bar"}
    let p8 = p1.then(null, () => new Promise(() => {}));
    setTimeout(console.log, 0, p8); // Promise {<pending>}
    let p9 = p1.then(null, () => Promise.reject());
    // Uncaught (in promise) undefined
    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
    //     at <anonymous>:3:31}*/
    
  3. Promise.prototype.catch()

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

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

    返回一个新的期约实例

    /*
        Promise.prototype.catch()
    let pp = Promise.reject();
    let onRejected = function (e) {
        setTimeout(console.log, 0, 'rejected');
    };
    pp.then(null, onRejected); // rejected
    pp.catch(onRejected); // rejected
    let p1 = new Promise(() => {});
    let p2 = p1.catch();
    setTimeout(console.log, 0, p2); // Promise { <pending> }*/
    
  4. Promise.prototype.finally()

    用于给期约添加onFinally处理程序。这个处理程序在期约转换为解决或拒绝状态时都会执行。可以避免onResolved和onRejected处理程序中出现冗余代码。不知道期约的状态是解决还是拒绝,主要用于添加清理代码

    返回一个新期约实例

    多数情况下,返回的是原期约实例的副本
    若onFinally返回的是一个待定的期约,或是拒绝的期约(或抛出异常),则返回相应状态的期约实例。如果返回的待定的期约状态落定了,新期约还是会转换为原期约实例的副本。

    /*
    * Promise.prototype.finally()
    * 返回一个新期约实例。
    * 多数情况下,返回的是原期约实例的副本;
    * 若onFinally返回的是一个待定的期约,或是拒绝的期约(或抛出异常),则返回相应状态的期约实例。如果待定的期约状态落定了,新期约还是会转换为原期约实例的副本。
    * */
    let p1 = Promise.resolve();
    let p2 = Promise.reject();
    let onFinally = function () {
        setTimeout(console.log, 0, 'Finally!');
    };
    p1.finally(onFinally); // Finally!
    p2.finally(onFinally); // Finally!
    /*
    p1 = Promise.resolve('foo');
    p2 = p1.finally(() => new Promise((resolve, reject)=> setTimeout(()=> resolve('bar'), 100)));
    setTimeout(console.log, 0, p2); // Promise { <pending> }
    setTimeout(()=> setTimeout(console.log, 0, p2), 200); // Promise { 'foo' }*/
    
  5. 执行顺序

    当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行。即使期约一开始就是与附加处理程序关联的状态,执行顺序也是这样的。这个特性由JavaScript运行时保证,被称为”非重入“(non-reentrancy)特性。

    跟在实例方法之后的同步代码,一定会在处理程序之前先执行。

    在一个解决期约上调用then()会把onResolved处理程序推进消息队列。(在当前线程上的同步代码执行完成前不会执行)。处理程序会等到运行的消息队列让它出列时才会执行。

    如给期约添加了多个处理程序,当期约状态变化时,相关处理程序会按照添加它们的顺序依次执行。

  6. 传递解决值和拒绝理由

    到了落定状态后,期约会提供其解决值(兑现)或其拒绝理由(拒绝)给相关状态的处理程序

    在执行器函数(executor)中,解决的值和拒绝的理由是分别作为resolve()和reject()的第一个参数往后传的。然后,这些值会传给它们各自的处理程序,作为onResolved或onRejected处理程序的唯一参数。

    Promise.resolve()和Promise.reject()在被调用时就会接收解决值和拒绝理由。它们返回的期约也会像执行器一样把这些值传给onResolved或onRejected处理程序。

    /*
    p1 = new Promise(((resolve, reject) => resolve('foo')));
    p1.then((value => console.log(value))); // foo
    p2 = new Promise(((resolve, reject) => reject('bar')));
    p2.catch(reason => console.log(reason)); // bar*/
    
  7. 拒绝期约与拒绝错误处理

    拒绝期约类似于throw()表达式,因为它们都代表一种程序状态,即需要中断或者特殊处理。

    在期约的执行器函数(executor)或处理程序中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由

    期约可以以任何理由拒绝,包括undefined,但最好统一使用错误对象。(创建错误对象可以让浏览器捕获错误对象中的栈追踪信息,而这些信息对调试是非常关键的。)

    在期约中抛出错误时(throw Error('error message');),因为错误实际上是从消息队列中异步抛出的,所以并不会阻止运行时继续执行同步指令。异步错误只能通过异步的onRejected处理程序捕获

    注!:在解决或拒绝期约之前,仍然可以使用try/catch在执行函数中捕获错误。

    onRejected处理程序在语义上相当于try/catch。出发点都是捕获错误之后将其隔离,同时不影响正常逻辑执行。故,onRejected处理程序的任务应该是在捕获异步错误之后返回一个解决的期约。

期约连锁与期约合成

  1. 期约连锁

    把期约逐个地串联起来。(每个期约实例的方法都会返回一个新的期约对象)

    每个处理程序都返回一个期约实例。就可以让每个后续期约都等待之前的期约,也就是串行化异步任务。解决之前依赖回调的难题(回调地狱),直观。

    let pp1 = new Promise(((resolve, reject) => {
        console.log('p1 executor');
        setTimeout(resolve, 1000);
    }));
    pp1.then(() => new Promise((resolve, reject) => {
        console.log('p2 executor');
        setTimeout(resolve, 1000);
    }))
        .then(() => new Promise(((resolve, reject) => {
            console.log('p3 executor');
            setTimeout(resolve, 1000);
        })))
        .then(() => new Promise(((resolve, reject) => {
            console.log('p4 executor');
            setTimeout(resolve, 1000);
        })));
    
    function delayedExecute(str, callback=null) {
        setTimeout(()=> {
            console.log(str);
            callback && callback();
        }, 1000);
    }
    delayedExecute('p1 callback', ()=> {
        delayedExecute('p2 callback', ()=> {
            delayedExecute('p3 callback', ()=> {
                delayedExecute('p4 callback');
            });
        });
    });
    
  2. 期约图

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

    图中的每个节点都会等待前一个节点落定,所以图的方向就是期约的解决或拒绝顺序。

    由于期约的处理程序是添加到消息队列,然后才逐个执行,因此构成了层序遍历。

    // 有向非循环。层序遍历。
    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'));
    A.then(()=> console.log('H'));
    // A
    // B
    // C
    // H
    // D
    // E
    // F
    // G
    
  3. Promise.all()和Promise.race()

    将两个或多个期约实例组合成一个期约的静态方法。

    • Promise.all()

      创建的期约会在一组期约全部解决之后再解决。接收一个可迭代对象(必传),返回一个新期约

      Promise.all([])等价于Promise.resolve([])

      可迭代对象中的(非期约)元素会通过Promise.resove()转换为期约。

      • 每个包含的期约都解决才解决。合成期约的解决值就是所有包含期约解决值的数组,按照迭代器顺序。
      • 如果至少有一个包含的期约待定,则合成的期约也会待定(无拒绝期约时)
      • 如果有一个包含的期约拒绝,则合成的期约也会拒绝。第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由。其他包含期约的拒绝操作会被静默处理,经过onRejected处理程序后,不会抛出异步错误。
      let ppp = new Promise(((resolve, reject) => reject('ppp')));
      let ppp1 = new Promise(((resolve, reject) => reject('ppp1')));
      let all = Promise.all([
          new Promise(((resolve, reject) => setTimeout(reject, 1000))),
          ppp,
          ppp1
      ]);
      all.catch(reason => setTimeout(console.log, 0, 2, reason));
      ppp1.then((value)=> console.log(value), (reason)=> console.log(11, reason));
      ppp.then((value)=> console.log(value), (reason)=> console.log(1, reason));
      // 11 "ppp1"
      // 1 "ppp"
      // 2 "ppp"
      
    • Promise.race()

      接收一个可迭代对象(必传),返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像。

      可迭代对象中的(非期约)元素会通过Promise.resove()转换为期约。

      Promise.race([])等价于new Promise(()=>{})

      只要是第一个落定的期约,Promise.race()就会包装其解决值或拒绝理由并返回新期约。

      所有包含期约的拒绝操作会被静默处理,经过onRejected处理程序后,不会抛出异步错误。

  4. 串行期约合成

    期约连锁:期约的串行执行。期约的另一个主要特性:异步产生值并将其传给处理程序。基于后续期约使用之前期约的返回值来串联期约是期约的基本功能。

    可以提炼出一个通用函数,把任意多个函数作为处理程序合成一个连续传值的期约连锁。这个通用的合成函数可以这样实现:

    function compose(...fns) {
      return x => fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(x));
    }
    
     // 使用
     function addTwo(x) { return x + 2 };
     function addThree(x) { return x + 3 };
     function addFive(x) { return x + 5 };
     let addTen = compose(addTwo, addFive, addThree);
     addTen(8).then(console.log); // 18
    

期约扩展

ES6期约的不足之处:期约取消和进度追踪。

  1. 期约取消

    场景:期约正在处理过程中,程序却不再需要其结果。

    ES6期约:只要期约的逻辑开始执行,就没有办法阻止它执行到完成。

    实现:可以在现有实现基础上提供一种临时性的封装,以实现取消期约的功能。”取消令牌“(cancel token)。

    class CancelToken {
      constructor(cancelFn) {
        this.promise = new Promise((resolve, reject) => {
          cancelFn(resolve);
        })
      }
    }
    
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Promise Extend</title>
    </head>
    <body>
    <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);
                // 实例化一个cancelToken的实例,关联了一个Promise实例。
                // 这个Promise实例初始化时,执行器函数给cancel按钮添加了click事件监听回调函数。
                // 当cancel按钮点击时,关联的Promise实例就被兑现了,并打印出"delay cancelled"。
                const cancelToken = new CancelToken((cancelCallback) => cancelButton.addEventListener("click", cancelCallback));
                // 当cancelToken实例关联的Promise实例被兑现后,then对应的处理程序中清空计时器,当前的Promise实例的状态就不会被落定兑现。
                cancelToken.promise.then(() => clearTimeout(id));
            })
        }
        // 给start按钮添加了click事件监听回调函数
        // 当start按钮点击时,cancellableDelayedResolve函数被执行,
        // 打印出"set delay",并通过执行器函数初始化一个Promise实例,
        // 1000毫秒后该Promise实例会被兑现。
        startButton.addEventListener("click", () => cancellableDelayedResolve(1000));
    </script>
    </body>
    </html>
    

    在一个Promise实例的执行器中,初始化一个令牌实例,通过触发令牌实例中的期约解决,,在其onResolved处理程序中取消执行这个Promise实例的resolve。

  2. 进度追踪

    场景:监控期约的执行进度。ES6期约不支持进度追踪。

    class TrackablePromise extends Promise {
        constructor(executor) {
            const notifyHandlers = [];
            super((resolve, reject) => {
                return executor(resolve, reject, (status) => {
                    notifyHandlers.map((handler) => handler(status)); // 所有的notifyHandlers执行一遍
                });
            });
            this.notifyHandlers = notifyHandlers;
        }
        notify(notifyHandler) {
            this.notifyHandlers.push(notifyHandler);
            return this;
        }
    }
    
    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))
     .notify(x => setTimeout(console.log, 0, 'progress1:', x));
    p.then(() => setTimeout(console.log, 0, 'completed'));
    

ES6不支持取消期约和进度追踪,一个主要原因是这样会导致期约连锁和期约合成过度复杂化。

异步函数(async/await)

跟期约结合,以同步方式编写异步代码,方便错误的捕获和控制。

ES8新增。从行为和语法上增强JavaScript。

如果程序中的其他代码要在一个期约的解决值可用时访问它,则需要写一个解决处理程序。=>其他代码都必须赛到期约处理程序中、以处理程序的形式来接收这个值。

异步函数

解决利用异步结构组织代码的问题

  1. async

    用于声明异步函数。始终返回期约对象。

    可以让函数具有异步特征,但总体上其代码仍然是同步求值。(类似Promise实例的执行器函数?)

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

    如果异步函数使用return关键字返回了值(没有显式的return默认返回undefined),这个值会被Promise.resolve()包装成一个期约对象。

    async function foo() {
        console.log(1);
        return Promise.resolve(3); // 3;
    }
    foo().then(console.log);
    console.log(2);
    
    async function bar() {
        return ['bar'];
    }
    bar().then(console.log);
    // 1
    // 2
    // [ 'bar' ]
    // 3
    /*
    1.先执行同步代码,打印1,把Promise.resolve落定后执行的任务插入消息队列
    2.执行同步代码,打印2
    3.把bar().then()的任务插入消息队列
    4.完成Promise.resolve落定后执行的任务出列执行,把foo().then()的任务插入消息队列
    5.把bar().then()的任务出列执行:打印['bar']
    6.把foo().then()的任务出列执行:打印3
     */
    

    异步函数的返回值期待一个实现Thenable接口的对象,但常规的值也可。如果返回的是实现Thenable接口的对象,可以由提供给then()的处理程序”解包“;如果不是,则返回值就被当作已经解决的期约。

    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
    

    在异步函数中抛出错误,会返回拒绝的期约;如果出现某落定为拒绝的期约实例(不是返回值),就会抛出异步错误(不能通过异步函数().catch()捕获到)。

    async function foo1() {
        console.log(11);
        throw 33;
    }
    foo1().catch(console.log); // 33 onRejected处理程序打印
    
    async function foo2() {
        console.log(111);
        // console.log( 'await', await Promise.resolve(333) );
        Promise.reject(333);
        console.log(334);
    }
    foo2().catch(console.log);
    console.log(222);
    // 111
    // 334
    // 222
    // (node:86722) UnhandledPromiseRejectionWarning: 333
    
    // 如果在Promise.reject(333); 前面加上await结果就不一样了
    // 111
    // 222
    // 333 
    // 334的打印不会被执行到
    
  2. await

    因为异步函数主要针对不会马上完成的任务,所以需要一种暂停和恢复执行的能力

    async function foo() {
        console.log(await Promise.resolve('foo'));
    }
    foo(); // foo
    

    使用await关键字可以暂停异步函数代码的执行,等待期约解决。与生成器函数中的yield关键字是一样的。

    async function bar() {
        return await Promise.resolve('bar');
    }
    bar().then(console.log); // bar
    
    async function baz() {
        await new Promise((resolve, reject) => setTimeout(resolve, 1000));
        console.log('baz');
    }
    baz(); // (1秒后)baz
    

    await关键字同样尝试”解包“对象的值,然后将这个值传给表达式,再异步恢复异步函数的执行。(可以单独使用,也可以在表达式中使用)。

    async function foo1() {
        console.log(await 'foo1');
    }
    foo1();
    async function bar1() {
        console.log(await ['bar1']);
    }
    bar1();
    

    await关键字期待一个实现Thenable接口的对象,但常规的值也可。如果是实现Thenable接口的对象,则这个对象可以由await来”解包“;如果不是,这个值就被当作已经解决的期约。

    async function baz1() {
        const thenable = {
            then(callback) {
                callback('baz1');
            }
        };
        console.log(await thenable);
    }
    baz1();
    

    await后跟会抛出错误的同步操作(或者某落定为拒绝的期约实例),异步函数会返回拒绝的期约(拒绝理由为抛出的错误信息或拒绝的期约的理由);后续的代码不会被执行。

    async function fooT() {
        console.log(1);
        await (() => {
            throw 33;
        })();
    }
    fooT().catch(console.log);
    console.log(2);
    
  3. await的限制

    await关键字必须在异步函数中使用。

    异步函数的特质不会扩展到嵌套函数。因此,await关键字只能直接出现在异步函数的定义中。在同步函数内部使用await会抛出SyntaxError。

停止和恢复执行

async/await中真正起作用的是await。异步函数如果不包含await关键字,其执行基本上跟普通函数没有什么区别。

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

所以,即使await后面跟着一个立即可用的值,函数的其余部分也会被异步求值。

如果await后面是一个期约,则问题会稍微复杂一些。为了执行异步函数,实际上会有两个任务被添加到消息队列并被异步求值。(Promise落定后执行的任务,给await提供值的任务)

async function test() {
    console.log(await Promise.resolve().then(() => 'test'));
}
test();

(async function() {
    console.log(await Promise.resolve(331));
})();

// test
// 331

异步函数策略

  1. 实现类似Java中的Thread.sleep()

    在程序中加入非阻塞的暂停。不影响外部的同步代码执行。

    async function sleep(delay) {
        return new Promise((resolve => setTimeout(resolve, delay)));
    }
    async function foox() {
        const t0 = Date.now();
        await sleep(1500);
        console.log('diff', Date.now() - t0); // diff 1504
    }
    console.log(5);
    foox();
    
  2. 利用平行执行

    平行加速。(并行执行,顺序使用结果,有点像Promise.all?)

    就算期约之间没有依赖,异步函数中的await也会依次暂停,等待每个完成。这样可以保证执行顺序,但总执行时间会变长。

    如果顺序不是必需保证的,则可以先一次性初始化所有期约,然后再分别等待它们的结果。

    期约虽然没有按照顺序执行,但是await按顺序收到了每个期约的值。

    async function randomDelay(id) {
        const delay = Math.random() * 1000;
        return new Promise((resolve) => setTimeout(() => {
            console.log(`${id} finished`);
            resolve(id);
        }, delay));
    }
    async function fooy() {
        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;*/
        const promises = Array(5).fill(null).map((_, i) => randomDelay(i));
        for (const p of promises) {
            console.log(`awaited ${await p}`);
        }
        /*for(let i = 0; i < 5; ++ i) {
            await randomDelay(i);
        }*/
        console.log(`${Date.now() - t0}ms elapsed`);
    }
    fooy();
    
  3. 串行执行期约

    前面Promise部分,串行执行期约并把值传给后续的期约。

    await直接传递了每个函数的返回值(onResolved处理程序接收到的参数),结果通过迭代产生。

    function compose(...fns) {
      return async (x)=> {
        for (const fn of fns) {
          x = await fn(x);
        }
        return x;
      }
    }
    
     /*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
    
  4. 栈追踪与内存管理

    期约与异步函数的功能有相当程度的重叠,但它们在内存中的表示则差别很大。

    • 拒绝期约的栈追踪信息:

      包含嵌套函数(执行器函数)的标识符,这些函数已经返回,因此栈追踪信息中不应该看到它们。(JavaScript引擎会在创建期约时尽可能保留完整的调用栈。)意味着栈追踪信息会占用内存,从而带来一些计算和存储成本。

    • 异步函数的栈追踪信息:

      引用的Promise实例的执行器函数不在错误信息中。但异步函数此时被挂起,并没有退出。JavaScript运行时可以简单地在嵌套函数中存储指向包含函数(异步函数?)的指针。

小结

可以针对异步行为,写出更清晰、简洁,并且容易理解、调试的代码。

期约主要功能,为异步代码提供了清晰的抽象。可以用期约表示异步执行的代码块,也可以用期约表示异步计算的值。需要串行异步代码时,期约可以连锁使用、复合、扩展和重组。

异步函数可以暂停执行,而不阻塞主线程。

posted @ 2022-10-25 00:15  beckyye  阅读(72)  评论(0编辑  收藏  举报