关于javascript:一次搞懂JS事件循环之宏任务和微任务

35次阅读

共计 4172 个字符,预计需要花费 11 分钟才能阅读完成。

家喻户晓,JS 是一门单线程语言,可是浏览器又能很好的解决异步申请,那么到底是为什么呢?

JS 的执行环境个别是浏览器和 Node.js,两者稍有不同,这里只探讨浏览器环境下的状况。

JS 执行过程中会产生两种工作,别离是:同步工作和异步工作。

  • 同步工作:比方申明语句、for、赋值等,读取后根据从上到下从左到右,立刻执行。
  • 异步工作:比方 ajax 网络申请,setTimeout 定时函数等都属于异步工作。异步工作会通过工作队列 (Event Queue) 的机制(先进先出的机制)来进行协调。

工作队列(Event Queue)

工作队列中的工作也分为两种,别离是:宏工作(Macro-take)和微工作(Micro-take)

  • 宏工作次要包含:scrip(JS 整体代码)、setTimeout、setInterval、setImmediate、I/O、UI 交互
  • 微工作次要包含:Promise(重点关注)、process.nextTick(Node.js)、MutaionObserver

工作队列的执行过程是:先执行一个宏工作 ,执行过程中如果产出新的宏 / 微工作,就将他们推入相应的工作队列, 之后在执行一队微工作 ,之后再执行宏工作,如此循环。 以上一直反复的过程就叫做 Event Loop(事件循环)

每一次的循环操作被称为tick

了解微工作和宏工作的执行执行过程

console.log("script start");

setTimeout(function () {console.log("setTimeout");
}, 0);

Promise.resolve()
  .then(function () {console.log("promise1");
  })
  .then(function () {console.log("promise2");
  });

console.log("script end");

依照下面的内容,剖析执行步骤:

  1. 宏工作:执行整体代码(相当于 <script> 中的代码):

    1. 输入: script start
    2. 遇到 setTimeout,退出宏工作队列,以后宏工作队列(setTimeout)
    3. 遇到 promise,退出微工作,以后微工作队列(promise1)
    4. 输入:script end
  2. 微工作:执行微工作队列(promise1)

    1. 输入:promise1,then 之后产生一个微工作,退出微工作队列,以后微工作队列(promise2)
    2. 执行 then,输入promise2
  3. 执行渲染操作,更新界面(敲黑板划重点)。
  4. 宏工作:执行 setTimeout

    1. 输入:setTimeout

Promise 的执行

new Promise(..)中的代码,也是同步代码,会立刻执行。只有 then 之后的代码,才是异步执行的代码,是一个微工作。

console.log("script start");

setTimeout(function () {console.log("timeout1");
}, 10);

new Promise((resolve) => {console.log("promise1");
  resolve();
  setTimeout(() => console.log("timeout2"), 10);
}).then(function () {console.log("then1");
});

console.log("script end");

步骤解析:

  • 当前任务队列:微工作: [], 宏工作:[<script>]
  1. 宏工作:

    1. 输入: script start
    2. 遇到 timeout1,退出宏工作
    3. 遇到 Promise,输入promise1,间接 resolve,将 then 退出微工作,遇到 timeout2,退出宏工作。
    4. 输入script end
    5. 宏工作第一个执行完结
  • 当前任务队列:微工作[then1],宏工作[timeou1, timeout2]
  1. 微工作:

    1. 执行 then1,输入then1
    2. 微工作队列清空
  • 当前任务队列:微工作[],宏工作[timeou1, timeout2]
  1. 宏工作:

    1. 输入timeout1
    2. 输入timeout2
  • 当前任务队列:微工作[],宏工作[timeou2]
  1. 微工作:

    1. 为空跳过
  • 当前任务队列:微工作[],宏工作[timeou2]
  1. 宏工作:

    1. 输入timeout2

async/await 的执行

async 和 await 其实就是 Generator 和 Promise 的语法糖。

async 函数和一般 函数没有什么不同,他只是示意这个函数里有异步操作的办法,并返回一个 Promise 对象

翻译过去其实就是:

// async/await 写法
async function async1() {console.log("async1 start");
  await async2();
  console.log("async1 end");
}
// Promise 写法
async function async1() {console.log("async1 start");
  Promise.resolve(async2()).then(() => console.log("async1 end"));
}

看例子:

async function async1() {console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {console.log("async2");
}
async1();
setTimeout(() => {console.log("timeout");
}, 0);
new Promise(function (resolve) {console.log("promise1");
  resolve();}).then(function () {console.log("promise2");
});
console.log("script end");

步骤解析:

  • 当前任务队列:宏工作:[<script>],微工作: []
  1. 宏工作:

    1. 输入:async1 start
    2. 遇到 async2,输入:async2,并将 then(async1 end)退出微工作
    3. 遇到 setTimeout,退出宏工作。
    4. 遇到 Promise,输入:promise1,间接 resolve,将 then(promise2)退出微工作
    5. 输入:script end
  • 当前任务队列:微工作[promise2, async1 end],宏工作[timeout]
  1. 微工作:

    1. 输入:promise2
    2. promise2 出队
    3. 输入:async1 end
    4. async1 end 出队
    5. 微工作队列清空
  • 当前任务队列:微工作[],宏工作[timeout]
  1. 宏工作:

    1. 输入:timeout
    2. timeout 出队,宏工作清空

“ 工作队列 ” 是一个事件的队列(也能够了解成音讯的队列),IO 设施实现一项工作,就在 ” 工作队列 ” 中增加一个事件,示意相干的异步工作能够进入 ” 执行栈 ” 了。主线程读取 ” 工作队列 ”,就是读取外面有哪些事件。

“ 工作队列 ” 中的事件,除了 IO 设施的事件以外,还包含一些用户产生的事件(比方鼠标点击、页面滚动等等)。只有指定过回调函数,这些事件产生时就会进入 ” 工作队列 ”,期待主线程读取。

所谓 ” 回调函数 ”(callback),就是那些会被主线程挂起来的代码。异步工作必须指定回调函数,当主线程开始执行异步工作,就是执行对应的回调函数。

“ 工作队列 ” 是一个先进先出的数据结构,排在后面的事件,优先被主线程读取。主线程的读取过程基本上是主动的,只有执行栈一清空,” 工作队列 ” 上第一位的事件就主动进入主线程。然而,因为存在后文提到的 ” 定时器 ” 性能,主线程首先要检查一下执行工夫,某些事件只有到了规定的工夫,能力返回主线程。

—-JavaScript 中没有任何代码时立刻执行的,都是过程闲暇时尽快执行

setTimerout 并不精确

由上咱们曾经晓得了 setTimeout 是一个宏工作,会被增加到宏工作队列当中去,按程序执行,如果后面有。

setTimeout() 的第二个参数是为了通知 JavaScript 再过多长时间把当前任务增加到队列中。

如果队列是空的,那么增加的代码会立刻执行;如果队列不是空的,那么它就要等后面的代码执行完了当前再执行。

看代码:

const s = new Date().getSeconds();
console.log("script start");
new Promise((resolve) => {console.log("promise");
  resolve();}).then(() => {console.log("then1");
  while (true) {if (new Date().getSeconds() - s >= 4) {console.log("while");
      break;
    }
  }
});
setTimeout(() => {console.log("timeout");
}, 2000);
console.log("script end");

因为 then 是一个微工作,会先于 setTimeout 执行,所以,尽管 setTimeout 是在两秒后退出的宏工作,然而因为 then 中的在 while 操作被提早了 4s,所以始终推延到了 4s 秒后才执行的 setTimeout。

所以输入的程序是:script start、promise、script end、then1。
四秒后输入:while、timeout

留神:对于 setTimeout 要补充的是,即使主线程为空,0 毫秒实际上也是达不到的。依据 HTML 的规范,最低是 4 毫秒。有趣味的同学能够自行理解。

<!– ### 异步渲染策略 –>
<!– 以 Vue 为例 nextTick –>

总结

有个小 tip:从标准来看,microtask 优先于 task 执行,所以如果有须要优先执行的逻辑,放入 microtask 队列会比 task 更早的被执行。

最初的最初,记住,JavaScript 是一门单线程语言,异步操作都是放到事件循环队列外面,期待主执行栈来执行的,并没有专门的异步执行线程。

参考

  • 知乎 -【JS】深刻了解事件循环, 这一篇就够了!(必看)
  • 掘金小册 - 前端性能优化 -Event Loop 与异步更新策略
  • Segmentfault- 译文:JS 事件循环机制(event loop)之宏工作、微工作
  • 这一次,彻底弄懂 JavaScript 执行机制
  • 面试肯定会问到的 -js 事件循环

正文完
 0