简介:回调
JS
会提供很多函数,允许异步行为。换句话说,现在开始执行的行为。但它们会在稍后完成。- 异步执行某项功能的函数应该提供一个
callback
参数用于在相应事件完成时调用。 - 处理
Error
:- 加载成功时,它会调用
callback(null, xxx)
,否则调用callback(error)
,这种被称为Error优先回调
风格。 - 约定:
callback
的第一个参数是为 error 而保留的。一旦出现 error,callback(err)
就会被调用。- 第二个参数(和下一个参数,如果需要的话)用于成功的结果,此时
callback(null, result1, result2…)
就会被调用。
- 单一的
callback
函数可以同时具有报告 error 和传递返回结果的作用。
- 加载成功时,它会调用
- 但对于一个接一个的多个异步行为,随着调用嵌套的增加,代码层次变得更深,维护难度也随之增加,尤其是我们使用的是可能包含了很多循环和条件语句的真实代码,这些被称为“回调地狱”或“厄运金字塔”。
- 有其他的方法可以避免此类金字塔,最好的方法之一就是
promise
Promise
-
Promise
是将生产者代码和消费者代码连接在一起的一个特殊JS
对象,是异步编程的一种解决方案 -
构造器语法:
let promise = new Promise(function(resolve, reject) { if (/* 异步操作成功 */){ resolve(value); } else { reject(error); } });
-
传递给
new Promise
的函数被称为 executor,当new Promise
被创建,executor 会自动运行 -
它的参数
resolve
和reject
是由JS
自身提供的回调,代码仅在 executor 的内部。当 executor 获得了结果,无论是早还是晚都没关系,它应该调用以下回调之一:resolve(value)
:任务成功完成并带有结果value
reject(error)
:如果出现了error
,error
即为error
对象
-
executor 只能调用一个
resolve
或一个reject
,任何状态的更改都是最终的。并且,resolve/reject
只需要一个参数(或不包含任何参数) -
executor 通常是异步执行某些操作,并在一段时间后调用
resolve/reject
,但这不是必须的,我们可以立即调用resolve
或reject
-
promise
对象具有以下内部属性:state
:最初是"pending"
resolve
被调用时变为"fulfilled"
reject
被调用时变为"rejected"
result
:最初是undefined
resolve(value)
被调用时变为value
reject(error)
被调用时变为error
- 与最初的
pending
promise
相反,一个resolved
或rejected
的promise
都会被称为settled
-
state
和result
属性都是内部的,无法直接访问它们,但我们可以对它们使用.then
/.catch
/.finally
方法 -
消费者:
then, catch
-
.then
语法:promise.then( function(result) { /* handle a successful result */ }, function(error) { /* handle an error */ } );
.then
的第一个参数是一个函数,在 promise resolved 且接收到结果后执行.then
的第二个参数也是一个函数,在 promise rejected 且接收到 error 信息后执行
-
只对成功完成的情况感兴趣,可以只为
.then
提供一个函数参数 -
只对 error 感兴趣,可以使用
null
作为第一个参数:.then(null, f)
,或者也可以使用.catch(f)
-
-
简单的例子:
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100).then((value) => {
console.log(value);
});
-
timeout
方法返回一个Promise
实例,表示一段时间以后才会发生的结果。过了指定的时间(ms
参数)以后,Promise
实例的状态变为resolved
,就会触发then
方法绑定的回调函数。 -
promise 中也有
finally
,调用.finally(fn)
类似于.then(f, f)
,因为当 promise settled 时f
就会执行:无论 promise 被 resolve 还是 reject。finally
的功能是设置一个处理程序在前面的操作完成后,执行清理/终结。 -
PS:
finally(f)
并不完全是then(f, f)
的别名finally
没有参数finally
将结果或error
“传递”给下一个合适的处理程序- 成功就传递给下一个
then
- 失败就传递给下一个
catch
- 成功就传递给下一个
finally
也不应该返回任何内容,如果它返回了会默认被忽略
-
基于回调的好处:
promise
允许我们按照自然顺序进行编码;而callback
就必须要知道如何处理结果promise
可以多次调用.then
;而callback
只能有一个回调
-
常见写法:
new Promise(请求1) .then(请求2(请求结果1)) .then(请求3(请求结果2)) .then(请求4(请求结果3)) .then(请求5(请求结果4)) .catch(处理异常(异常信息))
使用promise进行错误处理
-
当一个
promise
被reject
时,将移交到最近的rejection
处理程序 -
.catch
处理 promise 中的各种 error:在reject()
调用中的,或者在处理程序中抛出的 error -
如果给定
.then
的第二个参数(即 error 处理程序),那么.then
也会以相同的方式捕获 error -
未处理的
rejection
new Promise(function() { noSuchFunction(); // 这里出现 error(没有这个函数) }) .then(() => { // 一个或多个成功的 promise 处理程序 }); // 尾端没有 .catch!
-
如果出现了一个 error,并且在这没有
.catch
,那么unhandledrejection
处理程序就会被触发,并获取具有 error 相关信息的event
对象,所以我们就能做一些后续处理了window.addEventListener('unhandledrejection', function(event) { // 这个事件对象有两个特殊的属性: alert(event.promise); // [object Promise] —— 生成该全局 error 的 promise alert(event.reason); // Error: Whoops! —— 未处理的 error 对象 }); new Promise(function() { throw new Error("Whoops!"); }); // 没有用来处理 error 的 catch
-
-
例子
.catch
会被触发吗:new Promise(function(resolve, reject) { setTimeout(() => { throw new Error("Whoops!"); }, 1000); }).catch(alert);
- 不一定会被触发。函数代码周围有个隐式的
try..catch
,所有同步错误都会得到处理。但是这里的错误并不是在 executor 运行时生成的,而是在稍后生成的,因此,promise 无法处理它。
- 不一定会被触发。函数代码周围有个隐式的
Promise API
Promise.all
-
并行执行多个 promise,并等待所有 promise 都准备就绪
-
使用场景:一些游戏类的素材比较多的应用,打开网页时,预先加载需要用到的各种资源如图片、
flash
以及各种静态文件。所有的都加载完后,我们再进行页面的初始化 -
Promise.all
接受一个可迭代对象(通常是一个数组项为 promise 的数组),并返回一个新的 promise -
例子:
Promise.all([ new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1 new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2 new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3 ]).then(alert); // 1,2,3 当上面这些 promise 准备好时:每个 promise 都贡献了数组中的一个元素
- 结果数组中元素的顺序与其在源 promise 中的顺序相同。即使第一个 promise 花费了最长的时间才 resolve,但它仍是结果数组中的第一个
-
任意一个 promise 被 reject,由
Promise.all
返回的 promise 就会立即 reject,并且带有的就是这个 error -
Promise.all(...)
接受含有 promise 项的可迭代对象(大多数情况下是数组)作为参数,但如果这些对象中的任何一个不是 promise,那么它将被按原样传递给结果数组Promise.all([ new Promise((resolve, reject) => { setTimeout(() => resolve(1), 1000) }), 2, 3 ]).then(alert); // 1, 2, 3
Promise.allSettled
- 最近新增的特性,旧浏览器可能需要
polyfills
Promise.allSettled
等待所有的 promise 都被 settle,无论结果如何,结果数组具有:{status:"fulfilled", value:result}
对于成功的响应{status:"rejected", reason:error}
对于 error
- 例如,我们想要获取(fetch)多个用户的信息。即使其中一个请求失败,我们仍然对其他的感兴趣
Promise.race
-
只等待第一个
settled
的promise
并获取其结果或error
-
例子:
let promise = Promise.race(iterable); Promise.race([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).then(alert); // 1
- 第一个 promise 最快,所以它变成了结果,其他的
result/error
会被忽略
- 第一个 promise 最快,所以它变成了结果,其他的
-
谁跑得快,以谁为准执行回调
-
使用场景:可以用
race
给某个异步请求设置超时时间,并且在超时后执行相应的操作//请求某个图片资源 function requestImg(){ var p = new Promise((resolve, reject) => { var img = new Image(); img.onload = function(){ resolve(img); } img.src = '图片的路径'; }); return p; } //延时函数,用于给请求计时 function timeout(){ var p = new Promise((resolve, reject) => { setTimeout(() => { reject('图片请求超时'); }, 5000); }); return p; } Promise.race([requestImg(), timeout()]).then((data) =>{ console.log(data); }).catch((err) => { console.log(err); });
requestImg
函数会异步请求一张图片,把地址写为"图片的路径",所以肯定是无法成功请求到的。timeout函数是一个延时5秒的异步操作。我们把这两个返回Promise对象的函数放进race
,于是它俩就会赛跑。- 如果5秒之内图片请求成功了,那么进入then方法,执行正常的流程。
- 如果5秒钟图片还未成功返回,那么
timeout
就跑赢了,则进入catch
,报出“图片请求超时”的信息
Promise.any
- 与
Promise.race
类似,区别在于它只等待第一个 fulfilled 的 promise,并将这个 fulfilled 的 promise 返回 - 如果给出的 promise 都 rejected,那么返回的 promise 会带有
AggregateError
—— 一个特殊的 error 对象,在其errors
属性中存储着所有 promise error
Promise.resolve
- 给定
value
创建一个resolved
的promise
Promise.reject
- 给定
error
创建一个rejected
的promise
Promisification
- 将一个接受回调的函数转换为一个返回 promise 的函数
Promisification
是一种很好的方法,特别是在使用async/await
的时候,但不是回调的完全替代- 一个
promise
可能只有一个结果,但从技术上讲,一个回调可能被调用很多次。因此,promisification
仅适用于调用一次回调的函数,进一步的调用将被忽略。
微任务
-
promise 的处理程序
.then
、.catch
和.finally
都是异步的。即便一个 promise 立即被 resolve,.then
、.catch
和.finally
下面 的代码也会在这些处理程序之前被执行。let promise = Promise.resolve(); promise.then(() => alert("promise done!")); alert("code finished"); // 这个 alert 先显示
-
异步任务需要适当的管理,
ECMA
标准规定了一个内部队列PromiseJobs
,通常被称为微任务队列。当一个 promise 准备就绪时,它的.then/catch/finally
处理程序就会被放入队列中,但是它们不会立即被执行。当 JavaScript 引擎执行完当前的代码,它会从队列中获取任务并执行它。 -
如果有一个包含多个
.then/catch/finally
的链,那么它们中的每一个都是异步执行的。也就是说,它会首先进入队列,然后在当前代码执行完成并且先前排队的处理程序都完成时才会被执行。 -
想让
code finished
在promise done
之后出现,就要把.then
放入队列里Promise.resolve() .then(() => alert("promise done!")) .then(() => alert("code finished"));
-
现在可以解释 JavaScript 是如何发现未处理的 rejection 的。如果一个 promise 的 error 未被在微任务队列的末尾进行处理,则会出现“未处理的 rejection”
-
正常来说,我们会在 promise 链上添加
.catch
来处理 error:let promise = Promise.reject(new Error("Promise Failed!")); promise.catch(err => alert('caught')); // 不会运行:error 已经被处理 window.addEventListener('unhandledrejection', event => alert(event.reason));
-
忘记添加
.catch
,那么,微任务队列清空后,JavaScript 引擎会触发下面这事件:let promise = Promise.reject(new Error("Promise Failed!")); // Promise Failed! window.addEventListener('unhandledrejection', event => alert(event.reason));
-
如果迟一点再处理:
let promise = Promise.reject(new Error("Promise Failed!")); setTimeout(() => promise.catch(err => alert('caught')), 1000); // Error: Promise Failed! window.addEventListener('unhandledrejection', event => alert(event.reason));
- 我们会先看到
Promise Failed!
,然后才是caught
。 - 当微任务队列中的任务都完成时,才会生成
unhandledrejection
:引擎会检查 promise,如果 promise 中的任意一个出现rejected
状态,unhandledrejection
事件就会被触发 - 在上面这个例子中,被添加到
setTimeout
中的.catch
也会被触发,只是会在unhandledrejection
事件出现之后才会被触发,所以它并没有改变什么(没有发挥作用)
- 我们会先看到
async / await
-
async / await
是以更舒适的方式使用 promise 的一种特殊语法 -
async
可以被放置在一个函数前面,即这个函数总是返回一个 promise,其他值将自动被包装在一个 resolved 的 promise 中 -
await
它只在async
函数内工作,它让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果。如果有 error,就会抛出异常,就像那里调用了throw error
一样 -
await
实际上会暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行。这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务:执行其他脚本,处理事件等