你向往什么,就向着那里努力;你期待什么,就全身心地追寻。只要你不认输,就没有什么能阻挡你的脚步。
今日学习内容 1、JS 红皮书 P345-352 第十一章:期约与异步函数
今日笔记 1、期约扩展: ES6 期约实现是很可靠的,但它也有不足之处。比如,很多第三方期约库实现中具备而 ECMAScript 规范却未涉及的两个特性:期约取消和进度追踪。
期约取消: 我们经常会遇到期约正在处理过程中,程序却不再需要其结果的情形。这时候如果能够取消期约就好了。某些第三方库,比如 Bluebird,就提供了这个特性。实际上,TC39 委员会也曾准备增加这个特性,但相关提案最终被撤回了。结果,ES6 期约被认为是“激进的”:只要期约的逻辑开始执行,就没有办法阻止它执行到完成。 实际上,可以在现有实现基础上提供一种临时性的封装,以实现取消期约的功能。这可以用到 Kevin Smith 提到的“取消令牌”(cancel token)。生成的令牌实例提供了一个接口,利用这个接口可以取消期约;同时也提供了一个期约的实例,可以用来触发取消后的操作并求值取消状态
1 2 3 4 5 6 7 class CancelToken { constructor (cancelFn ) { this .promise = new Promise ((resolve, reject ) => { cancelFn (resolve); }); } }
这个类包装了一个期约,把解决方法暴露给了 cancelFn 参数。这样,外部代码就可以向构造函数中传入一个函数,从而控制什么情况下可以取消期约。这里期约是令牌类的公共成员,因此可以给它添加处理程序以取消期约。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <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”按钮设置的超时也会被取消。 2、期约进度通知: 执行中的期约可能会有不少离散的“阶段”,在最终解决之前必须依次经过。某些情况下,监控期约的执行进度会很有用。ECMAScript 6 期约并不支持进度追踪,但是可以通过扩展来实现。一种实现方式是扩展 Promise 类,为它添加 notify()方法,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 ; } }
这样,TrackablePromise 就可以在执行函数中使用 notify()函数了。可以像下面这样使用这个函数来实例化一个期约:
1 2 3 4 5 6 7 8 9 10 11 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 ); });
这个期约会连续 5 次递归地设置 1000 毫秒的超时。每个超时回调都会调用 notify()并传入状态值。假设通知处理程序简单地这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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" ));
notify()函数会返回期约,所以可以连缀调用,连续添加处理程序。多个处理程序会针对收到的每条消息分别执行一遍,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 p.notify ((x ) => setTimeout (console .log , 0 , "a:" , x)).notify ((x ) => setTimeout (console .log , 0 , "b:" , x) ); p.then (() => setTimeout (console .log , 0 , "completed" ));
3、异步函数: 异步函数,也称为“async/await”(语法关键字),是 ES6 期约模式在 ECMAScript 函数中的应用。 下面来看一个最简单的例子,这个期约在超时之后会解决为一个值:
1 let p = new Promise ((resolve, reject ) => setTimeout (resolve, 1000 , 3 ));
这个期约在 1000 毫秒之后解决为数值 3。如果程序中的其他代码要在这个值可用时访问它,则需要写一个解决处理程序:
1 2 let p = new Promise ((resolve, reject ) => setTimeout (resolve, 1000 , 3 ));p.then ((x ) => console .log (x));
这其实是很不方便的,因为其他代码都必须塞到期约处理程序中。不过可以把处理程序定义为一个函数:
1 2 3 4 5 function handler (x ) { console .log (x); } let p = new Promise ((resolve, reject ) => setTimeout (resolve, 1000 , 3 ));p.then (handler);
异步函数: ES8 的 async/await 旨在解决利用异步结构组织代码的问题。为此,ECMAScript 对函数进行了扩展,为其增加了两个新关键字:async 和 await。 async 关键字用于声明异步函数。这个关键字可以用在函数声明、函数表达式、箭头函数和方法上:
1 2 3 4 5 6 async function foo ( ) {}let bar = async function ( ) {};let baz = async ( ) => {};class Qux { async qux ( ) {} }
使用 async 关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。而在参数或闭包方面,异步函数仍然具有普通 JavaScript 函数的正常行为。正如下面的例子所示,foo()函数仍然会在后面的指令之前被求值:
1 2 3 4 5 6 7 async function foo ( ) { console .log (1 ); } foo ();console .log (2 );
不过,异步函数如果使用 return 关键字返回了值(如果没有 return 则会返回 undefined),这个值会被 Promise.resolve()包装成一个期约对象。异步函数始终返回期约对象。在函数外部调用这个函数可以得到它返回的期约:
1 2 3 4 5 6 7 8 9 10 async function foo ( ) { console .log (1 ); return 3 ; } foo ().then (console .log );console .log (2 );
当然,直接返回一个期约对象也是一样的:
1 2 3 4 5 6 7 8 9 10 async function foo ( ) { console .log (1 ); return Promise .resolve (3 ); } foo ().then (console .log );console .log (2 );
异步函数的返回值期待(但实际上并不要求)一个实现 thenable 接口的对象,但常规的值也可以。如果返回的是实现 thenable 接口的对象,则这个对象可以由提供给 then()的处理程序“解包”。如果不是,则返回值就被当作已经解决的期约。下面的代码演示了这些情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 async function foo ( ) { return "foo" ; } foo ().then (console .log );async function bar ( ) { return ["bar" ]; } bar ().then (console .log );async function baz ( ) { const thenable = { then (callback ) { callback ("baz" ); }, }; return thenable; } baz ().then (console .log );async function qux ( ) { return Promise .resolve ("qux" ); } qux ().then (console .log );
与在期约处理程序中一样,在异步函数中抛出错误会返回拒绝的期约:
1 2 3 4 5 6 7 8 9 10 async function foo ( ) { console .log (1 ); throw 3 ; } foo ().catch (console .log );console .log (2 );
不过,拒绝期约的错误不会被异步函数捕获:
1 2 3 4 5 6 7 8 9 10 async function foo ( ) { console .log (1 ); Promise .reject (3 ); } foo ().catch (console .log );console .log (2 );
await: 因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力。使用 await 关键字可以暂停异步函数代码的执行,等待期约解决。来看下面这个本章开始就出现过的例子:
1 2 3 4 5 6 7 8 9 let p = new Promise ((resolve, reject ) => setTimeout (resolve, 1000 , 3 ));p.then ((x ) => console .log (x)); async function foo ( ) { let p = new Promise ((resolve, reject ) => setTimeout (resolve, 1000 , 3 )); console .log (await p); } foo ();
注意,await 关键字会暂停执行异步函数后面的代码,让出 JavaScript 运行时的执行线程。这个行为与生成器函数中的 yield 关键字是一样的。await 关键字同样是尝试“解包”对象的值,然后将这个值传给表达式,再异步恢复异步函数的执行。 await 关键字的用法与 JavaScript 的一元操作一样。它可以单独使用,也可以在表达式中使用,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 async function foo ( ) { console .log (await Promise .resolve ("foo" )); } foo ();async function bar ( ) { return await Promise .resolve ("bar" ); } bar ().then (console .log );async function baz ( ) { await new Promise ((resolve, reject ) => setTimeout (resolve, 1000 )); console .log ("baz" ); } baz ();
await 关键字期待(但实际上并不要求)一个实现 thenable 接口的对象,但常规的值也可以。如果是实现 thenable 接口的对象,则这个对象可以由 await 来“解包”。如果不是,则这个值就被当作已经解决的期约。下面的代码演示了这些情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 async function foo ( ) { console .log (await "foo" ); } foo ();async function bar ( ) { console .log (await ["bar" ]); } bar ();async function baz ( ) { const thenable = { then (callback ) { callback ("baz" ); }, }; console .log (await thenable); } baz ();async function qux ( ) { console .log (await Promise .resolve ("qux" )); } qux ();async function foo ( ) { console .log (1 ); await (() => { throw 3 ; })(); } foo ().catch (console .log );console .log (2 );async function foo ( ) { console .log (1 ); await Promise .reject (3 ); console .log (4 ); } foo ().catch (console .log );console .log (2 );
await 的限制: await 关键字必须在异步函数中使用,不能在顶级上下文如
访问人数
总访问量