1. 异步
本文分为 JS
和Node
两部分,4章之前为JS
,之后为Node
,由于不是同时写的,看起来会有点跨越。
1.1 概念
异步任务是调用无法立即得到结果,需要额外的操作才能预期结果的任务,异步任务不进入主线程、而进入"任务队列",只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行
1.2 问题
由于异步调用无法确定数据返回的时间,因此回调函数调用的顺序无法保证。
为了确保函数的执行顺序,就出现了异步嵌套异步的方法,产生了非常丑陋的代码嵌套,又称为:回调地狱
1.3 解决方案
JS
原生提供了很多异步的解决方案,在JS
或Node
中直接使用即可。
比较流行的主要是Promise
和 async
两种。
2. Promise
2.1 含义和使用
异步编程的一种解决方案,比传统的解决方案更合理且强大
Promise对象有两个特点:
- 具有三种状态
Pending
,Fulfilled
,Rejected
。状态不受外界影响,只受异步操作结果影响 - 状态改变只有两种可能
Pending → Fulfilled
或Pending → Rejected
。只要状态改变便不会再变且一直保持这个接个
缺点:
- 一旦新建立即执行,无法取消且内部报错不会反应到外部
- 处于
Pending
状态时,无法得知异步操作进展情况
// 图片异步加载
function loadImageAsync (url) {
return new Promise((resolve, reject) => {
const image = new Image()
image.onload = function () {
resolve({
image: image,
newUrl: './photo.png'
})
}
image.onerror = function () {
reject('Load Error!')
}
image.src = url
})
}
// Promise状态传递
const p1 = new Promise((resolve, reject) => {
setTimeout(() => reject('Fail'), 3000)
})
const p2 = new Promise((resolve, reject) => {
setTimeout(() => resolve(p1), 1000) // 由于resolve了p1是另一个Promise,因此p2状态由p1决定
})
p2.then(res => console.log(res, 'then'))
.catch(err => console.log(err, 'catch')) // Fail catch
2.2 方法
2.2.1 Promise.prototype.then()
then
用于添加状态改变时的回调函数,返回一个新的 Promise
实例,因此可以链式调用。then
会将返回值包装成新的 Promise
实例并返回,第一个参数是Resolved
的回调,第二个是Rejected
的回调。
loadImageAsync('./image.png').then(res => {
return loadImageAsync(res.newUrl)
}).then(() => {console.log('success')}, () => {console.log('fail')})
2.2.2 Promise.prototype.catch()
Promise.prototype.then(null, rejection)
的别名,用于指定发生错误时的回调函数,也返回新的Promise
实例,可以链式调用,后面catch
会捕获前面catch
抛出的错误。建议使用catch
而不使用then(null, rejection)
// resolve改变了状态,抛错不会被捕获,没有任何反应
new Promise((resolve, reject) => {
resolve('success')
throw new Error('error')
}).catch(err => {
console.log(err)
})
// 会冒泡到最外层
new Promise((resolve, reject) => {
resolve('success')
setTimeout(() => throw new Error('error'), 0)
}).catch(err => {
console.log(err)
})
// Promise的fail被捕获,new error不会被捕获,也不会冒泡到外层
new Promise((resolve, reject) => {
reject('fail') // reject方法等同于 throw new Error()
}).catch(err => {
console.log(err) // fail
throw new Error('new error')
}).then(res => console.log(res))
2.2.3 Promise.all()
将多个 Promise
实例包装成一个新的 Promise
实例,参数为具有 Iterator
接口的数据且每个成员都是 Promise
实例。
状态改变情况:
- 所有成员状态都变成
Fulfilled
,新实例状态才会变成Fulfilled
并将所有成员返回值组成数组传递给回调函数。 - 任意一个成员状态变成
Rejected
,新实例状态就变成Rejected
并将第一个Rejected
实例的返回值传给回调函数
注: 任何数据成员的自行状态处理回调会屏蔽新实例回调的传参与行为
// p1处理catch
const p1 = new Promise((resolve, reject) => reject('error'))
.catch(err => console.log(err)) // error
const p2 = new Promise((resolve, reject) => resolve('success'))
const p3 = new Promise((resolve, reject) => resolve('success'))
Promise.all([p1, p2, p3]).then(res => console.log(res))
.catch(err => console.log(err)) // [undefined, 'success', 'success']
// p1处理catch, p3处理then
const p1 = new Promise((resolve, reject) => reject('error'))
.catch(err => console.log(err)) // error
const p2 = new Promise((resolve, reject) => resolve('success'))
const p3 = new Promise((resolve, reject) => resolve('success'))
.then(res => console.log(res)) // success
Promise.all([p1, p2, p3]).then(res => console.log(res))
.catch(err => console.log(err)) // [undefined, 'success', undefined]
2.2.4 Promise.race()
将多个 Promise
实例包装成一个新的 Promise
实例,传参与 Promise.all
相同。
状态改变情况:状态最先发生改变的成员决定新实例的状态与返回值。
2.2.5 Promise.resolve()
将参数转为Promise对象,状态为 Resolved
参数 | 行为 |
---|---|
Promise实例 | 不做修改,原封不动返回实例 |
具有then方法的对象 | 转为Promise对象后返回,并立即调用then方法 |
其他参数 | 返回具有Resolved状态的Promise对象,将参数传给回调 |
无参数 | 返回具有Resolved状态的Promise对象 |
2.2.6 Promise.reject()
将参数转为Promise对象,状态为 Rejected
。无论传参是什么,后续方法中的参数始终为原始传参。
const Obj = {
then () {
reject('error')
}
}
Promise.reject(Obj).catch(e => console.log(e === Obj)) // true
2.2.7 自行封装done()和finally()
done(): 用于回调链尾部,保证抛出任何可能出现的错误
// 书上说如下代码只要报错便会向全局抛错,然而then方法的onRejected会自行处理报错,并不会再运行之后的catch
Promise.prototype.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected)
.catch(err => {
setTimeout(() => { throw err }, 0)
}
)
}
new Promise((resolve, reject) => {
resolve('success')
}).then(res => {
console.log(res) // success
res.splice()
}).done(res => {
console.log(res)
}, err => {
console.log(err) // 报错
})
// 因此建议不传参,只用作抛错处理,代码改为
Promise.prototype.done = function (onFulfilled, onRejected) {
this.catch(err => {
setTimeout(() => { throw err }, 0)
})
}
finally(): 用于回调链尾部,执行无论怎么改变状态都会执行的函数
Promise.prototype.finally = function (callback) {
return this.then(
value => Promise.resolve(callback()).then(() => value),
error => Promise.resolve(callback()).then(() => { throw error }),
}
2.2.8 Promise.try()
该方法是一个提按,用于利用异步处理同步函数
// async await已写入规范
const f = () => console.log(1);
(async () => f())()
console.log(2)
// new Promise()
const f = () => console.log(1);
(
() => new Promise(
resolve => resolve(f()) // 利用Promise的创建立即执行
)
)()
console.log(2)
3. async
generator
函数的语法糖,将 *
换成async
,将yield
换成await
即可。
改进点:
- 更方便的调用,内置执行器,无需像
generator
一样调用next
方法 - 更好的语义
- 更广的适用性
- 更方便的操作,返回
Promise
对象
3.1 使用方法
async function test () {
const file = await getData(url)
return file
}
test().then(res => {console.log(res)}) // file
async
函数返回一个Promise
对象,返回值会成为then
方法的入参
await
命令后面一半跟着Promise
对象,如果不是,会用Promise.resolve
产生一个
3.2 注意点
- 由于
async
函数中await
出错会导致后续代码无法运行,因此最好是使用try ... catch...
包裹await
后使用 - 多个
await
命令如果不存在同步关系,最好让它们同时触发 await
用在非async
函数中会报错,在forEach
函数中会并发执行,可能得到错误结果
4. 异步I/O
4.1 为什么使用异步I/O
- 提升用户体验
- 更好的分配资源
4.2 非阻塞I/O与异步I/O
阻塞I/O: 调用之后必须等到系统内核完成所有操作之后,调用才结束。
非阻塞I/O: 调用之后立马返回调用状态,之后CPU可以用于处理其他事物。
4.2.1 轮询
- read:最原始、性能最低,重复调用来检查I/O状态
- select: read的改进方案,通过文件描述符上的时间状态判断,可同时检查1024个文件描述符
- poll: select的改进方案,通过链表避免数组长度限制,可检查更多的文件描述符
- epoll: Linux下效率最高I/O机制,进入轮询后休眠,I/O返回后唤醒,不再是遍历查询
轮询虽然可以满足非阻塞I/O需求,但是由于CPU会被用于遍历文件描述符或休眠,它仍然不够好。
4.2.2 多线程模拟
让部分线程进行阻塞I/O或者非阻塞I/O+轮询技术来实现数据的获取,让一个线程进行数据计算处理,通过线程之间的通信将I/O线程得到的数据传递给计算线程,这样就实现了异步I/O
5. Node中的异步
5.1 事件循环
Node的执行模式,会不断查看是否有事件待处理。有则取出事件与回调并执行,然后进入下个循环。如不再有需要执行的事件,则退出进程。
5.2 观察者
事件循环是生产者/消费者模型
, 异步I/O、网络请求等是事件的生产者,事件被传递到相应的观察者处,由事件循环从观察者处取出事件并处理。
5.3 请求对象
Javascript发起异步调用之后会产生一个对象,该对象包含传入的参数和当前方法、状态以及回调函数,对象包装完毕后被推入线程池,当线程池中有可用线程时,执行请求对象的I/O操作并将结果存入对象。
5.4 执行回调
当I/O操作执行完毕后会将结果储存在请求对象中,然后告知I/O观察者,事件循环取出请求对象后从对象中提取I/O操作结果以及回调函数并执行。
5.5 非I/O的异步
- 定时器
- process.nextTick() —— idle观察者,可理解为微事件
- setImmediate() —— check观察者,可理解为宏事件
优先级比较:
idle观察者 > I/O观察者 > check观察者