回调
在某些不确定时长的工作完成之后才执行对应的逻辑(函数),这些函数就是回调。
比如onload
function loadScript(src, callback) {
let s = document.create('script')
s.src = src
// 不是s.onload = callback
s.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 First
function loadScript(src, callback) {
let script = document.createElement('script')
script.src = scr
script.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完整写法中处理错误了,但不等于.catch
p.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状态图
![image.png](https://cdn.nlark.com/yuque/0/2021/png/241313/1627442987078-30a3a8d6-f2c0-4735-bf84-e416511c88fb.png#height=375&id=CPfv8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=750&originWidth=1360&originalType=binary&ratio=1&size=84605&status=done&style=none&width=680)
<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 />多个异步任务构成的异步链,我们不用每个异步任务单独处理报错,可以在最后处理。
```javascript
asyncByPromise(...)
.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 -> then
new 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 -> catch
new 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 的 promise
alert(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) {
// 第三步,返回一个promise
return 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)
})
}
}
// 验证OK
function 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内部的代码。