细聊 JavaScript 的事件执行机制
我使用的 Node.js 版本是 v12.13.0 的 , 11 的版本前会与11 版本后的事件循环有些区别。
线程?
都说 JavaScript 是单线程的 ,那么什么是单线程呢?
单线程就是进程只有一个线程。
那这又扯到进程这个概念了。
进程
进程 (Process): 进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。
简而言之,就是存在内存没有执行的程序叫程序,执行起来叫进程
简单说了一下进程,那么就得看看什么是线程了。
线程
线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
线程是进程的一个实体,一个进程可以有多个线程。它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
一看更不好理解 ,我的理解是去类比一个活动的执行 ,必然需要一个执行任务的人,或者多个执行任务的人,那么这些人就是线程。也就是活动再细分成任务,那么人就是执行这些任务的基本单位 ,当所有任务完成,也就是活动的完成。
单线程和多线程
那么些微了解了什么是线程,那么就容易理解什么是单线程什么是多线程了。
单线程和多线程都是基于一个进程而言的 。一个进程必须有一个或以上的线程。
单线程进程在执行任务时,只有一个线程可以去执行,并且在同一时刻只能执行一个任务。
而多线程进程在执行任务时是可以同时执行多个任务的。
同步?异步?
同步(sync):也就是同一时间只能做一件事,就拿上面的单线程任务执行过程来看 ,前一个任务完成之前,第二个任务是不会被执行的 。
异步(async) :异步是执行一个任务时,即使这个任务没有结束,也可以继续下去,执行其他任务。
那么根据这两个概念,就可以把任务分成同步任务和异步任务。
同步任务:在任务没有结果前,线程不会继续执行,而是等到当前任务完成后再执行。
异步任务: 执行到此任务时,会把此任务交给其他机制管理 ,而线程去执行其他任务,等到有结果了再返回给线程来继续处理。
对于异步任务的调用,调用的返回并不受调用者控制。
对于通知调用者的三种方式,具体如下:
-
状态:即监听异步任务的状态(轮询),调用者需要每隔一定时间检查一次,效率会很低。
-
通知:当被调用者执行完成后,发出通知告知调用者,无需消耗太多性能。
-
回调:与通知类似,当被调用者执行完成后,会调用调用者提供的回调函数。
同步和异步的区别
同步与异步的区别不在于任务执行的时间。也就是同步任务可能比异步任务执行的时间要长。
主要区别是线程在执行这些任务时,是否需要立即得到结果。需要的是同步,得到结果了才继续执行,而异步是不需要立即得到结果,那么线程可以继续执行也可以等待。
JavaScript 如何做到异步
那么既然 JavaScript 是单线程的,按理说应该是同步的。但同步会遇到一个问题。
就是如果一个JS执行时间太长。就会造成页面不连贯,卡顿。比如我发送了一个异步请求,而请求回来前,我即不能点击其他按钮,也不能滚动页面(也就是我做了这些动作,但是没有响应)。因此体验十分不好。
那么 JS 需要异步,但是执行异步任务,就需要把异步任务交给其他线程。那么如何实现异步呢?
别忘了 JavaScript 代码是在一个 JavaScript 运行时的环境里执行的 。如游览器 ,或者 Node.js 。
他们可以为 JavaScript 提供其他线程 。具体在 JS 执行机制一节会介绍。
堵塞?非堵塞?
堵塞: 是指线程执行任务时,在没有得到结果前,线程会被挂起,不会去执行其他任务了。等得到结果才会继续执行
非堵塞: 是指线程执行任务时,即使不能立即得到结果,该任务也不会影响线程的继续执行其他任务。
同步,异步 与 堵塞,非堵塞的区别
这么看起来 ,堵塞和同步 ,非堵塞和异步似乎很像,有些一模一样了 。但是它们是不一样的。
同步和异步是关于任务的 ,而堵塞和非堵塞是关于线程的。它们有一定的关系,但并不相等。
如何理解呢?
- 同步和异步是关于任务的 : 比如我有一个快递 ,同步是我自己去做 。由我亲自完成。而异步就是我让我同学帮我拿快递,我不用自己去。
- 堵塞和非堵塞 : 非堵塞就是 比如我去拿快递,路上我也可以顺便买些东西,只不过会先停止去拿快递。但拿快递并没有堵塞我想做什么 。 而堵塞就比如 我让舍友去拿快递,我有空,但是我什么都不想做,就等快递回来。
如此理解,就是说同步异步的任务不会直接决定线程的堵塞和非堵塞 。
因此我们就有以下4中情况 :
- 同步堵塞 : 执行一个同步任务,没有结果,不执行其他,线程被挂起。等有结果了再继续执行其他任务
我去买东西,发现没钱了,叫同学发红包,同学还没回,我就付不了。
- 同步非堵塞 : 任务是同步的 ,但我过程中,暂时停止的这个同步任务,去执行了其他任务,同时又可能时不时回来继续执行。
我不能同时抄作业,又同时打游戏,但我可以打一会,又玩一会。只要我的速度快,你就可能以为我是同时进行的
- 异步堵塞 : 执行到异步任务时 ,任务不占用当前线程,但是线程的设置机制就是等到任务有结果后再进行执行其他任务
我在打游戏,外卖到了,让舍友拿,但是我好饿,不打了,等外卖
- 异步非堵塞 : 执行到异步任务时 ,任务交给其他线程执行,当前线程继续执行其他任务。
我在打游戏,让舍友去给我拿外卖,我继续打游戏!
显然异步堵塞几乎没什么用 ,而同步非堵塞也不是很符合实际需求或解决大部分的问题。
JavaScript 的执行机制
先体验一波 JavaScript 的同异步任务的执行结果吧!
console.log(1)
setTimeout(() => {
console.log(2)
}, 0);
console.log(3);
// 1 3 2
在了解同异步前,我会认为答案 是 1 2 3
,因为定时器是 0 秒的 。应该会被立即执行的吧 。
但实际不是如此的 。这就得说到一种运行机制叫做事件循环机制。
事件循环 (Event Loop)
事件循环机制是计算机系统的一种运行机制 。而 JavaScript 是单线程的 ,因此采用了这种机制来解决运行中存在的一些问题。
**先粗浅的看看什么是事件循环 **
单线程无法同一时间执行多个事件 ,必须一件一件的完成 。但如果遇到异步的任务,就比方说点击事件 ,
but.onclick = (){
}
当线程执行到这段代码时 给节点添加点击事件后,发现 but 节点的点击还没发生 ,也是就等着。那么页面就会出现 “假死” 状态 ,页面操作不了 ,接下来的代码也不执行了 。
而如果使用事件循环机制 ,把异步任务交给其他线程完成 ,那么当执行完成后有结果了再返回主线程。那么在这段本来应该等待的时间里就可以做其他的事情了 。
那么这于多线程有什么区别呢 ? 输入事件回调也会使用到其他线程 ,但其他线程是相当于辅助作用的。主线程遇到异步了就交给它,其他情况下它并不活跃 ,不会占据太多的资源。
而相比多线程 ,多条线程是同地位的 ,这意味着占用的资源更多 ,并且遇到异步任务需要等待时 ,都没有相互帮忙,而是等着 。这显然不合理。
JavaScript 中的事件循环
先看一张图 :
如何理解呢 ? 我们来看下面一段代码 :
console.log('a') // 函数 1
setTimeout(() => {
// 函数2 , 回调函数3
console.log('b') // 函数4
}, 1000);
console.log('c'); // 函数5
// a c b
过程 : JavaScript 执行线程按照顺序执行 ,执行函数以时 ,把函数1 压入函数调用栈 。输出 a 。执行完毕弹出。
然后执行到函数2 ,也会被压入函数执行栈,但它是计时器 ,它为异步函数 ,会立即弹出,被放入其他线程中处理,也就是异步处理模块那。
执行函数5 ,同函数1一样,执行完后弹出 。输出 c 。
异步处理模块于主执行线程互不影响,异步函数在进入异步处理模块后根据 FIFO (也就是先进先出,先来后到)的顺序执行 ,一秒后执行完成,将回调函数放入任务队列中 。
此时如果函数执行栈有其他函数 ,则任务队列不会被理睬 ,等到函数执行栈空了,就会监听任务队列,如果有任务就会压入执行栈执行。执行完了就会继续监听任务队列,是否有任务需要执行。如果执行到异步任务,则又放到异步处理模块
那这个重复循环的过程就是事件循环 。
任务队列(Event Queue)
任务队列有的也叫(消息队列) ,里面的任务 可分为 宏任务(Macro-task) 和微任务 ( Micro-task ) 。
游览器和 Node.js 在任务队列的不同
游览器和 Node 在宏任务和微任务上是有区别的。主要原因是使用场景不同,提供的 API 也不同 。
游览器 :
macro-task大概包括:
- setTimeout
- setInterval
- I/O
- UI render
micro-task大概包括:
- Promise.then(回调)
- Async/Await(实际就是promise)
- MutationObserver(html5新特性)
**Node.js **
macro-task 大概包括:
- setTimeout
- setInterval
- setImmediate
- I/O
micro-task 大概包括:
- process.nextTick(与普通微任务有区别,在微任务队列执行之前执行)
- Promise().then(回调)等。
ES6 作业队列
ECMAScript 2015 引入了作业队列的概念,Promise 使用了该队列(也在 ES6/ES2015 中引入)。 这种方式会尽快地执行异步函数的结果,而不是放在调用堆栈的末尾
也就是异步任务也需要排队的 ,也就是如果前面有一个宏任务,就必须等前面一个先执行完。微任务也需要等它前面的微任务先执行完才能被执行。
而 作业队列我们可以理解成微队列 ,因为作业队列必须在同步任务执行完后,但它无需等待宏任务 。因为无论在游览器还是 Node.js ,都会先执行完微任务再执行宏任务 。
console.log(1)
setTimeout(() => {
console.log(2); // 宏任务
}, 0);
new Promise((resolve, reject) =>{
console.