同步模式和异步模式的差异和意义?
同步执行模式:
代码中的任务依次执行,执行顺序和代码顺序一致。
示例如下:
console.log('全局开始')
function bar(){
console.log('bar 任务')
}
function foo(){
console.log('foo 任务')
bar()
}
foo()
console.log('全局结束')
执行简述:
- JavaScript 执行引擎首先会在调用栈里压入一个匿名函数,同时把所有代码放入匿名函数中并执行,然后逐行执行每一行的代码。
- 第 1 行压入调用栈执行,执行完毕后弹出。
- bar 函数和 foo 函数的声明不会触发调用,会继续向下走。
- 第 9 行 foo 函数压入调用栈调用,不弹出。
- 第 6 行压入调用栈执行,执行完毕后弹出。
- 第 7 行 bar 函数压入调用栈调用,不弹出。
- 第 3 行压入调用栈执行,执行完毕后弹出。
- bar 函数执行完毕,从调用栈弹出。
- foo 函数执行完毕,从调用栈弹出。
- 第 10 行压入调用栈执行,执行完毕后弹出。
- 整体代码执行结束,调用栈清空。
总结:
同步模式就是代码一行一行的顺序执行,如果某行耗时严重,程序将被阻塞,阻塞对于用户来讲就是卡死。
因此我们必须要使用异步模式来解决程序中无法避免的耗时操作,避免页面程序卡死。
异步执行模式:
Api 不会等待任务结束才会执行下个任务,耗时任务开始后就立即执行下个任务,异步任务的后续一般通过回调函数的方式定义,异步任务执行完毕时会自动执行回调函数。
异步模式的缺点:代码执行顺序比较跳跃,理解起来比较混乱。
示例如下:
console.log('全局开始')
setTimeout(function timer_1() {
console.log('timer_1 exec')
}, 1800)
setTimeout(function timer_2() {
console.log('timer_2 exec')
setTimeout(function inner() {
console.log('inner exec')
}, 1000)
}, 1000)
console.log('全局结束')
需要注意的是,异步模式比同步模式多出了三个概念:事件循环、消息队列、平台API(Web 就是 Web Apis)。 也有人把消息队列称为回调队列。
执行简述:
- JavaScript 执行引擎首先会在调用栈里压入一个匿名函数,同时把所有代码放入匿名函数中并执行,然后逐行执行每一行的代码。
- 第 1 行压入调用栈执行,执行完毕后弹出。
- 第 3 行 setTimeout 压入调用栈,这里因为函数是异步调用,Web Apis 会在内部为 timer 函数开启一个 1.8s 的倒计时器然后放到一边儿玩时间沙漏去了,而这个沙漏是单独工作不受 JavaScript 单线程影响的,会立即开始倒数。同时需要注意的是,对于 setTimeout 来说,它已经调用完毕了,于是 setTimeout 会弹出调用栈。
- 第 7 行 setTimeout 压入调用栈,timer_2 开启 1s 倒计时器也玩沙漏去了,然后 setTimeout 弹出调用栈。
- 第 14 行压入调用栈执行,执行完毕后弹出。
- 此时所有代码执行完毕,清空调用栈。
- Event Loop 会始终监听调用栈和消息队列,一旦调用栈里所有的任务都结束了,那么事件循环就会从消息队列当中取出第一个回调函数。但此时消息队列是空的不存在未执行的函数,因为两个 timer 还都在玩沙漏。
- 在时间过去 1s 后,timer_2 率先结束玩耍时间,进入了消息队列的第一个位置。
- 在时间又过去 0.8s 后,timer_1 紧随其后也进入了消息队列,此时它会被放置在第二个位置。
- 一旦消息队列当中发生了变化,事件循环就会监听到。然后它会把消息队列当中的第一个也就是 timer_2 函数压入调用栈。
- 第 8 行压入调用栈执行,执行完毕后弹出。(此时调用栈里 timer_2 还在)
- 第 9 行 setTimeout 压入调用栈,inner 玩沙漏去后,setTimeout 弹出。
- timer_2 弹出,清空调用栈。
- 事件循环监听到调用栈无任务执行,把消息队列中第一位 timer_1 取出压入调用栈。
- 第 4 行压入调用栈执行,执行完毕后弹出。
- 时间过去 1s 后,inner 进入消息队列。
- 事件循环从消息队列取出 inner 压入调用栈。
- 第 10 行压入调用栈执行,执行完毕后弹出。
- inner 弹出,清空调用栈。
需要注意的是,示例中的任务(setTimout)耗时是在玩沙漏,这里的耗时可以是任意耗时任务,都会独立在 JavaScript 单线程外执行,执行完毕后就会把回调函数放入消息队列,这中间的所有环节都不会阻塞单线程那边的代码执行,也不会去管调用栈和消息队列里是什么情况,异步任务执行结束了就会把回调排在消息队列后面。
总结:
我们从执行过程中可以看出,异步模式核心是通过事件循环和消息队列实现的。
而我们解决耗时任务阻塞程序的取巧之处,就是 JavaScript 确实是单线程的,而浏览器却是多线程的,也就是我们利用了 JavaScript 提供的一些 异步 Api,如:setTimeout 等。这些 异步 Api 内部就会有独立的线程做需要等待的操作(耗时任务)。而我们一直说的单线程则指的是:执行代码的线程是一个线程。
还需要注意的是,同步模式和异步模式指的并不是我们代码的抒写方式,而是我们代码里使用的 Api 是以同步模式还是异步模式的方式工作的。