浅谈事件循环
什么是事件循环
我们都知道主线程是最繁忙的,既然如此繁忙,那你是否思考过主线程是如何调度任务的?
比如:
- 主线程正在执行一个 js 函数,执行到一半的时候用户点击了按钮,主线程该立即去执行点击事件的处理函数吗?
- 主线程正在执行一个 js 函数,执行到一半的时候某个定时器时间到了,主线程该立即去执行它的回调没?
- 用户点击按钮的同时某个定时器时间到了,又该先处理哪个?
答案是 排队 。
在最开始的时候,渲染主线程会进入无限循环,每一次循环都会检查消息队列中是否有任务存在,如果有就取出第一个任务执行,执行完一个后进入下一次循环;如果没有则进入休眠状态。
其它所有线程可以随时向消息队列添加任务。新任务会加到消息队列的末尾,在添加新任务时,如果主线程是休眠状态,则会将其唤醒继续循环拿取任务。
整个过程被称为事件循环(消息循环)。
什么是异步
代码在执行过程中,会遇到一些无法立即处理的任务,比如:
定时器 setTimeout
网络请求 Ajax
用户的交互事件 addEventListener 。
如果让渲染主线程等待这些任务的时机达到,就会导致主线程长期【阻塞】的状态,从而导致浏览器【卡死】,下面是一个例子👇
1 | setTimeout(function(){ |
在上面的代码中,用 setTimeout 开了一个三秒的定时器,倒计时结束后会输出 1 ,最后一行代码输出 2 。
如果没有异步,主线程会阻塞三秒,在这三秒里,是无法进行其它操作的,如果这时候用户点击了按钮也毫无反应,而是要等倒计时结束后才会接着往后执行。
有了异步之后,当主线程再遇到类似于 setTimeout 这种情况就会采取别的办法,比如遇到 setTimeout 会开一个计时线程把它放到里面去执行,而主线程会处理其它任务,当定时器到时间后,会把 setTimeout 里的函数放到消息队列里排队,依次等待主线程拿取任务执行。
任务有优先级吗?
任务没有优先级,在消息队列中先进先出。
但消息队列是有优先级的。
根据 W3C 的解释:
- 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。
- 浏览器必须准备好一个微队列,微队列中的任务优先所有其它任务执行 https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint
在目前 Chrome 的实现中,至少包含了下面的队列:
- 延时队列:用于存放定时器到达后的回调任务,优先级【中】。
- 交互队列:用于存放用户操作后产生的事件处理任务,优先级【高】。
- 微队列:用户存放需要最快执行的任务,优先级【最高】
添加任务到微队列的方式主要使用 Promise 、MutationObserver ,例如 👇
1 | //立即把一个函数添加到微队列 |
练习
1 | console.log("1"); |
在上面的代码中首先,打印出 “1”,然后,遇到 setTimeout
,会将其回调函数放入延时队列中,并继续执行下面的代码。接着,遇到 Promise.resolve().then()
,会将其回调函数放入微队列中,但由于此时微任务队列优先级高于宏任务队列,所以会先执行微任务队列中的任务。接着执行同步代码打印出 “4”。然后执行微队列中的任务,打印出 “3”。最后,延时队列中的 setTimeout
的回调函数被执行,打印出 “2”。输出结果是 “1432”。
第二题:
1 | console.log("1"); |
首先输出同步代码 “1”,然后把 setTimeout
放到延时队列中,接着遇到 Promise
,但是Promise
的构造函数是同步执行的,所以这时候输出 “3”,再往后执行遇到了 setTimeout
将其放到了延迟队列,然后调用 resolve()
改变 Promise
的状态,接着触发 .then
产生了一个新的Promise
,这时外层的 Promise
是已完成状态,所以会将.then
的回调函数加到微队列,然后输出 “6”。接着查看微队列中的任务,输出微队列中的 “5”,这时微队列是空的,接着取出延时队列的第一个任务输出 “2”,然后拿出第二个任务输出 “4”。输出结果是 “136524”。