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
(整个脚本)、setTimeout
、setInterval
、I/O、UI事件、postMessage
、MessageChannel
、setImmediate
(仅Node.js环境)。宏任务是事件循环的基本单位,它们会在事件队列中排队,等待主线程空闲时执行。 - 微任务:包括
MutationObserver
、Promise
、process.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().then
和a1()
函数的调用将产生微任务,而promise2
的then
回调也将生成微任务。 这段代码包含了同步、异步、宏任务、微任务以及Promise和异步函数的使用。下面是代码的逐行解析及执行流程:
- 执行开始,打印
script start
。javascript
代码解读复制代码console.log('script start');
- 注册一个
setTimeout
宏任务,设定0毫秒后执行,但实际上会被推迟到当前宏任务栈清空后执行。javascript
代码解读复制代码setTimeout(() => { console.log('setTimeout'); }, 0);
- 创建一个Promise并立即解析,然后将
then
回调添加到微任务队列中。javascript
代码解读复制代码Promise.resolve().then(() => { console.log('promise1'); });
这个微任务会在当前宏任务结束后立即执行。
- 定义异步函数
a1
,其中包含对另一个异步函数a2
的调用。javascript
代码解读复制代码async function a1() { console.log('a1 start'); await a2(); console.log('a1 end'); }
- 定义异步函数
a2
,简单地打印a2
。javascript
代码解读复制代码async function a2() { console.log('a2'); }
- 调用
a1
函数,开始执行a1
,并打印a1 start
。由于a1
内部有await
关键字,a2
的执行会被挂起,直到a2
完成。javascript
代码解读复制代码a1();
- 创建一个新的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'); }); });
这个微任务同样会在当前宏任务结束后立即执行。
- 执行结束,打印
script end
。javascript
代码解读复制代码console.log('script end');
执行流程
- 首先,打印
script start
和script end
,因为它们是同步任务,立即执行。 - 接着,
a1
开始执行,打印a1 start
。 - 然后,
a2
开始执行,打印a2
。 - 由于
await
,a1 end
不会立即执行,而是等待a2
完成。 - 此时,
a2
已经完成,a1
继续执行,打印a1 end
。 Promise.resolve().then
和promise2.then
的回调被添加到微任务队列中,将在当前宏任务结束后立即执行。- 微任务开始执行,打印
promise1
。 - 然后执行
promise2.then
,打印promise2.then
和promise3
。
输出顺序
由于微任务的优先级高于宏任务,所以微任务会先于setTimeout
宏任务执行。因此,最终的输出顺序应该是:
script start
a1 start
a2
a1 end
script end
promise1
promise2
promise2.then
promise3
setTimeout
注意,setTimeout
虽然设定为0毫秒,但实际执行时间取决于事件循环何时能再次轮到它执行,通常是在所有微任务执行完毕之后。
结论
理解JavaScript的单线程性质和事件循环机制对于编写高效、响应迅速的应用至关重要。可以更有效地组织代码,避免阻塞性操作,确保良好的用户体验。觉得文章对你有帮助的话还请给个赞赞?吧!我知道你肯定不会吝惜的?
评论