JS基础总结(4)—— 异步

1. 异步

本文分为 JSNode两部分,4章之前为JS,之后为Node,由于不是同时写的,看起来会有点跨越。

1.1 概念

异步任务是调用无法立即得到结果,需要额外的操作才能预期结果的任务,异步任务不进入主线程、而进入"任务队列",只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行

1.2 问题

由于异步调用无法确定数据返回的时间,因此回调函数调用的顺序无法保证。
为了确保函数的执行顺序,就出现了异步嵌套异步的方法,产生了非常丑陋的代码嵌套,又称为:回调地狱

1.3 解决方案

JS原生提供了很多异步的解决方案,在JSNode中直接使用即可。
比较流行的主要是Promiseasync 两种。

2. Promise

2.1 含义和使用

异步编程的一种解决方案,比传统的解决方案更合理且强大
Promise对象有两个特点:

  1. 具有三种状态 Pending, Fulfilled, Rejected。状态不受外界影响,只受异步操作结果影响
  2. 状态改变只有两种可能 Pending → FulfilledPending → Rejected 。只要状态改变便不会再变且一直保持这个接个

缺点:

  1. 一旦新建立即执行,无法取消且内部报错不会反应到外部
  2. 处于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 实例。
状态改变情况:

  1. 所有成员状态都变成 Fulfilled ,新实例状态才会变成 Fulfilled 并将所有成员返回值组成数组传递给回调函数。
  2. 任意一个成员状态变成 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即可。
改进点:

  1. 更方便的调用,内置执行器,无需像generator一样调用next方法
  2. 更好的语义
  3. 更广的适用性
  4. 更方便的操作,返回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 注意点

  1. 由于async函数中await出错会导致后续代码无法运行,因此最好是使用try ... catch...包裹await后使用
  2. 多个await 命令如果不存在同步关系,最好让它们同时触发
  3. 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的异步

  1. 定时器
  2. process.nextTick() —— idle观察者,可理解为微事件
  3. setImmediate() —— check观察者,可理解为宏事件

优先级比较:
idle观察者 > I/O观察者 > check观察者

发布了6 篇原创文章 · 获赞 0 · 访问量 81

猜你喜欢

转载自blog.csdn.net/weixin_44844528/article/details/104408867