深入理解JavaScript的事件执行机制

前言

熟悉事件循环,了解浏览器运行机制将对我们理解 JavaScript 的执行过程和排查运行问题有很大帮助。以下是总结的一些浏览器事件循环的一些原理和示例。

浏览器 JS 异步执行的原理

JS 是单线程的,也就是同一个时刻只能做一件事情,那么,为什么浏览器可以同时执行异步任务呢?

因为浏览器是多线程的,当 JS 需要执行异步任务时,浏览器会另外启动一个线程去执行该任务。

也就是说,JS 是单线程的指的是 执行JS 代码的线程只有一个,是浏览器提供的 JS 引擎线程(主线程)。浏览器中还有定时器线程和 HTTP 请求线程等,这些线程主要不是来跑 JS 代码的。

比如主线程中需要发一个 AJAX 请求,就把这个任务交给另一个浏览器线程(HTTP 请求线程)去真正发送请求,待请求回来了,再将 callback 里需要执行的 JS 回调交给 JS 引擎线程去执行。

即浏览器才是真正执行发送请求这个任务的角色,而 JS 只是负责执行最后的回调处理。所以这里的异步不是 JS 自身实现的,其实是浏览器为其提供的能力。

DA872E71-09D4-9397-1CB5-2CF4F1210A8E.png

浏览器中的事件循环
执行栈与任务队列

JS 在解析一段代码时,会将同步代码按顺序排在某个地方,即执行栈,然后依次执行里面的函数。

遇到异步任务时就交给其他线程处理,待当前执行栈所有同步代码执行完成后,会从一个队列中去取出已完成的异步任务的回调加入执行栈继续执行。

遇到异步任务时又交给其他线程,.....,如此循环往复。

而其他异步任务完成后,将回调放入任务队列中待执行栈来取出执行。

047F243C-DDAE-FA6E-BF0A-BA739A682A5F.png

宏任务和微任务

根据任务的种类不同,可以分为微任务(micro task)队列和宏任务(macro task)队列。

事件循环的过程中,执行栈在同步代码执行完成后,优先检查微任务队列是否有任务需要执行,如果没有,再去宏任务队列检查是否有任务执行,如此往复。

微任务一般在当前循环就会优先执行,而宏任务会等到下一次循环,因此,微任务一般比宏任务先执行,并且微任务队列只有一个,宏任务队列可能有多个。另外我们常见的点击和键盘等事件也属于宏任务。

常见宏任务:

  • setTimeout()
  • setInterval()
  • UI交互事件
  • postMessage
  • setImmediate() -- nodeJs

常见微任务:

  • promise.then()、promise.catch()
  • new MutationObserver()
  • process.nextTick() -- nodeJs

如下示例:

console.log('同步代码1');
setTimeout(() => {
    console.log('setTimeout')
}, )

new Promise((resolve) => {
  console.log('同步代码2')
  resolve()
}).then(() => {
    console.log('promise.then')
})
console.log('同步代码3');

// 最终输出"同步代码1"、"同步代码2"、"同步代码3"、"promise.then"、"setTimeout"

具体分析如下:

  1. setTimeout 回调和 promise.then 都是异步执行的,将在所有同步代码之后执行;

  2. 虽然 promise.then 写在后面,但是执行顺序却比 setTimeout 优先,因为它是微任务;

  3. new Promise 是同步执行的,promise.then 里面的回调才是异步的。

注意: 在浏览器中 setTimeout 的延时设置为 0 的话,会默认为 4ms,NodeJS 为 1ms。

微任务和宏任务的本质区别:

  • 宏任务特征:有明确的异步任务需要执行和回调;需要其他异步线程支持。

  • 微任务特征:没有明确的异步任务需要执行,只有回调;不需要其他异步线程支持。

Async/await的运行顺序
特点
  1. async声明的函数只是把该函数的return包装了,使得无论如何都会返回promise对象(非promise会转化为promise{resolve})。

  2. await声明只能用在async函数中。

    • 当执行async函数时,遇到await声明,会先将await后面的内容按照'平常的执行规则'执行完(此处注意,当执行函数内容中又遇到await声明,此时接着执行该await声明内容)。

    • 执行完后立马跳出async函数,去执行主线程其他内容,等到主线程执行完再回到await处继续执行后面的内容。

示例
const a = async () => {
  console.log("a");
  await b();
  await e();
};

const b = async () => {
  console.log("b start");
  await c();
  console.log("b end");
};

const c = async () => {
  console.log("c start");
  await d();
  console.log("c end");
};

const d = async () => {
  console.log("d");
};

const e = async () => {
  console.log("e");
};

console.log('start');
a();
console.log('end');
运行结果

C3BF93CD-D02B-D0F7-B22C-F8663E7B2EAE.png

个人分析
  1. 在当前同步环境中,先执行console.log('start');输出'start'

  2. 遇到同步函数a(),执行a()

  3. a()是sync/await构造,函数内遇到console.log("a") 输出 'a',遇到await声明 await b(),为异步函数,进入函数b()内执行。(类似的操作,我自己想的)并将 await b()后的内容推入微任务队列中。我们可以记做[await b()后]

  4. b()是sync/await构造,顺序执行遇到console.log("c start") 输出 'c start',遇到await声明 await c(),为异步函数,进入函数c()内执行。并将 await c()后的内容推入微任务队列中。我们可以记做[await c()后, await b()后]

  5. c()是sync/await构造,顺序执行遇到console.log("b start") 输出 'b start',遇到await声明 await d(),为异步函数,进入函数d()内执行。并将 await d()后的内容推入微任务队列中。我们可以记做[await d()后, await c()后, await b()后]

  6. d()中,顺序执行遇到console.log("") 输出 'd'd()函数运行结束。

  7. 这是执行完 d()后,无可执行异步函数,此时进入同步环境,执行 a()后的内容。

  8. 遇到console.log("end") 输出 'end'。此时同步环境中主线程执行完,检查微任务队列是否有微任务。

  9. 微任务队列中[await d()后, await c()后, await b()后]有微任务,执行await d()后的内容。

  10. await d()后的内容是,console.log("c end"), 输出 'c end'。此时内容执行完毕,再从微任务队列中[await c()后, await b()后]检查,执行await c()后的内容。

  11. 执行await c()后的内容,console.log("b end");, 输出 'b end'。此时内容执行完毕,再从微任务队列中[await b()后]检查,执行await b()后的内容。

  12. await d()后的内容是,await e(),遇到await声明,执行e()。并判断并将 await e()后无运行代码,不用入微任务队列。

  13. 执行e(),顺序执行console.log("e");,输出 'e'。此时函数结束。

  14. 微任务队列中[]无微任务,执行结束。进入同步环境。

收藏 (0)
评论列表
正在载入评论列表...
我是有底线的
为您推荐
    暂时没有数据