Javascript事件循环与任务


任务概述

任务分为宏任务和微任务

  • 宏任务主要包括:JS同步代码(算作一个宏任务)、setTimeout、setInterval、setImmediate、I/O(包括网络请求)、UI 交互等;
  • 微任务主要包括:Promise、process.nextTick(Node.js)、MutaionObserver等

即,宏任务由宿主环境Node或浏览器发起,微任务由JS引擎发起;


队列概述

队列分为宏任务队列微任务队列,程序运行中产生的宏任务会被压入宏任务队列,微任务会被压入微任务队列;

两者的区别是执行时机不同:宏任务队列中的任务在下一次事件循环开始后执行;而微任务队列中的任务在当前事件循环中执行,并且微任务队列执行过程中产生的微任务会继续追加到当前微任务队列中,并在当前事件循环中被执行。

队列遵循先入先出(FIFO)的规则


javascript中的线程

JS设计之初是为了处理浏览器网页交互,由此决定了JS是一门单线程语言;因为多个线程同时操作DOM,会带来很复杂的同步问题。

JS单线程是指负责解释、执行 JS 代码的只有一个线程,即JS引擎线程,但浏览器是提供多个线程的,如下:

1> JS引擎线程:用于解释执行JS代码,与GUI渲染线程互斥;

2> 事件触发线程:用于将异步操作的回调压入任务队列;

3> 定时触发器线程:setIntervalsetTimeout所在的线程,专门用于计时;之所以不使用JS引擎线程计时,是为了防止线程阻塞响应计时准确性;

4> 异步http请求线程:用于执行 XMLHttpRequest 异步请求;

5> GUI渲染线程:用于渲染界面,解析HTML、CSS,构建DOM树和RenderObject树,布局和绘制等;与JS引擎线程互斥,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时被执行。


当JS引擎遇到计时器、事件监听、网络请求时,会直接将它们交给相应线程去处理,而JS引擎线程会继续向后执行,这样便实现了异步非阻塞


事件循环

事件触发线程维护一个任务队列;当异步任务,如:DOM事件监听、网络请求、计时器有了结果时,如:计时器结束、网络请求成功、用户点击DOM等;事件触发线程就会将这些异步任务对应的回调加入到任务队列中等待被执行。

JS引擎线程维护一个执行栈,同步代码被依次加入栈中执行,栈中任务执行完成后,栈为空时(JS引擎线程空闲);事件触发线程会从任务队列取出一个异步回调任务(函数)放入执行栈中供JS引擎立即执行;JS引擎常驻内存,反复等待任务执行的过程即为事件循环。


同步代码是一个宏任务,在程序开始执行时,被压入执行栈中执行;执行过程中遇到微任务或宏任务时,会将其压入对应的任务队列中;当前执行栈执行完毕为空时,开始从微任务队列中取出任务执行,微任务队列执行完毕为空时;开始渲染页面;页面渲染完成后,开始下一个事件循环;当前事件循环中收集到的宏任务,会在下一个事件循环中被依次取出执行。

事件循环既可能是浏览器的主事件循环也可能是一个被 web worker 所驱动的事件循环。为了允许第三方库、框架、polyfills 能使用微任务,Window 暴露了 queueMicrotask() 方法,而 Worker 接口则通过WindowOrWorkerGlobalScope mixin 提供了同名的 queueMicrotask() 方法。


例1:setTimeout之所以可能存在误差,是因为回调函数被加入任务队列后需要等待执行栈中的同步任务执行完成后才能执行;即,执行栈中的任务会阻塞setTimeout的回调执行:

// 计时器中的代码在1000毫秒后被放入任务队列
setTimeout(() => {
  console.log('timer 1 over')
}, 1000)

// 计时器中的代码在0毫秒后被放入任务队列
setTimeout(() => {
  console.log('timer 2 over')
}, 0)

以上两个定时器的回调函数被放入任务队列的时间不同,所以执行顺序和时间会有所区别。


例2:宏任务和微任务都是从当前全局同步代码执行结束时开始运行,即第一个宏任务结束后开始;

setTimeout(() => { // 产生一个宏任务
  console.log("4");
  Promise.resolve().then(() => { // 产生一个微任务
    console.log("5");
  });
});

console.log("1"); 

Promise.resolve().then(() => {  // 产生一个微任务
  console.log("2");
  setTimeout(() => {   // 产生一个宏任务
    console.log("6");
  });
}).then(function () {  // 执行上一个then时,产生一个新的微任务,新的微任务会立即添加到当前微任务队列中等待执行
    console.log("3");
});;

// 同步代码执行完毕

// 打印:1、2、3、4、5、6


例3:事件绑定与异步操作

$btn.on('click', function (e) {
   console.log('你点击了按钮')
})

虽然事件绑定和异步操作的实现机制一样,但它们的调用源不一样。异步操作是在$.ajax异步返回后由系统会自动调用;而事件绑定是由用户手动触发,事件绑定有着明显的“订阅-发布”模式,而异步操作却没有。


任务与异步线程

不论微任务或宏任务,都是用于执行一段代码或函数,它们并不具备异步执行的能力。异步执行是由单独的线程去执行,宏任务或微任务只是负责在合适的时机去执行异步任务的回调而已。

宏任务很早就存在了,而微任务是为了弥补宏任务的实时性而产生的。

在代码中使用 promisesetTimeout等,可以让主线程在等待请求返回结果的同时继续往下执行,从而达到不阻塞主线程的目的;但它们自身并不能异步执行耗时任务。如果遇到耗时任务,推荐使用 web workers 让主线程另起新的线程来运行耗时脚本。一个设计良好的网站或应用应该把复杂或耗时的操作交给 worker 去做,这样可以让主线程除了更新、布局和渲染网页之外,尽可能少的去做其他事情。



参见:https://segmentfault.com/a/1190000037526686

https://juejin.im/post/5c30375851882525ec200027

https://juejin.im/post/5a6ad46ef265da3e513352c8


前端优质文章:https://github.com/ljianshu/Blog



举报

© 著作权归作者所有


1