同步模式和异步模式的差异和意义?

同步执行模式:

代码中的任务依次执行,执行顺序和代码顺序一致。
示例如下:

  1. console.log('全局开始')
  2. function bar(){
  3. console.log('bar 任务')
  4. }
  5. function foo(){
  6. console.log('foo 任务')
  7. bar()
  8. }
  9. foo()
  10. console.log('全局结束')

执行简述:

  1. JavaScript 执行引擎首先会在调用栈里压入一个匿名函数,同时把所有代码放入匿名函数中并执行,然后逐行执行每一行的代码。
  2. 第 1 行压入调用栈执行,执行完毕后弹出。
  3. bar 函数和 foo 函数的声明不会触发调用,会继续向下走。
  4. 第 9 行 foo 函数压入调用栈调用,不弹出。
  5. 第 6 行压入调用栈执行,执行完毕后弹出。
  6. 第 7 行 bar 函数压入调用栈调用,不弹出。
  7. 第 3 行压入调用栈执行,执行完毕后弹出。
  8. bar 函数执行完毕,从调用栈弹出。
  9. foo 函数执行完毕,从调用栈弹出。
  10. 第 10 行压入调用栈执行,执行完毕后弹出。
  11. 整体代码执行结束,调用栈清空。

总结:
同步模式就是代码一行一行的顺序执行,如果某行耗时严重,程序将被阻塞,阻塞对于用户来讲就是卡死。
因此我们必须要使用异步模式来解决程序中无法避免的耗时操作,避免页面程序卡死。

异步执行模式:

Api 不会等待任务结束才会执行下个任务,耗时任务开始后就立即执行下个任务,异步任务的后续一般通过回调函数的方式定义,异步任务执行完毕时会自动执行回调函数。
异步模式的缺点:代码执行顺序比较跳跃,理解起来比较混乱。
示例如下:

  1. console.log('全局开始')
  2. setTimeout(function timer_1() {
  3. console.log('timer_1 exec')
  4. }, 1800)
  5. setTimeout(function timer_2() {
  6. console.log('timer_2 exec')
  7. setTimeout(function inner() {
  8. console.log('inner exec')
  9. }, 1000)
  10. }, 1000)
  11. console.log('全局结束')

需要注意的是,异步模式比同步模式多出了三个概念:事件循环、消息队列、平台API(Web 就是 Web Apis)。 也有人把消息队列称为回调队列。

执行简述:

  1. JavaScript 执行引擎首先会在调用栈里压入一个匿名函数,同时把所有代码放入匿名函数中并执行,然后逐行执行每一行的代码。
  2. 第 1 行压入调用栈执行,执行完毕后弹出。
  3. 第 3 行 setTimeout 压入调用栈,这里因为函数是异步调用,Web Apis 会在内部为 timer 函数开启一个 1.8s 的倒计时器然后放到一边儿玩时间沙漏去了,而这个沙漏是单独工作不受 JavaScript 单线程影响的,会立即开始倒数。同时需要注意的是,对于 setTimeout 来说,它已经调用完毕了,于是 setTimeout 会弹出调用栈。
  4. 第 7 行 setTimeout 压入调用栈,timer_2 开启 1s 倒计时器也玩沙漏去了,然后 setTimeout 弹出调用栈。
  5. 第 14 行压入调用栈执行,执行完毕后弹出。
  6. 此时所有代码执行完毕,清空调用栈。
  7. Event Loop 会始终监听调用栈和消息队列,一旦调用栈里所有的任务都结束了,那么事件循环就会从消息队列当中取出第一个回调函数。但此时消息队列是空的不存在未执行的函数,因为两个 timer 还都在玩沙漏。
  8. 在时间过去 1s 后,timer_2 率先结束玩耍时间,进入了消息队列的第一个位置。
  9. 在时间又过去 0.8s 后,timer_1 紧随其后也进入了消息队列,此时它会被放置在第二个位置。
  10. 一旦消息队列当中发生了变化,事件循环就会监听到。然后它会把消息队列当中的第一个也就是 timer_2 函数压入调用栈。
  11. 第 8 行压入调用栈执行,执行完毕后弹出。(此时调用栈里 timer_2 还在)
  12. 第 9 行 setTimeout 压入调用栈,inner 玩沙漏去后,setTimeout 弹出。
  13. timer_2 弹出,清空调用栈。
  14. 事件循环监听到调用栈无任务执行,把消息队列中第一位 timer_1 取出压入调用栈。
  15. 第 4 行压入调用栈执行,执行完毕后弹出。
  16. 时间过去 1s 后,inner 进入消息队列。
  17. 事件循环从消息队列取出 inner 压入调用栈。
  18. 第 10 行压入调用栈执行,执行完毕后弹出。
  19. inner 弹出,清空调用栈。

    需要注意的是,示例中的任务(setTimout)耗时是在玩沙漏,这里的耗时可以是任意耗时任务,都会独立在 JavaScript 单线程外执行,执行完毕后就会把回调函数放入消息队列,这中间的所有环节都不会阻塞单线程那边的代码执行,也不会去管调用栈和消息队列里是什么情况,异步任务执行结束了就会把回调排在消息队列后面。

总结:
我们从执行过程中可以看出,异步模式核心是通过事件循环和消息队列实现的。
而我们解决耗时任务阻塞程序的取巧之处,就是 JavaScript 确实是单线程的,而浏览器却是多线程的,也就是我们利用了 JavaScript 提供的一些 异步 Api,如:setTimeout 等。这些 异步 Api 内部就会有独立的线程做需要等待的操作(耗时任务)。而我们一直说的单线程则指的是:执行代码的线程是一个线程。
还需要注意的是,同步模式和异步模式指的并不是我们代码的抒写方式,而是我们代码里使用的 Api 是以同步模式还是异步模式的方式工作的。