麦克阿瑟曾经说过:理解了Event Loop就理解了前端

前端不能失去事件循环机制,就好像西方不能失去耶路撒冷

------麦克阿瑟

Event Loop

地球人都知道Javascript语言的一大特点就是单线程,这意味这它同一时间只能干一件事,特别的专一。而单线程就意味着所有任务都得排队来,前一个任务干完了才能执行下一个任务。

后来设计者发现,假如上个任务是个响应速度很慢的Ajax请求,那下一个任务不得干等着完犊子了嘛。于是乎,所有任务被分为了同步任务和异步任务。 具体来说,js的执行机制如下:

  1. 所有同步任务在主线程上执行
  2. 主线程之外,还存在个任务队列,异步任务有了运行结果,就会在任务队列中放置一个事件
  3. 执行栈中所有的同步任务执行完毕,就会从任务队列中取事件进入执行栈开始执行
  4. 主线程不断重复第三步

主线程从任务队列中读取事件的过程是不断循环的,所以整个运行机制又称为Event Loop

微任务与宏任务

假如js按照真正的单线程执行的话,一旦遇到任务繁多的情况,势必执行效率会下降。为了解决这个问题,就需要利用微任务和宏任务来模拟"多线程"提高执行效率。通常我们把由宿主发起的任务称为宏任务,由JavaScript引擎发起的任务称为微任务。

  • 常见宏任务:setTimeout(),setInterval(),setImmediate()
  • 常见微任务:Promise().then(function(){}),new MutationObserver(),process.nextTick()

需要注意的是Promise()这个构造函数属于同步任务,而.then()里面的函数才会被推入到微任务队列里。

下面看一段代码:

js 复制代码
setTimeout(function () {
  console.log(1);
});

new Promise(function (resolve, reject) {
  console.log(2);
  resolve(3);
}).then(function (val) {
  console.log(val);
  console.log(4);
});

//2 3 4 1

按照JS的执行顺序是先执行主线程上的同步任务,当同步任务执行完毕,执行栈清空后,再去微任务队列里取微任务执行,当执行栈为空后,我们再去宏任务队列中取宏任务执行。

那么我们从上往下看,首先setTimeout是个宏任务,那么我们把它推入到宏任务队列里,下面Promise的构造函数则属于同步任务,那么我们在控制台上打印出个2,resolve(3)我们会传递给then()方法里的回调,而.then()里面的函数属于微任务,则把其推入到微任务队列里,则得出2 3 4 1的结果。

总结一下事件循环闭环流程

当主线程同步代码执行完毕,执行栈为空时,执行以下步骤

  1. 执行微任务队列
    a.选择微任务队列中最早的任务(任务X)
    b.如果微任务队列为空,则跳转到步骤g
    c.将当前正在运行的任务设置为任务X
    d.运行任务X
    e.将当前正在运行的任务设置为空,删除任务X
    f.选择微任务队列中下一个最早的任务,跳转到步骤b
    g.完成微任务队列
  2. 选择宏任务队列中最早的任务(任务Y)
  3. 将当前正在运行的任务设置为任务Y
  4. 运行任务Y,先执行其中的同步代码
  5. 跳转到步骤1
  6. 将当前正在运行的任务设置为空,删除任务Y结束本次Loop循环
  7. 跳转到步骤2

我们把上面的代码改造一下再看看打印结果:

js 复制代码
setTimeout(function () {
  console.log(1);
});

new Promise(function (resolve, reject) {
  console.log(2);
  resolve(3);
}).then(function (val) {
  console.log(val);
  console.log(4);
  setTimeout(() => {
    console.log(5);
  });
});

//2 3 4 1 5

我们在.then()的回调函数中塞入了一个setTimeout宏任务,那么在本次事件循环中,我们会把该setTimeout放入宏任务队列中去,但是它的执行时机并不是本次事件循环,而是会把这个回调放在下一轮的事件回调里,也就是说该任务会被放在宏任务队列里等待执行,而放置的位置取决于它前面还有没有宏任务在排队。

那么再看回代码,我们在加载js代码的时候就会遇到打印1的setTimeout事件被挂起推入了宏任务队列里,所以在宏任务队列里1是排在第一位的,最后的打印结果就是2 3 4 1 5。

为什么要设计微任务

总的来说,微任务解决了宏任务执行时机不可控的问题。

假设我们只有主线程和宏任务,那么当主线程上的同步代码执行完成之后,就会去宏任务队列里去取事件放到主线程的执行栈中去执行,此时宏任务A要执行多久我们就没办法控制,假如宏任务B中有一个特别紧急的操作,要进行UI的渲染的话就没法及时操作,只能在这里干等着,听天由命。而引入微任务后就会发生改变,我们可以把这些特别紧急的操作放到微任务队列里去。

再来试试

js 复制代码
console.log("1");

//setTimeout1
setTimeout(function () {
  console.log("2");
  new Promise(function (resolve) {
    console.log("3");
    resolve();
  }).then(function () {
    console.log("4");
  });
  //setTimeout2
  setTimeout(function () {
    console.log("5");
    new Promise(function (resolve) {
      console.log("6");
      resolve();
    }).then(function () {
      console.log("7");
    });
  });
  console.log("14");
});

new Promise(function (resolve) {
  console.log("8");
  resolve();
}).then(function () {
  console.log("9");
});

//setTimeout3
setTimeout(function () {
  console.log("10");
  new Promise(function (resolve) {
    console.log("11");
    resolve();
  }).then(function () {
    console.log("12");
  });
});

console.log("13");

首先主线程上的同步代码执行顺序为1 8 13,把9塞入微任务队列里去,等主线程的执行栈空了执行9,将setTimeout1和setTimeout3塞入宏任务队列里去;先执行setTimeout1,从上到下执行2 3,遇到4塞入微任务队列里去,下面的setTimeout2塞入宏任务队列里去,执行14,这时候把微任务队列里的4放到执行栈里执行;下面再执行宏任务队列里的setTimeout3,打印出10 11后将12塞入微任务队列里等待执行,执行栈为空后打印12,最后宏任务队列里还有setTimeout2等待执行,打印结果为5 6 7。所以最终打印结果是1 8 13 9 2 3 14 4 10 11 12 5 6 7。

相关推荐
Hilaku1 分钟前
为什么业务型前端容易遭遇中年危机?
前端·javascript·程序员
এ慕ོ冬℘゜10 分钟前
深度解析 JavaScript:赋予 Web 灵魂的“全栈语言”
开发语言·前端·javascript
iCOD3R12 分钟前
Skill - 3秒生成精美GitHub主页
前端·程序员·ai编程
Mr_凌宇14 分钟前
# AI 应用工程入门学习笔记 一
人工智能·面试·程序员
爱勇宝18 分钟前
《道德经》第5章:生产环境不相信眼泪
前端·后端·架构
掘金者阿豪23 分钟前
LEFT JOIN 凭空消失的背后,是优化器偷偷帮你做了决定
前端·后端
HackTwoHub31 分钟前
AntiDebug Mcp、AI逆向绕过前端,Hook 加密接口,抓取 Vue 路由漏洞,接入 MCP 让 AI 自动化挖掘前端漏洞
前端·vue.js·人工智能·安全·web安全·网络安全·系统安全
Mr_凌宇33 分钟前
AI 应用工程进阶扩展学习笔记 二
前端·面试
Hyyy1 小时前
Git Worktree 完全讲解
前端·后端·面试
铁皮饭盒1 小时前
Bun 处理 IO 密集型比 Java 强多少?
javascript