暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

JS事件循环机制

原创 Orange 2022-05-05
419

一、关于javascript

1.单线程

众所周知,JavaScript 是单线程的,所谓单线程,就是指程序只能一段一段的执行,一次只能完成一个任务,如果有多个任务就必须要排队,前面的一个任务完成了,再执行后面的任务,以此类推。
需要注意的是 JavaScript 只在一个线程上运行,不代表浏览器内核只有一个线程,事实上浏览器内部有多个线程,主线程用于 JavaScript 代码的编译和执行,其它线程都是在后台配合主线程。
JavaScript 之所以选择单线程,跟历史有关系。JavaScript 从诞生起就是单线程,原因是不想让浏览器变得太复杂,多线程需要面临锁、状态同步等问题,这对于一种网页脚本语言来说开销太大。如果 JavaScript 同时有两个线程,一个线程在网页 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,JavaScript 一开始就是单线程,这已经成了这门语言的核心特征

2.同步和异步

既然js是单线程,那就像只有一个窗口的银行,客户需要排队一个一个办理业务,同理js任务也要一个一个按顺序执行。如果一个任务耗时过长,那么后一个任务也必须等着。那么问题来了,假如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来?因此javascript将任务分为两类:
● 同步任务:就是上面说的排队等待的形式。
● 异步任务:异步操作发生在未知或不可预测的时间,是指在执行一个任务的时候不能立即返回结果,而是在将来通过一定途径得到,后一个任务不用等前一个任务结束就执行。
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。
那么 JavaScript 是如何来执行异步任务的呢,就是后面要说的事件循环机制。

3.栈

栈 是一种 LIFO(Last In, First Out)的数据结构,特点即 后进先出。
类似我们吃过的薯片,薯片在包装时只能从顶部放出,拿出来的时候也只能从顶部拿出。

4.队列

队列 是一种 FIFO(First In, First Out) 的数据结构,它的特点就是 先进先出
生活中最常见的例子就是排队,排在队伍最前面的人最先被提供服务。

5.Event Table

Event Table 可以理解成一张 事件->回调函数 对应表
它就是用来存储 JavaScript 中的异步事件 (request, setTimeout, IO等) 及其对应的回调函数的列表

6.Event Queue

Event Queue 简单理解就是回调函数队列,所以它也叫Callback Queue
当Event Table中的事件被触发,事件对应的回调函数就会被 push进这个Event Queue,然后等待被执行

二、javascript事件循环

1646890392787bb2ecb12c7804d52a36b33df58ecea63.png

用文字来表述的话:
● 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
● 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
● 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
● 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
我们不禁会问,那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。
说了这么多文字,不如直接一段代码更直白:

let data = []; $.ajax({ url:www.javascript.com, data:data, success:() => { console.log('发送成功!'); } }) console.log('代码执行结束');

上面是一段简易的ajax请求代码:
● ajax进入Event Table,注册回调函数success。
● 执行console.log(‘代码执行结束’)。
● ajax事件完成,回调函数success进入Event Queue。
● 主线程从Event Queue读取回调函数success并执行。
相信通过上面的文字和代码,你已经对js的执行顺序有了初步了解。接下来我们来研究进阶话题:setTimeout。

三、setTimeout

setTimeout是浏览器最常见的异步操作方式之一,对它第一印象就是异步可以延时执行,我们经常这么实现延时3秒执行:

setTimeout(() => { console.log('延时3秒'); },3000)

我们还经常遇到setTimeout(fn,0)这样的代码,0秒后执行又是什么意思呢?是不是可以立即执行呢?

//代码1 console.log('1'); setTimeout(() => { console.log('2') },0); console.log('3')

答案是不会的,事件循环必须等栈清空,才能将队列中的任务压入栈中,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。
//1
//3
//2
//代码的输出顺序仍为1,3,2
定时任务可能不能按时执行

const test = () => { let t = +new Date() while (true) { if (+new Date() - t >= 5000) { break } } } setTimeout(() => { console.log(2) }, 2000) test() // 等到5秒钟后才打印出了2

因为 test 是会耗时 5 秒钟的同步任务,异步任务只能等待同步任务执行完之后才能执行,也就是说只能等 5 秒钟后才能检查的任务队列里的任务。

上述的流程走完,我们知道setTimeout这个函数,是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于设定的时间。

定时器最小间隔
关于setTimeout要补充的是,即便主线程为空,0毫秒实际上也是达不到的。
根据HTML的标准,最低是4毫秒。

四、setInterval

上面说完了setTimeout,接下来说说setInterval。他俩差不多,只不过后者是循环的执行。对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。
唯一需要注意的一点是,对于setInterval(fn,ms)来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。

五、Promise与process.nextTick(callback)

传统的定时器我们已经研究过了,接着我们探究Promise与process.nextTick(callback)的表现。Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。
而process.nextTick(callback)类似node.js版的"setTimeout",在事件循环的下一次循环中调用 callback 回调函数。

六、宏任务和微任务

除了广义的同步任务和异步任务,我们对异步任务有更精细的定义:
● macro-task(宏任务):包括整体代码script,setTimeout,setInterval
● micro-task(微任务):Promise,process.nextTick

不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。

1644907217036620aa8aa97704b149513fd2c9ba22980.png
事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。听起来有点绕,我们用一段代码说明:

setTimeout(function() { console.log('setTimeout'); }) new Promise(function(resolve) { console.log('promise'); }).then(function() { console.log('then'); }) console.log('console');

● 这段代码作为宏任务,进入主线程。
● 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)
● 接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。
● 遇到console.log(),立即执行。
● 好啦,整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。
● ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
● 结束

事件循环,宏任务,微任务的关系如图所示:

1646890230936a3ab4eddd0de4905b00abe03151aca68.png

练习:以下代码的输出结果

console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') }) }) process.nextTick(function() { console.log('6'); }) new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { console.log('8') }) setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) })

列表分析:

第一轮
I/O nextTick1 1,7,6,8
setTimeout1 then1
setTimeout2
第二轮
setTimeout1 nextTick1 2,4,3,5
then2
第三轮
setTimeout2 nextTick3 9,11,10,12
then3

所以最后的输出结果是1,7,6,8,2,4,3,5,9,11,10,12

最后用一张图表示完整的事件循环执行机制
1647489869595f823c7183cc04eeda00de495cb5fc516.png

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论