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

零 JavaScript教程评论81字数 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

代码解读
复制代码
console.log(1);
setTimeout(() => {
    console.log(2);
}, 0);
console.log(3);
//打印的顺序为 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

代码解读
复制代码
setTimeout(function(){
    console.log(1)
},0)
new Promise(function(resolve){
    console.log(2)
    resolve()
}).then(function(){
    console.log(3)
})
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

代码解读
复制代码
console.log('script start');
setTimeout(() => {
    console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
    console.log('promise1');
});
async function a1() {
    console.log('a1 start');
    await a2();
    console.log('a1 end');
}
async function a2() {
    console.log('a2');
}
a1();
let promise2 = new Promise((resolve) => {
    resolve('promise2.then');
    console.log('promise2');
});
promise2.then((res) => {
    console.log(res);
    Promise.resolve().then(() => {
        console.log('promise3');
    });
});
console.log('script end');

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

  1. 执行开始,打印 script start

    javascript

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

    javascript

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

    javascript

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

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

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

    javascript

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

    javascript

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

    javascript

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

    javascript

    代码解读
    复制代码
    let promise2 = new Promise((resolve) => {
        resolve('promise2.then');
        console.log('promise2');
    });
    promise2.then((res) => {
        console.log(res);
        Promise.resolve().then(() => {
            console.log('promise3');
        });
    });
    

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

  8. 执行结束,打印 script end

    javascript

    代码解读
    复制代码
    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
    本社区资源仅供用于学习和交流,请勿用于商业用途
    未经允许不得进行转载/复制/分享

发表评论