回调
在某些不确定时长的工作完成之后才执行对应的逻辑(函数),这些函数就是回调。
比如onload
function loadScript(src, callback) {let s = document.create('script')s.src = src// 不是s.onload = callbacks.onload = () => {callback(s)}document.head.append(s)}loadScript('xxx.js', () => {// do some})
回调中回调
如果需要加载2个脚本,有序,第一个,第二个。需要把加载第二个的逻辑卸载第一个的callback里
loadScript('1.js', () => {alert('1.js is ok')loadScript('2.js', () => {// 这里安全访问1和2的脚本内容alert('2.js is ok')})})
处理Error
上例没有处理失败情况
// 经典的Error Firstfunction loadScript(src, callback) {let script = document.createElement('script')script.src = scrscript.onload = () => callback(null, script)script.onerror = (e) => callback(e)document.head.append(script)}loadScript('xxx', (error, sciprt) => {if(error) {// 处理错误} else {// 脚本逻辑}})
嵌套地狱
回调逻辑如果变多,则是一种不可维护的代码。
loadScript('1', (err, script) => {if(err){// do} else {loadScript('2', (err, script) => {if(err){// do} else {loadScript('3', (err, script) => {if(err) {// do} else {//}})}})}})
优化,独立函数,缓解
将异步任务拆成一个个函数,每个函数中调用另一个异步任务函数,达到拆分代码的效果。
loadScript('1', step1)function step1(err, script) {if(err) {} else {loadScript('2', step2)}}function step2(err, script) {...}
Promise
语法
let promise = new Promise(function(resolve, reject) {// executor 生产者代码})
- 创建Promise的同时,executor会自动执行。
- 任务成功,resolve(value)
- 任务失败,reject(error)
resolve和reject,表明该promise被settled了
内部属性
一个Promise具有以下内部属性,不可访问
state
- 初始pending
- resolve调用,则变为 fulfilled
- reject调用,则变为 rejected
result
- 初始为undefined
- resolve(value),变为value
- reject(error)调用,变为error
只能有一个终态,确定后不可变
```javascript let p = new Promise((resolve, reject) => { resolve(1) resolve(2) // 忽略 reject(new Error(‘h’)) // 忽略 })
// 只能通过then或catch访问到结果 p.then(r => console.log(r)) // 1
// 之后的任何时候访问,都是1
<a name="uwNSw"></a>### 消费者通过 `then` `catch` `finally` 来访问一个Promise的结果。```javascript// then的完整语法p.then(function(r) {// 处理resolve的结果}, function(err) {// 处理reject的结果})// then函数的参数不是必须的,且错误处理通常用catch。上面then完整写法中处理错误了,但不等于.catchp.then(r => {// 处理resolve}).catch(e => {// 处理reject})p.finally() // 同try finally, 最开始由于兼容性用的比较少。 node 10+支持,ie完全不支持。
Promise链
异步任务一个接一个的执行,我们可以利用链的特性。
new Promise((resolve, reject) => {resolve(1)}).then(r => {return r * 2 // return 2}).then(r => {return r * 2 // return 4}).then(r => {console.log(r) // 4})
能这么写的原因?
- then本身返回的也是一个promise,所以我们可以继续.then
- return的值将作为当前promise的result(当前promise状态已经resolved了),所以可以对当前Promise使用then获取其result。达到传递效果。
return一个新Promise
.then(handler)中handler可以继续创建并返回一个promise,这种情况下,后续处理程序将等这个新的promise被settled后再获取其结果。new Promise((resolve, reject) => {resolve(1)}).then(r => {return new Promise((resolve, reject) => {setTimeout(() => {resolve(r + 2)}, 1000)})}).then(r => {console.log(3)})
避免代码向右增长
假设有一个用promise实现的loadScript函数
要使用链式loadScript('a.js').then(a => {loadScript('b.js').then(b => {loadScript('c.js').then(c => {a();b();c()})})})
loadScript('a.js').then(a => loadScript('b.js')).then(b => loadScript('c.js')).then(c => {console.log(c)})// 注意,这里只能拿到c了
thenable
鸭子类型的独享,需要实现.then方法,第三方可借助此实现自己的Promise兼容对象。 ```javascript class Thenable { constructor(n) { this.n = n }
then(resolve, reject) { setTimeout(() => {
}, 1000)resolve(this.n * 2)
} }
new Promise(resolve => resolve(1)).then(r => {
return new Thenable(r)
}).then(r => {
console.log(r)
})
<a name="sMbW6"></a>## promise状态图<a name="W8oHU"></a>## 注意:then(f1, f2)不等于catch如果f1出现错误,f2是捕获不到的,只能靠catch,因此通常我们用catch来兜底。<a name="ZqKq7"></a># Promise的错误处理<a name="3j6iW"></a>## Promise链+错误处理Promise一旦被rejected,会将控制权交给最近的处理错误函数(通常是.catch)。<br />多个异步任务构成的异步链,我们不用每个异步任务单独处理报错,可以在最后处理。```javascriptasyncByPromise(...).then(r => {...}).then(r => {...}).then(r => {...}).catch(e => {...}) // 可以接收到上面3个then中任意一个错误,进行处理
隐式try…catch
如果Promise发生异常,或主动throw error,周围会有一个隐式的try…catch,捕获错误,并将Promise视为 rejection 进行处理。
new Promise(resolve => {throw new Error('抓我')}).catch(e => {console.log(e) // 抓我})// 等同new Promise((resolve, reject) => {reject('抓我')}).catch(e => {console.log(e) // 抓我})
上面代码异常是发生在 executor 里,也就是Promise接收的函数中。对 handler 函数中异常,同样
new Promise((resolve, reject) => {resolve("ok");}).then((result) => {throw new Error("Whoops!"); // reject 这个 promise}).catch(alert); // Error: Whoops!
再次抛出
上面在Promise链式最后的catch,类似于在最外层包裹了 try catch ,同 try catch 中讲过的,我们可以在 catch 中分析错误,如果处理不了,就继续抛出。
错误的正常处理和再次抛出
如果错误被正常处理,则会将控制权移交到最近的 .then 处理程序,继续走下去。
如果错误被再次抛出,则会将错误移交到下一个最近的error处理程序。
// 错误正常处理了,交给最近的then// 执行流:catch -> thennew Promise(resolve => {throw new Error('whoops')}).catch(e => {// 这里正常处理掉console.log('错误被处理了,继续吧')}).then(r => {console.log('第1个then')console.log(r)}).then(r => {console.log('第2个then')})// 错误没有被处理,抛出,继续给下面的catch来// 执行流:catch -> catchnew Promise(resolve => {throw new Error('错了')}).catch(e => {throw e // 处理不动了}).then(r => {// then这里拿不到(如果你提供给then第二个函数呢?也可以处理这个错误,就用不到后面的catch了)}, e => {console.log(`then第二个处理函数有的话,错误就接住了`, e)}).catch(error => {console.log('兜底了 ' + error)})
未处理的rejection
如果有未处理的Promise rejection,JS引擎将会跟踪此类 rejection ,生成一个全局的 error ,类似 window.onerror ,我们也有 unhandledrejection 全局事件来捕获这类错误。
window.addEventListener('unhandledrejection', function(event) {// 这个事件对象有两个特殊的属性:alert(event.promise); // [object Promise] - 生成该全局 error 的 promisealert(event.reason); // Error: Whoops! - 未处理的 error 对象});
注意:隐式try…catch
try catch只能捕获同步错误,异步错误无法捕获。同理在Promise中,抛出一个错误,交给隐式try…catch来处理的话,只能捕获同步错误
new Promise(resolve => {setTimeout(() => {// 异步抛出throw new Error('catch捕获不到')}, 1000)}).catch(e => {console.log('这里抓不到异步的错误,除非你用reject啊')})// 错误会溢出到全局,未捕获。
Promise其他API
all
Promise.all([...promiseArray])
- 当给定的所有Promise都被
resolved,新的promise才会被resolved,其结果将成为新的promise的结果(一个数组)。 - 但如果有一个Promise被rejected,则新的promise会rejected,且error就是这个被rejected的promise,其他的promise也不再理会。
allSettled
与Promise.all不同,会等待所有promise都被settled的,而不是一个rejected,整体都rejected。最近的提案,有些浏览器还不支持,我们可以模拟下
// 统一转换为一个对象值,这里是 => ({}),直接返回了这个对象值,这步很重要!const rejectHandler = reason => ({status: 'rejected', reason})const resolveHandler = value => ({status: 'fulfilled', value})Promise.allSettled = function(promises) {// 记得我们前面说过的,then 返回的是一个新的Promise,前一个Promise return的值会传递到下一个Promise里// 所以这里拿到的新的promises数组,rejected的都被转换为了一个正常的对象值,都是fulfilled的状态。const convertedPromises = Promise.resolve(item).then(resolveHandler,rejectHandler)// 这里利用all等到所有promise都resolved特性。return Promise.all(convertedPromises)}
race
只等待promise数组中第一个settled的promise,不管是结果还是错误。
resolve/reject
resolve(value)用value创建一个resolved的promisereject(error)用error创建一个rejected的promise
Promisfication
很多三方库和函数都是基于回调的,为了方便使用Promise,我们通常会将基于回调的函数和库转换为promise形式的。
假设有一个读取脚本的函数
// Error First形式的callback很常见function loadScript(src, callback) {// 创建script标签,插入doc中let script = ...script.onload = () => callback(null, script)script.onload = () => callback(new Error(`${src}加载失败`)}loadScript('a.js', (err, s) => {...})// 转换后function loadScriptPromise = function(src) {return new Promise((resolve, reject) => {loadScript(src, (err, script) => {if(err) reject(err)else resolve(script)})})}loadScriptPromise('a.js').then(...)
通用的promisfy(f)
思路:
- 因为是包装callback(error, value)形式的函数,所以我们接受一个函数f,运行后生成一个新的函数newF
- newF就是我们真正业务使用的,接受我们业务的参数,比如
loadScript接收的业务参数就是src,但要注意参数可能并不唯一,所以我们需要收集到所有参数,使用ES6的rest参数来拿到所有参数。至于callback,我们需要统一处理为上面loadScript中那样,封装resolve和reject,所以内部实现,无需关注 - 当我们调用newFn时,应该是这样的
newFn(arg1, arg2).then(res => {}).catch(err => {})。所以我们需要newFn返回一个Promise 在Promise中,executor函数(也就是包装的fn)需要执行,
- 注意
this指向问题,我们可能是对象调用如obj.newFn(arg1, arg2, callback),也可能是newFn(arg1, arg2, callback),也可能是newFn.bind(obj)生成又一个新函数调用,或者newFn.call(obj, arg1, arg2, callback)。 注意callback永远都是在最后面的,所以我们可以统一封装一个callback来以前代理用户自己传递 ```javascript // 第一步,包装callback(err,value)形式的函数f function Promisfy(f) { // 吐出一个新的函数newFn // 第二步,接收所有的参数 return function(…args) { // 第三步,返回一个promise return new Promise((resolve, reject) => { function callback(err, value) {
if(err) {reject(err)
} else {
resolve(value)
} }
// 第四步,执行f,或者f.apply(this, args) f.call(this, …args, callback) }) } }
- 注意
// 验证OK function callbackAdd(a, b, callback) { setTimeout(() => { const sum = a + b; if (sum > 10) { callback(new Error(‘大于10’)); } else { callback(sum); } }, 1000); }
const newCallbackAdd = Promisfy(callbackAdd); newCallbackAdd(1, 3) .then((r) => { console.log(r); // 4 }) .catch((err) => { console.log(err); });
有没有问题?<br />有,入参我们已经支持多个了,出参我们目前不支持多个,我们无法支持一个多个出参的callback,如`callback(err, res1, res2, ...)`。我们的内置callback只能支持1个出参。```javascript// 改进,将callback的出参支持多个function Promisfy(f) {// 吐出一个新的函数newFn// 第二步,接收所有的参数return function(...args) {// 第三步,返回一个promisereturn new Promise((resolve, reject) => {function callback(err, ...value) {if(err) {reject(err)} else {// 这里根据callback的接收到的除err之外的参数来决定返回数组还是单值if(value.length === 0) {resolve(value[0])} else {// resolve 只能接收单值resolve(value)}}}// 第四步,执行f,或者f.apply(this, args)f.call(this, ...args, callback)})}}// 验证OKfunction compute(a, b, callback) {setTimeout(() => {const sum = a + b;const product = a * b;if (sum > 10 || product > 100) {callback(new Error('错误'));} else {callback(null, sum, product);}}, 1000);}const newComp = Promisfy(compute);newComp(1, 4).then((r) => {console.log(r); // [5, 4]}).catch((err) => {console.log(err);});
微任务
Promise.then/catch/finaly 都是异步的。JS引擎中有微任务队列负责控制,当微任务队列清空后,继续执行其他任务。
let p = Promise.resolve(1)p.then(r => {console.log(r)})console.log('code done')// code done// 1
async/await
使用关键字async标识函数,函数总返回一个promise
使用关键字await获取promise的值,并且会等待promise状态变为settled
await 也接受thenable,就像promise支持thenable一样
error处理
使用try catch包裹住async内部的代码。
