前言
js 作为一门单线程的语言,则注定异步编程会是其最核心的内容。设想一下如果 js 不支持异步会怎样?
代码从上至下顺序执行,遇到计算量较大的算法则发生阻塞,即使后面有非常紧急的事情也只能等着。进而影响到用户的体验。
异步编程的发展过程
回调时期
在 es6 提供 promise 以前,处理异步操作必然绕不开回调,即执行完某方法后调用某个回调方法,伪代码如下
const _loadSomething = (callback) => {
....某异步请求,返回结果为 res
callback(res);
}
_loadSomething((res) => {
console.log(res);
});
但回调里如果还有异步操作或回调的回调里仍然有呢?
const callback = (res, cellCallback) => {
cellCallback(res);
}
const _loadSomething = (callback) => {
....某异步请求,返回结果为 res
callback(res, () => {
callback(res, () => {
callback(res, () => {
console.log('end')
})
})
});
}
_loadSomething(callback1);
如果遇到以上场景,那必然会出现回调层级过深,影响代码可读性
为了解决此问题,es6 推了 promise
Promise 时期
promise 的设计很巧妙,它将回调的形式转换为了链式调用
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve('promise1')
}, 1000)
}).then((value1) => {
console.log(value1);
return new Promise((resolve) => {
setTimeout(() => {
resolve('promise2')
})
})
}).then((value2) => {
console.log(value2)
return new Promise((resolve) => {
resolve('promise3')
})
})
上方代码在执行了 resolve 方法后会通知其观察者,也就是 then 中的回调函数,若 then 的回调中仍有异步操作,那可以继续创建 promise 对象,逐渐形成一个执行链。
promise 的出现无疑是一大进步、但可读性仍然没有同步代码强,那有没有办法将其看起来比较像同步执行呢
Generator 时期
使用 “*” 定义的 generator 方法内部的代码将被强制中断,只有调用了其方法所产生的 generator 对象的 next 方法才可执行到下一个 yield 处,next 方法会返回 yield 后的执行结果,若想第二个 yield 处需要使用,则可将值通过 next 的形参传入
function *lazyFunction() {
console.log('start')
const promise1 = yield new Promise((resolve) => {
resolve(1)
})
yield new Promise((resolve) => {
resolve(2)
})
}
const generator = lazyFunction();
const { value: promise1 } = generator.next();
const { value: promise2 } = generator.next(promise1);
promise1.then((value) => {
console.log(value)
})
promise2.then((value) => {
console.log(value)
})
若在某个异步操作后才能执行某方法,使用 generator 后的代码将变成这样,在方法内部各个异步方法从上向下书写,一眼看去就像是串同步代码,但有一个问题是
generator 操作起来太麻烦了!
我需要定义懒函数,就是执行都需要手动 next,戳一下跳一下,只能说 generator 太不讲武德。
所以开发中最常用的 async 来了
Async Await
async await 实际上是一种对于 generator 的封装,将其手动执行变为自动执行
定义与 generator 基本一致,只是将关键字改为 async 和 await
async function asyncFunction() {
const value1 = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
})
});
console.log(value1)
const value2 = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2)
})
});
console.log(value2)
}
asyncFunction();
当代码执行到 await 处时,async 方法内部将暂停,只有当 promise 的 resolve 方法执行,代码才会继续向下走
注意:若 await 后的 promise 状态变为 fail ,即调用了 reject 方法,那 async 方法内部将彻底中断。
这样一来我们就再也看不见,过多的回调或繁琐的 next 操作了,对编程体验无疑有巨大的提升
不论是 promise、generator 还是 async await,其实都是代码设计上的优化,内部实际上还是回调加回调的形式,所以熟悉整个异步事件的执行过程显得更加重要
事件轮询
在 js 的异步编程中有一个事件轮询的概念,这里的事件指异步事件,轮询即是周期性的访问的意思。
js 代码在运行过程中遇到异步代码的解释过程如下
- 在经子线程计时后放进任务队列
- 主线程代码执行完毕,询问任务队列,有可执行任务则推入执行栈,执行完毕后弹出
- 进入下一次的轮询
值得注意的是,异步任务分为两种:宏任务、微任务(仅考虑在浏览器环境下)
宏任务 | 微任务 |
---|---|
setTimeout | promise.then/catch/finaly |
setInterval | |
requestAnimationFrame |
所有代码执行的优先级关系为:微任务 --> 宏任务
也就是同在回调队列时,微任务执行优先级大于宏任务
所以下方代码的执行顺序是什么呢?
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
setTimeout(() => {
console.log(3)
})
new Promise((resolve) => {
console.log(4)
resolve('data')
}).then((data) => {
console.log(data)
console.log(5)
})
console.log(6)
自上而下解析
同步代码:1 --> 4 --> 6
微任务:data --> 5
宏任务:2 --> 3
所以最后的顺序是:1 --> 4 --> 6 --> data --> 5 --> 2 --> 3
所以可证实 js 代码的执行顺序为:同步代码 --> 微任务 --> 宏任务
写在最后
以上只是一些比较基础的知识,但以此衍生出的框架、库层出不穷,对于前端开发人员的挑战也越来越大,设计虽各有精妙,但原理却大同小异,所以我认为学习当以不变应万变,掌握最根本的技术,才能走的更远。