原理深究之事件循环和异步流程控制

这篇文章是我读了掘金上一篇非常好的博客,总结的一些知识要点。主要讲事件循环的诞生背景(解决什么问题), 处理异步执行问题的思路(怎样解决的问题)以及javascript语言层面对于异步逻辑编写的封装。
掘金大佬多多,一篇博客开阔眼界,希望我能消化这些知识点吧,感觉偏原理了些。

事件循环

为什么要有事件循环

  • 归根究底,是因为JavaScript是一门单线程语言,也就是说,一次只能响应一个操做。那么对于一些比较耗费时间的操作,比如复杂的图片运算或等待服务返回数据,响应的时间就很长,可能造成页面假死。我们想要有一种机制,来实现,用户的操作无阻碍的进行,而这个时候,事件循环便产生了。
  • 事件循环主要是监控调用栈和回调队列,调用栈负责处理,JavaScript线程中的任务,当有ajax或settimeout这样的异步逻辑任务时,调用栈会执行他们,但并不会阻塞后续的操作,执行完后会把他们从调用栈中删除并且将相应的回掉函数放入回调队列中,等到调用栈中没有其他任务时,就将回掉队列中的函数加入调用栈中继续执行。

事件循环机制

  • 以下面这段代码为例:
setTimeout(function cb1() { 
console.log('cb1');
}, 5000);
console.log('Bye');
  • 执行这段代码,我们来看看发生了什么
  1. 添加console.log(‘Hi’)至调用栈
  2. console.log(‘Hi’)被执行
  3. console.log(‘Hi’)被移除出调用栈
  4. 添加setTimeout(function cb1() { … })至调用栈
  5. setTimeout(function cb1() { … })被执行,浏览器会根据web API创建一个定时器
  6. setTimeout(function cb1() { … })执行完成并被移除出调用栈
  7. 添加console.log(‘Bye’)到调用栈
  8. 执行console.log(‘Bye’)
  9. console.log(‘Bye’)被移除出调用栈,调用栈再度为空。
  10. https://user-gold-cdn.xitu.io/2017/12/2/16015434824d2066?imageslim
  11. 至少5000ms后,定时器执行完成,此时它会将cb1回调函数加入到回调队列中
  12. 事件循环检测到此时调用栈为空,将cb1取出压入到调用栈中
  13. cb1被执行,console.log(‘cb1’)被压入调用栈
  14. console.log(‘cb1’)被执行
  15. console.log(‘cb1’)被移除出调用栈
  16. cb1被移除出调用栈
  • 动态图为:
  • 事件循环机制

事件循环机制总结

  • 通过上面的例子我们可以清楚的了解到代码在执行过程中,事件循环、调用栈、回调任务队列的合作机制。同时也了解到,我们常见的setTimeout这类方法并不是我们认为的按照特定的时刻执行,而是在该时刻它会被加入到回调队列中,等待调用栈没有在执行中的任务的时候,才会由事件循环去读取它,将其放到调用栈中执行,如果调用栈一直有任务在执行,那么该回调函数会一直被阻塞,即使传给setTimeOut方法的时间参数0ms也是一样。由此可见,异步任务的执行时机是不可预测的,可是我们要如何让不同的异步回调任务按照我们想要的顺序去执行呢,这就需要用到异步流程控制的解决方案了。

异步控制流程

回调函数

  • JavaScript在发展,对异步流程的控制也有了越来越多的解决方案。以时间线来看,主要有四种。
  • 回调函数,假设我们希望xx2的请求发生在xx1的请求完成之后,那么来看下面这段代码:
以jquery中的请求为例
$.ajax({
url: 'xx1',
success: function () {
console.log('1');
$.ajax({
url: 'xx2',
success: function () {
console.log('2')
}
})
}
})
  • 上面这段代码,我们通过在xxx1请求完成后的回调函数中发起xxx2请求的这种回调嵌套的方式来实现两个异步任务的执行顺序控制。这种回调函数的方式在ES6出现之前是应用最广泛的实现方案,但是有一个问题,当需要执行的异步任务特别多的时候,会出现多级嵌套,最后可能会导致回调地狱的产生,降低代码的可读性。

Promise

  • es6中提供了promise的语法糖对异步流程做了更好的封装处理,它提供了更加优雅的方式管理异步任务的执行,可以让我们以一种接近同步的方式来编写异步代码。依旧以上述的两个请求处理作为示例:
ajax1
  return new Promise(function (resolve, reject) {
$.ajax({
url: 'xx1',
success: function () {
console.log('1')
resolve()
}
})
})
}
ajax1().then(() => {
$.ajax({
url: 'xx1',
success: function () {
console.log('2')
}
})
})
  • Promisethen方法的链式调用将需要按顺序执行的异步任务串起来在哪个位置、在代码可读性方面有很大的提升。
  • 究其实现原理,Promise是一个构造函数,他有三个状态,分别为:pending,fullfilled,rejected。构造函数接受一个回调作为参数,在该回调函数中执行异步任务,后通过resolve或者reject将promise的状态由pending置为fullfilled或者rejected。
  • Promise的原型对象上定义了then方法,该方法的作用是将传递给它的函数压入到resolve或者reject状态对应的数组中,当Promise的状态发生改变时,依次执行与状态相对应的数组中的回调函数,此外,promise在其原型上还提供了catch方法来处理执行过程中遇到的异常。
  • Promise函数本身也有两个熟悉race,all。race和all都接受一个Promise实例数组作为参数,两者的区别在于前者只要数组中的某个Promis任务先执行完成就会直接调用回调函数中的函数,后者需要等待全部的promise任务执行完成。
  • 一个mini的promise代码实现如下图所示:
Promise (fn) {
  this.status = 'pending';
this.resolveCallbacks = [];
this.rejectCallbacks = [];
let _this = this
function resolve (data) {
_this.status = 'fullfilled'
_this.resolveCallbacks.forEach((item) => {
if (typeof item === 'function') {
item.call(this, data)
}
})
}
function reject (error) {
_this.status = 'rejected'
_this.rejectCallbacks.forEach((item) => {
if (typeof item === 'function') {
item.call(this, error)
}
})
}
fn.call(this, resolve, reject)
}
Promise.prototype.then = function (resolveCb, rejectCb) {
this.resolveCallbacks.push(resolveCb)
this.rejectCallbacks.push(rejectCb)
}
Promise.prototype.catch = function (rejectCb) {
this.rejectCallbacks.push(rejectCb)
}
Promise.race = function (promiseArrays) {
let cbs = [], theIndex
if (promiseArrays.some((item, index) => {
return theIndex = index && item.status === 'fullfilled'
})){
cbs.forEach((item) => {
item.call(this, promiseArrays[theIndex])
})
}
return {
then (fn) {
cbs.push(fn)
return this
}
}
}
Promise.all = function (promiseArrays) {
let cbs = []
if (promiseArrays.every((item) => {
return item.status === 'fullfilled'
})) {
cbs.forEach((item) => {
item.call(this)
})
}
return {
then (fn) {
cbs.push(fn)
return this
}
}
}
  • 以上是对promise的一个非常简短的实现 ,主要是为了说明promise的封装运行原理,他对异步的任务的管理是如何实现的。

Generator函数

  • generator也是es6中新增的一种语法糖,它是一种特殊的函数,可以被用来做异步流程管理。依旧以之前的ajax请求作为示例, 来看看用generator函数如何做到流程控制:
ajaxManage () {
  yield $.ajax({
url: 'xx1',
success: function () {
console.log('1')
}
})
yield $.ajax({
url: 'xx2',
success: function () {
console.log('2')
}
})
return 'ending'
}
var manage = ajaxManage()
manage.next()
manage.next()
manage.next() // return {value: 'ending', done: true}
  • 在上述示例中我们定义了ajaxManage这个generator函数,但是当我们调用该函数时他并没有真正的执行其内部逻辑,而是会返回一个迭代器对象,generator函数的执行与普通函数不同,只有调用迭代器对象的next方法时才会去真正执行我们在函数体内编写的业务逻辑,且next方法的调用只会执行单个通过yield或return关键字所定义的状态,该方法的返回值是一个含有value以及done这两个属性的对象,value属性值为当前状态值,done属性值为false表示当前不是最终状态。
    我们可以通过将异步任务定义为多个状态的方式,用generator函数的迭代器机制去管理这些异步任务的执行。这种方式虽然也是一种异步流程控制的解决方案,但是其缺陷在于我们需要手动管理generator函数的迭代器执行,如果我们需要控制的异步任务数量众多,那么我们就需要多次调用next方法,这显然也是一种不太好的开发体验。
    为了解决这个问题,也有很多开发者写过一些generator函数的自动执行器,其中比较广为人知的就是著名程序员TJ Holowaychuk开发的co 模块,有兴趣的同学可以多了解下。

async/await

  • async/await是es8中引入的一种处理异步流程控制的方案,它是generator函数的语法糖,可以使异步操作更加简洁方便,还是用之前的示例来演示下async/await这种方式是如何使用的:
function ajaxManage () {
  await $.ajax({
url: 'xx1',
success: function () {
console.log('1')
}
})
await $.ajax({
url: 'xx2',
success: function () {
console.log('2')
}
})
}
ajaxManage()
  • 通过代码示例可以看出,async/await在写法上与generator函数是极为相近的,仅仅只是将*号替换为async,将yield替换为await,但是async/await相比generator,它自带执行器,像普通函数那样调用即可。另一方面它更加语义化,可读性更高,它也已经得到大多数主流浏览器的支持。
    async/await
  • async/await相比promise可以在很多方面优化我们的代码,比如:
  • 代码更精简清晰,比如多个异步任务执行时,使用promise需要写很多的then调用,且每个then方法中都要用一个function包裹异步任务。而async/await就不会有这个烦恼。此外,在异常处理,异步条件判断方面,async/await都可以节省很多代码。
  • 报错定位更加准确
  • debug调试问题 : 如果你在promise中使用过断点调试你就会知道这是件多么痛苦的事,当你在then方法中设置了一个断点,然后debug执行时此时如果你想使用step over跳过这段代码,你会发现在promise中无法做到这点, 因为debugger 只能跳过同步代码。而在async/await中就不会有这个问题,await的调用可以像同步逻辑那样被跳过。

结语

事件循环是宿主环境处理JavaScript单线程带来的执行阻塞问题的解决方案,所谓异步,就是当事件发生时将指定的回调加入到任务队列中,等待调用栈空闲时将事件循环将其取出压入到调用栈中执行,从而达到不阻塞主线程的目的。因为异步回调的执行时机是不可预测的,所以我们需要一种解决方案可以帮助我们实现异步执行流程控制。

引用来源

最后一定要注重版权,尊重他人劳动成果,这是我文章引用内容的来源:

作者:墨筝
链接:https://juejin.im/post/5a2e21486fb9a0450407d370
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

文章作者: Rong
文章链接: http://yoursite.com/2023/05/20/原理深究之事件循环和异步流程控制/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 jsRongのBlog