@
目录Promise
对于Promise的理解
解决的难点(相对于回调方式的异步)
-
解决回调地狱问题
大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流程的方式是非线性的、非顺序的,这使得正确推导这样的代码难度很大,将在多个回调中反复横条,且错误追踪不易。
promise以队列的方式组合了异步流程,以promise链代替了回调金字塔。
生成器以看似同步顺序的方式表达异步流程。
-
解决信任问题
回调会受到控制反转的影响,因为回调暗中把控制权交给第三方(通常是不受你控制的第三方工具!)来调用你的代码。这种控制转移导致一系列麻烦的信任问题,比如回调被调用的次数是否会超出预期。
不同的方向理解(使用)Promise
-
封装未来值
由于 Promise 封装了依赖于时间的状态——等待底层值的完成或拒绝,所以Promise 本身是与时间无关的。因此, Promise 可以按照可预测的方式组成(组合),而不用关心时序或底层的结果。
Promise 是一种封装和组合未来值的易于复用的机制。
-
事件完成(事件触发)
Promise类似订阅发布模式中的订阅管理器,异步完成类似发布(发布事件),Promise通过then和catch收集订阅,或者传递完成的事件,then和catch函数类似事件订阅。
Promise 的决议:一种在异步任务中作为两个或更多步骤的流程控制机制,时序上的 this-then-that。
Promise 决议不仅可以将 Promise 作为未来值查看,一样会涉及发送消息。它也可以只作为一种流程控制信号。
Promise构造
在 Promise 的创建过程(new构造器或resolve和reject方法)中或在查看其决议结果(then和catch方法)过程中的任何时间点上出现了一个 JavaScript 异常错误,比如一个 TypeError 或 ReferenceError,那这个异常就会被捕捉, 并且会使这个 Promise 被拒绝。
new Promise(fn) 构造器
必须提供一个函数回调。这个回调是同步的或立即调用的。 这个函数接受两个函数回调,用以支持 promise 的决议。通
常我们把这两个函数称为 resolve(..) 和 reject(..):
var p = new Promise( function(resolve,reject){
// resolve(..)用于决议/完成这个promise
// reject(..)用于拒绝这个promise
} );
reject(..) 就是拒绝这个 promise;rejecti不会展开Promise/thenable,如果向reject(..) 传入一个 Promise/thenable 值, 它会把这个值原封不动地设置为拒绝理由。后续的拒绝处理函数接收到的是你实际传给 reject(..) 的那个Promise/thenable,而不是其底层的立即值
resolve(..) 既可能完成 promise,也可能拒绝,要根据传入参数而定。
-
传给 resolve(..) 的是一个非 Promise、非 thenable 的立即值,这个 promise 就会用这个值完成。
-
传给 resolve(..) 的是一个真正的 Promise 或 thenable 值,这个值就会被递归展开,并且(要构造的) promise 将取用其( Promise 或 thenable )最终决议值或状态。
如果使用多个参数调用 resovle(..) 或者 reject(..),第一个参数之后的所有参数都会被默默忽略。
Promise.resolve(..) 和 Promise.reject(..)
创建一个已被拒绝的 Promise 的快捷方式是使用 Promise.reject(..) ,rejecti不会展开Promise/thenable,如果向reject(..) 传入一个 Promise/thenable 值, 它会把这个值原封不动地设置为拒绝理由。后续的拒绝处理函数接收到的是你实际传给 reject(..) 的那个Promise/thenable,而不是其底层的立即值
var p1 = new Promise( function(resolve,reject){
reject( "Oops" );
} );
var p2 = Promise.reject( "Oops" );
// p1和p2是等价的
Promise.resolve(..) 常用于创建一个已决议(完成或拒绝)的 Promise;
- Promise.resolve(..) 会展开 thenable 值。在这种情况下,返回的 Promise 采用传入的这个 thenable 的最终决议值,可能是完成,也可能是拒绝:
var fulfilledTh = {
then: function(cb) { cb( 42 ); }
};
var rejectedTh = {
then: function(cb,errCb) {
errCb( "Oops" );
}
};
var p1 = Promise.resolve( fulfilledTh );
var p2 = Promise.resolve( rejectedTh );
// p1是完成的promise
// p2是拒绝的promise
- 如果传入的是真正的 Promise, Promise.resolve(..) 什么都不会做,只会直接把这个值返回。
then(..) 和 catch(..)
每个 Promise 实例(不是 Promise API 命名空间)都有 then(..) 和 catch(..) 方法,通过这两个方法可以为这个 Promise 注册完成和拒绝处理函数。 Promise 决议之后,立即会调用这两个处理函数之一,但不会两个都调用,而且总是异步调用
then(..)
then(..) 接受一个或两个参数:第一个用于完成回调,第二个用于拒绝回调。如果两者中的任何一个被省略或者作为非函数值传入的话,就会替换为相应的默认回调。默认完成回调只是把消息传递下去,而默认拒绝回调则只是重新抛出(传播)其接收到的出错原因。
catch(..)
只接受一个拒绝回调作为参数,并自动替换默认完成回调。换句话说,它等价于 then(null,..):
p.then( fulfilled );
p.then( fulfilled, rejected );
p.catch( rejected ); // 或者p.then( null, rejected )
then(..) 和 catch(..) 也会创建并返回一个新的 promise,这个 promise 可以用于实现Promise 链式流程控制。
如果完成或拒绝回调中抛出异常,返回的 promise 是被拒绝的。(即then和catch中有异常,返回的promise的状态是拒绝的)
如果任意一个回调返回非 Promise、非 thenable 的立即值,这个值会被用作返回 promise 的完成值。 (即then和catch正常运行,返回的promise的状态是正常的)
如果完成处理函数返回一个 promise 或 thenable,那么这个值会被展开,并作为返回promise 的决议值。
判别Promise类型:具有 then 方法的类型(thenable 鸭子类型)
判别某个值是否是promise,常用方法可以通过 p instanceof Promise 来检查。但是并不准确,存在一下主要原因:
- Promise 值可能是从其他浏览器窗口(iframe 等)接收到的。这个浏览器窗口自己的 Promise 可能和当前窗口 /frame 的不同,因此这样的检查无法识别 Promise实例。
- 库或框架可能会选择实现自己的 Promise,而不是使用原生 ES6 Promise 实现。实际上,很有可能你是在早期根本没有 Promise 实现的浏览器中使用由库提供的 Promise。
识别 Promise(或者行为类似于 Promise ) :任何具有 then(..) 方法的对象和函数。 任何这样的值就是Promise 一致的 thenable。
根据一个值的形态(具有哪些属性)对这个值的类型做出一些假定。这种类型检查(typecheck)一般用术语鸭子类型(duck typing)来表示——“如果它看起来像只鸭子,叫起来像只鸭子,那它一定就是只鸭子”
注意:如果 thenable 鸭子类型误把不是 Promise 的东西识别为了 Promise,可能就是有害的。 (在 ES6 之前,社区已经有一些著名的非 Promise 库恰好有名为 then(..) 的方法。这些库中有一部分选择了重命名自己的方法以避免冲突。而其他的那些库只是因为无法通过改变摆脱这种冲突,就很不幸地被降级进入了“与基于 Promise 的编码不兼容”的状态。 )
标准决定劫持之前未保留的——听起来是完全通用的——属性名 then。这意味着所有值(或其委托),不管是过去的、现存的还是未来的,都不能拥有 then(..) 函数,不管是有意的还是无意的;否则这个值在 Promise 系统中就会被误认为是一个 thenable,这可能会导致非常难以追踪的 bug。
Promise局限性
-
顺序错误处理
在promise链中,拒绝错误处理函数(catch函数)能处理该函数之前的promise未处理的错误;但是因为没有为promise链中产生的promise保留引用,无法做到可靠的检查错误。
-
单一值:
Promise 只能有一个完成值或一个拒绝理由,简单的例子中,这不是什么问题,但是在更复杂的场景中,可能这是一种局限;一般的建议是构造一个值封装(比如一个对象或数组)来保持这样的多个信息。这个解决方案可以起作用,但要在 Promise 链中的每一步都进行封装和解封,就十分丑陋和笨重了。
-
单决议:
Promise 只能被决议一次(完成或拒绝)。在许多异步情况中,你只会获取一个值一次,所以这可以工作良好。但是,对于很多其他异步模式:类似于事件和 / 或数据流的模式支持不够。
-
惯性:
基于回调异步的代码,重构为promise(promise化),有心智负担,原生 ES6 Promise 并没有提供辅助函数用于这样的 promisory (“Promise” +“factory” Promise工厂)封装,但多数库都提供了这样的支持,或者你也可以构建自己的辅助函数。
-
无法取消的 Promise:
一旦创建了一个 Promise 并为其注册了完成和 / 或拒绝处理函数,如果出现某种情况使得这个任务悬而未决的话,你也没有办法从外部停止它的进程;单独的 Promise 不应该可取消,但是取消一个序列(promise链)是合理的,因为你不会像对待 Promise
那样把序列作为一个单独的不变值来传送。
生成器
概述
生成器就是一类特殊的函数,相较于传统函数是同步运行,不能暂停与恢复(除非有异常),可以一次或多次启动和停止,并不一定非得要完成。 生成器停止(由迭达器停止)后,交出线程控制权(程序控制权),由迭达器来恢复 ,继续运行。
使用方式
运行生成器创建了一个迭代器对象,把它赋给了一个变量(如it),用于控制生成器。然后调用 it.next(),指示生成器开始继续运行,停在下一个 yield 处或者直到生成器结束。
next() 调用返回一个对象。这个对象有两个属性: done 是一个 boolean 值,标识迭代器的完成状态,当为true时,表示生成器结束; value 中放置迭代值。
关于生成器结束:
- 迭达器调用next():返回对象中, done属性为true时表示结束,迭达器不再迭达。
- 迭达器调用return():返回结果(将值传递给迭达器的value属性)并结束生成器(迭达器的done属性为true);如果 生成器内部有
try...finally
代码块,且正在执行try
代码块,那么return()
方法会导致立刻进入finally
代码块,执行完以后,整个生成器才会结束。- 迭达器调用throw():跌达器抛出异常,然后传入生成器,如生成器未被捕获,该生成器将结束,否则继续运行生成器;
throw
方法抛出的错误要被内部捕获,前提是必须至少执行过一次next
方法;throw
方法被捕获以后,会附带执行下一条yield
表达式。也就是说,会附带执行一次next
方法(可以把throw方法理解为,需要处理)。
特性
- 控制权转移:生成器可以由迭达器暂停和开始,暂停交出线程控制权,等待迭达器恢复或者不恢复
- 双向消息传递 :通过迭达器可以生成器传递数据,也可以向迭达器传递数据,内建消息输入输出功能;
- 生成值:最基础特性,也是名字的由来,可以无限生成,只要done不为true;
迭达器与可迭达(迭达对象)
迭达器:对象中有next方法;
可迭达对象(iterable):指包含一个可以在其值上迭达的迭达器的对象,(对象包含迭达器,迭达器可以迭达对象上的值);
- 迭代器是一个定义良好的接口,用于从一个生产者一步步得到一系列值。 JavaScript 迭代器的接口,与多
数语言类似,就是每次想要从生产者得到下一个值的时候调用 next()。- 从 ES6 开始,从一个 iterable 中提取迭代器的方法是: iterable 必须支持一个函数,其名称是专门的 ES6 符号值 Symbol.iterator。调用这个函数时,它会返回一个迭代器。通常每次调用会返回一个全新的迭代器,虽然这一点并不是必须的。
生成器,迭达器,可迭达
每次构建一个迭代器,实际上就隐式构建了生成器的一个实例,通过这个迭代器来控制的是这个生成器实例。不同的生成器实例相互独立。
当你执行一个生成器,就得到了一个迭代器 ,严格说来,生成器本身并不是可迭达;
ES6中for..of 循环,可以迭达迭达对象(可迭达),for..of可以迭达运行生成器构建的迭达器,是因为该迭达器是一个可迭达对象。
生成器与异步
解决异步同步方式管理(顺序管理)
因为生成器具有交出控制权的特性,不阻塞线程的特性,所以可以通过将生成器的迭达器交予异步来启动生成器(如果有需要可以通过消息传递功能,向生成器传递异步结果),来达到以看似同步的方式管理异步流程。
看似阻塞同步的代码,实际上并不会阻塞整个程序,它只是暂停或阻塞了生成器本身的代码。
从本质上而言,我们把异步(不管是回调异步或者Promise异步)作为实现细节抽象了出去,使得生成器可以以同步顺序的形式追踪流程控制:
解决异步错误跟踪(同步的方式处理错误)
生成器 yield 暂停的特性意味着我们不仅能够从异步函数调用得到看似同步的返回值,还可以同步捕获来自这些异步函数调用的错误。
生成器构建的迭达器也可以手工回传错误(通过throw函数),
注意:如果生成器内部抛出的错误,外部需要自行捕获,不然会造成程序运行错误从而结束。
生成器实现看似同步的错误处理(通过 try..catch),是异步代码的可读性和合理性的巨大进步。
生成器 +Promise
ES6中异步的完美组合是生成器+Promise
- 生成器:看似同步的异步代码控制。
- Promise :可信任性和可组合性 。
获得 Promise 和生成器最大效用的最自然的方法就是 yield 出来一个 Promise,然后通过这个 Promise 来控制生成器的迭代器。
async
它就是 Generator(生成器) 函数的语法糖。
生成器运行返回迭达器,迭达器,需要手动运行,有第三方库可以自动执行迭达器(根据生成器内部不同的异步方式:基于回调函数和Promise),有了async和await函数后,原生支持自动执行。
使用
async函数返回一个 Promise 对象,可以使用then方法添加回调函数。
返回值为Promise
async函数内部return语句返回的值,会成为then方法回调函数的参数.
async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变(函数运行完成),除非遇到return语句或者抛出错误。
await 命令
正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
另一种情况是,await命令后面是一个thenable对象(即定义了then方法的对象),那么await会将其等同于 Promise 对象。
await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行(类似于生成器中未捕获错误,错误往外抛).
注意:如果需要即使前一个异步操作失败,也不要中断后面的异步操作(捕获错误,恢复运行)。可以通过一下方法:
- 将可能错误的Promise放入放在
try...catch
结构里面,这样不管这个Promise操作是否成功,后续的await都会执行。
async function f() {
try {
await Promise.reject('出错了');
} catch (e) {
console.log('处理错误',e);
}
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// hello world
- 另一种方法是await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。
async function f() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// 出错了
// hello world
错误处理
如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject。
如需要捕获异常,继续运行可查看 await命令 处理异常的方式;
注意:
try...catch
只能捕获单次异步异常,不能捕获多次异常(和生成器一样);
async function f() {
try {
await Promise.reject('出错了1') // 捕获单次异常
await Promise.reject('出错了2') // 这里的代码将不再运行,不会再捕获该异常
} catch (e) {
console.log('处理错误', e)
}
return await Promise.resolve('hello world') // 恢复异常后继续运行
}
f()
.then((v) => console.log(v))
.catch((err) => {
console.log(err)
})
// 处理错误 出错了1
// hello world
顶层 await
早期的语法规定是,await命令只能出现在 async 函数内部,否则都会报错。
从 ES2022 开始,允许在模块的顶层独立使用await命令。它的主要目的是使用await解决模块异步加载的问题。它保证只有异步操作完成,模块才会输出值,才能使用模块。
顶层await的一些使用场景:
// import() 方法加载
const strings = await import(`/i18n/${navigator.language}`);
// 数据库操作
const connection = await dbConnector();
// 依赖回滚
let jQuery;
try {
jQuery = await import('https://cdn-a.com/jQuery');
} catch {
jQuery = await import('https://cdn-b.com/jQuery');
}
标签:总结,异步,..,await,生成器,Promise,promise From: https://www.cnblogs.com/niehao/p/17439212.html顶层的await命令有点像,交出代码的执行权给其他的模块加载,等异步操作完成后,再拿回执行权,继续向下执行。