你真的了解JavaScript的运行机制吗?

零 JavaScript教程评论155字数 4265阅读14分13秒阅读模式

你真的了解JavaScript的运行机制吗?

JavaScript为何采用单线程设计?

JavaScript最初设计用于浏览器环境,旨在增强网页的交互性。若JavaScript采用多线程模式,尤其是在处理DOM(文档对象模型)时,可能会引发冲突。假设存在两个线程分别尝试修改同一DOM元素,一个正在删除它,而另一个正试图修改其属性,这将导致不可预测的结果。为了规避此类问题,JavaScript选择了单线程执行路径,确保任何时候只有一项任务在执行,简化了内存管理和避免了线程同步带来的复杂性。

为什么要引入异步机制?

尽管单线程设计简化了编程模型,但它也有局限性。如果一个耗时的操作(如网络请求或大型计算)阻塞了主线程,会导致用户界面变得无响应,即所谓的“卡顿”现象。为了解决这一问题,JavaScript引入了异步执行机制,允许某些操作在后台执行,不会阻塞主线程,从而保持应用的响应速度和用户体验。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16962.html

如何在单线程中实现异步?

JavaScript通过——事件循环(Event Loop) 机制实现了异步行为。事件循环监控JavaScript执行栈,消息队列(Event Queue)以及微任务队列,协调它们之间的执行顺序。当主线程上的任务执行完毕,即执行栈为空时,事件循环会检查消息队列中是否有待处理的任务,并将它们依次加入到执行栈中执行。此外,还存在微任务,这些任务具有更高的优先级,会在当前宏任务结束后立即执行,但在此之前所有宏任务代码已经执行完毕。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16962.html

同步与异步任务的区别

为了更好地理解事件循环,我们可以通过示例来观察同步和异步任务的执行顺序:文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16962.html

javascript

代码解读
复制代码
  1. console.log(1);
  2. setTimeout(() => {
  3. console.log(2);
  4. }, 0);
  5. console.log(3);
  6. //打印的顺序为 1 3 2

其中setTimeout需要延迟一段时间才去执行,这类代码就是异步代码。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16962.html

看到这个结果,可以怎么去理解JS的执行原理:文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16962.html

第一,判断JS是同步还是异步,同步进入主线程,异步则进入event table。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16962.html

第二,异步任务在event table中注册函数,当满足触发条件后,被推入event queue(事件队列)。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16962.html

第三,同步任务进入主线程后一直执行,直到主线程空闲,才会去event queue中查看是否有可执行的异步任务,如果有就推入主线程。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16962.html

按照这个逻辑,上面这段实例代码,是不是就很好理解了。1,3是同步任务进入主要线程,自上而下执行,2是异步任务,满足触发条件后,推入事件队列,等待主线程有空时调用。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16962.html

宏任务与微任务的差异

2.宏任务和微任务文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16962.html

然而,按照同步和异步任务来理解JS的运行机制似乎并不准确。

来看一段代码。看看它的输出顺序。

javascript

代码解读
复制代码
  1. setTimeout(function(){
  2.   console.log(1)
  3. },0)
  4. new Promise(function(resolve){
  5.   console.log(2)
  6.   resolve()
  7. }).then(function(){
  8.   console.log(3)
  9. })
  10. console.log(4)

上面这段代码,按同步和异步的理解,输出结果是:2,4,1,3。因为2,4是同步任务,按顺序在主线程自上而下执行,而1,3是异步任务,按顺序在主线程有空后自先而后执行。

可事实输出并不是这个结果,而是这样的:2,4,3,1。为什么呢?来理解一下宏任务和微任务。

宏任务和微任务是事件循环中两种不同类型的异步任务,它们的执行时机和顺序不同:

  • 宏任务:包括script(整个脚本)、setTimeoutsetInterval、I/O、UI事件、postMessageMessageChannelsetImmediate(仅Node.js环境)。宏任务是事件循环的基本单位,它们会在事件队列中排队,等待主线程空闲时执行。
  • 微任务:包括MutationObserverPromiseprocess.nextTick(仅Node.js环境)。微任务具有更高的优先级,会在当前宏任务执行完毕后立即执行,但在下一个宏任务开始前完成。

在事件循环的一个周期内,同步宏任务执行后,会执行所有微任务,然后才开始执行下一个宏任务。

第一,执行一个宏任务(主线程的同步script代码),过程中如果遇到微任务,就将其放到微任务的事件队列里。

第二,当前宏任务(同步的)执行完成后,会查找微任务的事件队列,将全部的微任务依次执行完,再去依次执行宏任务事件队列。

上面代码中promise的then是一微任务,因此它的执行在setTimeout之前。

宏任务和微任务在意义上是有绝对区别的,宏任务便是 JavaScript 与宿主环境产生的回调,需要宿主环境配合处理并且会被放入回调队列的任务都是宏任务。微任务则是语言层面的。

JavaScript执行顺序总结为: 同步宏任务→微任务→异步宏任务

process.nextTick 是一个独立于 eventLoop 的任务队列。

同步宏任务完成后会去检查这个队列,如果里面有任务,会让这部分任务优先于微任务执行。

最最最重要的代码

可以看一下这段代码,我们将分析其执行顺序:

javascript

代码解读
复制代码
  1. console.log('script start');
  2. setTimeout(() => {
  3. console.log('setTimeout');
  4. }, 0);
  5. Promise.resolve().then(() => {
  6. console.log('promise1');
  7. });
  8. async function a1() {
  9. console.log('a1 start');
  10. await a2();
  11. console.log('a1 end');
  12. }
  13. async function a2() {
  14. console.log('a2');
  15. }
  16. a1();
  17. let promise2 = new Promise((resolve) => {
  18. resolve('promise2.then');
  19. console.log('promise2');
  20. });
  21. promise2.then((res) => {
  22. console.log(res);
  23. Promise.resolve().then(() => {
  24. console.log('promise3');
  25. });
  26. });
  27. console.log('script end');

这段代码展示了宏任务、微任务以及异步函数的交互。首先,'script start''script end'作为同步任务将立即执行。setTimeout注册了一个宏任务,将在后续执行。Promise.resolve().thena1()函数的调用将产生微任务,而promise2then回调也将生成微任务。 这段代码包含了同步、异步、宏任务、微任务以及Promise和异步函数的使用。下面是代码的逐行解析及执行流程:

  1. 执行开始,打印 script start

    javascript

    代码解读
    复制代码
    1. console.log('script start');
  2. 注册一个setTimeout宏任务,设定0毫秒后执行,但实际上会被推迟到当前宏任务栈清空后执行。

    javascript

    代码解读
    复制代码
    1. setTimeout(() => {
    2. console.log('setTimeout');
    3. }, 0);
  3. 创建一个Promise并立即解析,然后将then回调添加到微任务队列中。

    javascript

    代码解读
    复制代码
    1. Promise.resolve().then(() => {
    2. console.log('promise1');
    3. });

    这个微任务会在当前宏任务结束后立即执行。

  4. 定义异步函数a1,其中包含对另一个异步函数a2的调用。

    javascript

    代码解读
    复制代码
    1. async function a1() {
    2. console.log('a1 start');
    3. await a2();
    4. console.log('a1 end');
    5. }
  5. 定义异步函数a2,简单地打印a2

    javascript

    代码解读
    复制代码
    1. async function a2() {
    2. console.log('a2');
    3. }
  6. 调用a1函数,开始执行a1,并打印a1 start。由于a1内部有await关键字,a2的执行会被挂起,直到a2完成。

    javascript

    代码解读
    复制代码
    1. a1();
  7. 创建一个新的Promise并立即解析,同时打印promise2。然后,将then回调添加到微任务队列中。

    javascript

    代码解读
    复制代码
    1. let promise2 = new Promise((resolve) => {
    2. resolve('promise2.then');
    3. console.log('promise2');
    4. });
    5. promise2.then((res) => {
    6. console.log(res);
    7. Promise.resolve().then(() => {
    8. console.log('promise3');
    9. });
    10. });

    这个微任务同样会在当前宏任务结束后立即执行。

  8. 执行结束,打印 script end

    javascript

    代码解读
    复制代码
    1. console.log('script end');

执行流程

  • 首先,打印 script start 和 script end,因为它们是同步任务,立即执行。
  • 接着,a1开始执行,打印 a1 start
  • 然后,a2开始执行,打印 a2
  • 由于awaita1 end不会立即执行,而是等待a2完成。
  • 此时,a2已经完成,a1继续执行,打印 a1 end
  • Promise.resolve().thenpromise2.then的回调被添加到微任务队列中,将在当前宏任务结束后立即执行。
  • 微任务开始执行,打印 promise1
  • 然后执行promise2.then,打印 promise2.then 和 promise3

输出顺序

由于微任务的优先级高于宏任务,所以微任务会先于setTimeout宏任务执行。因此,最终的输出顺序应该是:

  1. script start
  2. a1 start
  3. a2
  4. a1 end
  5. script end
  6. promise1
  7. promise2
  8. promise2.then
  9. promise3
  10. setTimeout

注意,setTimeout虽然设定为0毫秒,但实际执行时间取决于事件循环何时能再次轮到它执行,通常是在所有微任务执行完毕之后。

结论

理解JavaScript的单线程性质和事件循环机制对于编写高效、响应迅速的应用至关重要。可以更有效地组织代码,避免阻塞性操作,确保良好的用户体验。觉得文章对你有帮助的话还请给个赞赞?吧!我知道你肯定不会吝惜的?

零
  • 转载请务必保留本文链接:https://www.0s52.com/bcjc/javascriptjc/16962.html
    本社区资源仅供用于学习和交流,请勿用于商业用途
    未经允许不得进行转载/复制/分享

发表评论