文章首发于语雀。
页面交互的核心是 Dom 操作,多线程操作 Dom 肯定会发生问题,所以就设计为单线程。
这样,JS 执行环境中负责执行代码的线程就只有一个了。
那么如果有多个任务要执行,怎么办呢?那就排队!
这种模式的优点就是更安全、更简单。
但同时就会带来缺点:耗时任务会拖延排在它后面所有任务的执行。
这就是单线程 JavaScript 的严重问题:无法同时处理大量的耗时任务。同时也就出现了耗时任务阻塞程序执行的问题。那么为了解决这个问题,JavaScript 把任务的执行模式分成了两种:同步模式 和 异步模式。
那么下面我们主要讲解关于异步编程的知识。
- 同步模式和异步模式的差异和意义?
- JavaScript 单线程如何实现异步模式?(事件循环和消息队列)
- JavaScript 异步编程有几种方法?
- Promise 异步编程解决方案如何上手?其中牵扯到的宏任务和微任务是什么?
- Generator 异步编程解决方案如何上手?
- 如何使用 Async/Await 语法糖写出更扁平的异步代码?
同步模式和异步模式的差异和意义?
同步执行模式
代码中的任务依次执行,执行顺序和代码顺序一致。
示例如下:
console
执行简述:
- JavaScript 执行引擎首先会在调用栈里压入一个匿名函数,同时把所有代码放入匿名函数中并执行,然后逐行执行每一行的代码。
- 第 1 行压入调用栈执行,执行完毕后弹出。
- bar 函数和 foo 函数的声明不会触发调用,会继续向下走。
- 第 9 行 foo 函数压入调用栈调用,不弹出。
- 第 6 行压入调用栈执行,执行完毕后弹出。
- 第 7 行 bar 函数压入调用栈调用,不弹出。
- 第 3 行压入调用栈执行,执行完毕后弹出。
- bar 函数执行完毕,从调用栈弹出。
- foo 函数执行完毕,从调用栈弹出。
- 第 10 行压入调用栈执行,执行完毕后弹出。
- 整体代码执行结束,调用栈清空。
总结:
同步模式就是代码一行一行的顺序执行,如果某行耗时严重,程序将被阻塞,阻塞对于用户来讲就是卡死。
因此我们必须要使用异步模式来解决程序中无法避免的耗时操作,避免页面程序卡死。
异步执行模式:
Api 不会等待任务结束才会执行下个任务,耗时任务开始后就立即执行下个任务,异步任务的后续一般通过回调函数的方式定义,异步任务执行完毕时会自动执行回调函数。
异步模式的缺点:代码执行顺序比较跳跃,理解起来比较混乱。
示例如下:
console
需要注意的是,异步模式比同步模式多出了三个概念:事件循环、消息队列、平台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 是以同步模式还是异步模式的方式工作的。
致谢:感谢拉勾教育提供的学习环境。