前言
本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。
JavaScript在浏览器中负责执行代码的线程只有一个,我们把线程比作人,可以想象一下只有一个人在执行任务,如果有多个任务,那么就会需要排队依次执行这些任务。这种模式的优点是更加安全和简单,缺点也很明显,如果有特别耗时的任务,其他任务就必须等待这个任务完成,那么就会出现假死的情况。为了解决这种情况,js设计了两种模式,同步模式和异步模式。
同步模式
同步模式其实就是需要执行的任务依次执行,后一个任务必须要等到前一个任务结束才能开始执行。程序的执行顺序和代码的执行顺序是一致的,看起来比较简单。 让我们看一个简单的同步代码:
console.log('globalBegin')
function bar () {
console.log('bar')
}
function foo () {
console.log('foo')
bar()
}
foo()
console.log('globalEnd')
首先JS引擎会把我们整体的代码作为一个匿名调用压入到调用栈中(call stack),然后逐行执行js代码,第一行是 console.log,js引擎会把 console.log 压入调用栈中执行,执行的过程中,在控制台里面 ,然后在调用栈中弹出 console.log。然后是两个函数的声明,不管是函数还是变量的声明都不会产生调用,因此会往下走foo函数的调用,这时需要把foo函数压入调用栈,foo函数中第一行是console.log ,因此在调用栈执行console.log ,控制台 ,并弹出console.log,foo函数第二行是bar函数的调用,这时需要把bar函数压入调用栈中,bar函数的第一行是console.log ,控制台,bar函数执行完毕,弹出bar调用,这时foo函数也执行完毕,调用栈弹出foo调用,最后执行 console.log, 。
异步模式
异步模式是指执行的耗时任务可以不需要等待,开启过后就立即执行下一个任务,耗时任务一般会通过回调函数的方式定义,耗时任务完成后自动执行这个回调函数。例如你马上要做的任务是烧水15分钟、打扫卫生10分钟、遛狗10分钟,你可以先烧水,然后不等待烧水结束。直接打扫卫生、然后遛狗,这样的耗时是20分钟,如果要同步执行需要35分钟,效率大打折扣, 具体我们看一下代码:
console.log('global begin')
setTimeout(function timer1 () {
console.log('timer1 invoke')
}, 1800)
setTimeout(function timer2 () {
console.log('timer2 invoke')
setTimeout(function inner () {
console.log('inner invoke')
}, 1000)
}, 1000)
console.log('global end')
异步模式中,不光有调用栈(call stack),还有web APIs 、 Queue 、Event loop的概念
和同步模式一样,首先JS引擎会把我们整体的代码作为一个匿名调用压入到调用栈中(call stack),然后逐行执行js代码,第一行是 console.log,js引擎会把 console.log 压入调用栈中执行,执行的过程中,在控制台里面打印 global begin ,然后在调用栈中弹出 console.log。接下来是第一个 setTimeout,把第一个 setTimeout 压入调用栈中,在 web APIs中为 timer1 开启了一个倒计时器,把第一个 setTimeout 弹出。然后是第二个 setTimeout,把这个 setTimeout 压入调用栈中,在 web APIs 中为 timer2 开启一个倒计时器,然后弹出这个 setTimeout。接着执行到最后一行,压入console.log,输出 global end , 弹出 console.log。这时候匿名调用里没有任务执行,弹出匿名调用,调用栈清空。调用栈为空的时候,event loop 开始执行,把消息队列(Queue)的回调函数依次放入调用栈中,这时消息队列是空的(最快的回调函数在 web APIs里是在一秒之后才需要执行)。1秒过后, timer2 的回调函数压入Queue,event loop执行,timer2回调函数压入调用栈中,控制台打印 timer2 invoke, 把 inner 压入 web APIs中,0.8秒后, timer1 的回调函数压入Queue,event loop执行,timer1回调函数压入调用栈中,控制台打印 timer1 invoke,0.2秒后, inner 的回调函数压入Queue,event loop执行,inner 回调函数压入调用栈中,控制台打印 inner invoke。
具体异步流程可以看下图:
在图中有一个异步调用线程,有些读者会产生疑惑,这个js不是单线程吗。js确实是单线程,上方绿色的线程就是js的线程,但是浏览器不是单线程的,js调的某些 web APIs不是单线程的,我们所说的单线程是指我们代码的线程是单线程,某些 web APIs 会用自己的线程执行异步操作,运行环境提供的API是以同步或异步模式的方式工作。