前言
掌握浏览器的EventLoop的知识有助于我们了解浏览器的渲染进程是如何工作的,并且我们以后写代码时 知其然亦能知其所以然。
浏览器是如何工作的
我们知道页面的工作是依靠事件循环机制的,那事件循环机制是怎么工作的呢,接下来我们一步一步的进行分析!
我们知道浏览器是一个任务一个任务去执行的,也就是单线程。任务依次执行这很容易理解,但是我们知道在我们操作页面的时候,比如说鼠标的点击事件、键盘的操作事件,这些事件是不固定和不定时的,那浏览器又怎么保证这些时间的实时性和有序性呢。
在这里浏览器是这样子实现的,首先用队列的数据结构来存储任务,队列的结构是先进先出的,能够很好的和浏览器事件搭配使用,我们把新发生的事件推入任务队列的尾部。比如说我们首先用鼠标点击了一个input框,然后我们在input框中输入信息。这时总共有两个事件发生,如下图:
使用了队列这种数据结构后,可以保证事件的顺序的有序性
实时性
那浏览器难道知道我在什么时候会触发一些事件吗?他怎么执行我触发的事件的呢?
浏览器实现这些的基础在于它会不停的轮询去获取队列中的任务来执行,其实就是相当于一个for循环,每次循环都会从队列中获取队头的任务去执行,当执行完这个任务后再次重复上面的步骤,直到任务队列中不存在任务了。如下图所示:
宏任务和微任务
浏览器把任务分为了宏任务和微任务,下面来讲一下这两种任务,首先是宏任务。
宏任务
宏任务常见的有:
① html的解析事件
② 用户的交互事件,就如同上面的点击事件和键盘输入事件
③ js代码的执行
④ 一些Web Api事件,比如说setTimeout、setInterval等。
普通宏任务的执行流程就像上面说的一样,推入队列的队尾中,依次执行该事件,但是对于像setTimeout这种延时执行的api就不会不同的处理方式。
定时器在eventloop的流程
如果我们像普通宏任务一样把定时器的回调函数推入任务队列中,那么如果排在该定时器前的任务有很多,执行时间超过了定时器设置的执行时间,那么就不能做到 定时 这一特点。所以浏览器特地创建了另外一个专门存放定时器这种宏任务的队列-- 延时队列。
浏览器的主线程会把定时器的id、回调函数、发起时间、延时时间 都推入延时队列中,当事件循环系统在执行一个宏任务时,在执行完该宏任务的时候,就回去检查延时队列中是否有任务已经到期执行,有的话则会执行该任务。为什么总是常说不要用setTimeout去执行一些精度要求比较高的事件,比如说用setTimeout去实现动画,这是因为如果当前任务执行时间过长,那么定时器设置的到期时间就会和预期时间不匹配,造成一些卡顿。
微任务
微任务主要有,MutationObserver、Promise.then() 等事件。微任务是用来解决异步回调的问题的,每一个宏任务都会携带一个微任务队列,它会在当前宏任务快要执行完将要退出该宏任务的时候去检查微任务队列中是否有任务,有的话则会执行微任务,反之则进入下一个循环。
代码演练
console.log(1);
Promise.resolve().then((res) => {
console.log(2);
});
setTimeout(() => {
Promise.resolve().then((res) => {
console.log(3);
});
console.log(4);
}, 0);
console.log(5);
js代码属于宏任务,在当前宏任务下,我们看到了Promise.then()所以我们把它放入该宏任务的微任务队列中,然后遇到了setTimeout定时器,我们会把它放入延时队列中,因为setTimeout也属于宏任务,所以它也会有自己的微任务队列,我们把setTimeout回调函数中的Promise.then()放入它的微任务队列中。然后我们来开始分析:
① 把当前宏任务的js代码执行,首先打印 1,然后会打印 5
② 宏任务中的代码执行完后会去查找它的微任务队列中是否有任务,在这里检查到了一个任务,执行它会输出 2
③ 当前的宏任务已经执行完了,就回去查看延时队列中是否有到期的任务,在这里存在到期的任务,就回去执行它,输出 4
④ 执行完代码后检查微任务队列中是否有任务,在这里有一个Promise.then()的任务,执行输出3
最后打输出结果为:1 5 2 4 3
总结
希望自己的梳理能给各位看官带来一点作用,如果讲的不好,请多多包含,感谢^.^