分享

JS单线程和任务队列

 python_lover 2020-02-21

一、为什么JavaScript必须是单线程

所谓单线程,就是同一个时间只能做一件事。JavaScript从诞生之初就是作为浏览器的一种脚本语言,其主要用途是与用户互动,以及操作DOM,而这就决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容另一个线程删除了这个节点,这个时候浏览器就不知道该如何处理了,到底是应该在节点上添加内容还是应该删除这个节点呢
虽然为了利用CPU的多核计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制且不得操作DOM,所以,这个新标准并没有改变JavaScript单线程的本质

二、任务队列

任务队列是指task queue,由于JavaScript是单线程的,所以所有任务必须进行排队依次进行处理。而任务又分为同步任务异步任务同步任务直接进入主线程中进行排队异步任务则进入任务队列中进行排队,同步任务是在主线程的调用栈中执行的,只有主线程的调用栈被清空的时候才会执行任务队列中的任务,这也就是所说的JavaScript的运行机制

通常异步操作都会进入到任务队列中,比如setTimeout()、setInterval(),这里需要注意的就是浏览器是多线程的,主要为UI渲染线程JS引擎线程GUI线程(主要用于处理事件交互),其中,JS引擎线程和UI渲染线程是互斥的,即,如果JS引擎主线程在执行,那么UI将无法进行渲染,因为JS引擎线程是可以进行DOM操作的,只有互斥才能保证不会出现UI引擎在渲染的同时,JS引擎线程同时在修改DOM,如页面中有一个按钮,点击按钮后会开始一段耗时比较长的计算,这里要求实现点击按钮后按钮文字显示"计算中",计算完成后,按钮文字显示"计算完成"。
<body>
    <button id="btn">点我</button>
</body>
let btn = document.getElementById("btn");
function long_running() {
    console.log("long_running");
    var result = 0;
    for(var i = 0; i < 1000; i++) {
        for(var j= 0; j < 1000; j++) {
            for(var k=0; k< 1000; k++) {
                result = result + i + j + k;
            }
        }
    }
    btn.innerHTML = "计算完成";
}
btn.addEventListener("click", (e) => {
    btn.innerHTML = "计算中...";
    long_running();
});
运行如上代码,我们可以发现点击按钮后并没有先变成"计算中",然后再变成"计算完成",而是点击之后无变化,然后等计算完成后直接变成了"计算完成"。因为btn.innerHTML = "计算中...";是进行DOM操作使用的UI渲染线程,此时,JS引擎线程调用栈还未清空(还需要往下执行js),所以还不能立即执行,然后执行long_running(),long_running()不是异步任务,不进入到任务队列中,直接进入到主线程的调用栈中执行,由于耗时比较长,等long_running()执行完成后,主线程调用栈被清空,UI渲染引擎开始执行,所以直接显示"计算完成"了,要实现上述效果,我们需要给long_running()添加一个延时,让其进入到任务队列中,不要占用主线程调用栈,让btn.innerHTML = "计算中..."先执行,再进行计算。如:
btn.addEventListener("click", (e) => {
    btn.innerHTML = "计算中...";
    setTimeout(() => {
        long_running();
    }, 0);
});
添加延时后,long_running();也进入到了任务队列中,所以会先执行btn.innerHTML = "计算中...";再执行long_running();等计算完成后再更新为"计算完成"。

三、宏认为和微任务

异步任务又分为宏认为微任务。宏任务包括整体代码scriptsetTimeoutsetInterval宏认为进入宏任务队列,并且宏任务队列可以有多个;微任务包Promise的then(回调)process.nextTick微任务进入微任务队列,并且微任务队列只有一个,当宏任务队列的中的任务全部执行完以后,会查看微任务队列中是否有微任务,比如在执行宏任务的时候产生了微任务,那么会先执行微任务队列中的所有微任务,如果微任务队列中没有微任务,那么直接执行下一个宏任务队列,重复执行之前的执行步骤,从而形成事件环。

① 示例1

setTimeout(() => console.log('setTimeout1'), 0);  //1宏任务
setTimeout(() => {                              //2宏任务
    console.log('setTimeout2');
    Promise.resolve().then(() => {
        console.log('promise3');
        Promise.resolve().then(() => {
            console.log('promise4');
        })
        console.log(5)
    })
    setTimeout(() => console.log('setTimeout4'), 0);  //4宏任务
}, 0);
setTimeout(() => console.log('setTimeout3'), 0);  //3宏任务
Promise.resolve().then(() => {//1微任务
    console.log('promise1');
})
首先整体代码首先产生了1、2、3三个宏任务,进入宏任务队列,然后执行到最后一行Promise的时候产生了一个微任务,进入微任务队列,因为整体代码是一个宏任务宏任务结束后会检查微任务队列中是否有任务,发现有一个,所以首先输出promise1,微任务清空后,接着执行下一个宏任务,虽然一下产生了三个宏任务,但是由于时间都是0,所以这三个宏任务其实相当于是一个大的宏任务,可以合在一起,如:
setTimeout(() => {
    console.log('setTimeout1'); // 宏任务1
    
    console.log('setTimeout2');
    Promise.resolve().then(() => {
        console.log('promise3');
        Promise.resolve().then(() => {
            console.log('promise4');
        })
        console.log(5)
    })
    setTimeout(() => console.log('setTimeout4'), 0);
    
    console.log('setTimeout3') // 宏任务3
}, 0);
所以接着执行这个大的宏任务,输出setTimeout1,setTimeout2,setTimeout3,然后执行宏任务的过程中产生了一个微任务和一个宏任务,所以接着执行这个微任务,输出promise3,5,然后执行微任务的过程中又产生了一个微任务,然后继续执行微任务输出setTimeout4,此时微任务清空完毕,执行最后一个宏任务,输出setTimeout4。

②示例2

new Promise((resolve,reject)=>{
    console.log("promise1")
    resolve()
}).then(()=>{
    console.log("then1-1")
    new Promise((resolve,reject)=>{
        console.log("promise2")
        resolve()
    }).then(()=>{
        console.log("then2-1")
    }).then(()=>{
        console.log("then2-2")
    })
}).then(()=>{
    console.log("then1-2")
})
首先执行整体代码,立即输出promise1,然后产生了一个微任务,没有宏任务,接着执行产生的微任务,输出then1-1,promise2,执行微任务的过程中又产生了一个微任务,进入到微任务队列,外层第一个then执行完成,接着执行第二个then又产生了一个微任务,于是添加到微任务队列,此时微任务队列中有两个微任务了,即内层的第一个then,和外层的第二个then,故依次输出then2-1、then1-2,在执行内层第一个then的过程中又产生了一个微任务,继续添加到微任务队列,然后输出then2-2

四、promise.then,process.nextTick, setTimeout 以及 setImmediate的执行顺序

首先promise.then和process.nextTick属于微任务,setTimeout和setImmediate属于宏任务,并且process.nextTick的优先级要高于promise.thensetTimeout的优先级高于setIImmediate
setImmediate(function(){ // 宏任务1
    console.log(1);
},0);
setTimeout(function(){ // 宏任务2
    console.log(2);
},0);
new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){ // 微任务1
    console.log(5);
});
console.log(6);
process.nextTick(function(){ // 微任务2
    console.log(7);
});
console.log(8);
首先执行整体代码,产生了两个宏任务,然后创建Promise执行同步代码输出3和4,此时产生了一个微任务1,接着输出6,然后再产生了一个微任务2,接着输出8,此时整体代码执行完毕,然后检测微任务队列并执行,此时微任务队列中有两个,虽然微任务2后面添加进去,但是微任务2是由process.nextTick创建具有更高优先级,所以先执行微任务2,依次输出7和5,接着再执行宏任务,由于setTimeout比setImmediate具有更高优先级,所以先执行宏任务2,依次输出2和1,故最终结果为3、4、6、8、7、5、2、1。

五、常见示例

① 示例1

console.log(1);
setTimeout(function() { // 宏任务1
    console.log('2');
    process.nextTick(function() { // 微任务3
        console.log('3');
    });
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() { // 微任务4
        console.log('5');
    });
}, 0);

process.nextTick(function() { // 微任务1
 console.log('6');
});

new Promise(function(resolve) {
    console.log('7');
    resolve();
   }).then(function() { // 微任务2
    console.log('8')
   });
setTimeout(() => { // 宏任务2
    console.log('9');
    process.nextTick(function() { // 微任务5
    console.log('10');
    })
    new Promise(function(resolve) {
    console.log('11');
    resolve();
    }).then(function() { // 微任务6
    console.log('12');
    });
}, 0);
首先执行整体代码,输出1,并产生宏任务1,接着产生一个微任务1,接着创建Promise执行同步代码输出7,并产生微任务2,最后产生一个宏任务2;接着情况微任务队列,微任务队列中有两个任务,故依次输出6和8;然后再执行宏任务队列,由于宏任务1和2时间都是0,所以可以看做是一个大的宏任务,先输出2,并产生微任务3,接着创建Promise执行同步代码输出4,然后产生微任务4,继续执行宏任务2,输出9,产生微任务5,接着创建Promise执行同步代码输出11,并产生微任务6,此时宏任务1和2执行完毕,接着需要清空微任务队列,微任务队列中有3、4、5、6,由于process.nextTick优先级高于Promise.then,所以先输出3和10,然后再输出5和12,故最终输出结果为1、7、6、8、2、4、9、11、3、10、5、12

② 示例2

async function async1() {
    console.log('async1 start')
    await async2();
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}
console.log('script start')
setTimeout(() => {
    console.log('setTimeout0')
},0)

setTimeout(() => {
    console.log('setTimeout3')
},3)

setImmediate(() => {
    console.log("setImmediate");
});
async1();
new Promise((resolve) => {
    console.log("promise1");
    resolve();
    console.log("promise2");
}).then(() => {
    console.log("promise3");
});
process.nextTick(() => {
    console.log("nextTick");
});
console.log("scritp end.");
这道题主要考察的是async函数的执行原理,async函数会返回一个Promise对象,当函数执行的时候,一旦遇到await就会立即返回,但是要等到await后的代码执行完成后才能回到主线程,即接着执行函数外的同步代码,函数外的同步代码执行完成后再回到async函数内接着执行,并且之间如果产生了微任务,那么需要先清空微任务
首先执行整体代码,输出script start,然后setTimeout0、setTimeout3、setImmediate进入到宏任务队列,接着执行async1函数,输出async1 start,然后遇到await,async1函数立即返回,但是还要等到await之后的代码async2执行完毕,async2执行完成输出async2,此时回到主线程继续执行,即执行Promise中的同步代码,输出promise1promise2,然后产生一个promise3微任务,接着nextTick也进入到微任务对列,接着输出scritp end,此时主线程执行完毕,即主线程调用栈已经被清空,接着检测是否有微任务队列,发现有,开始执行微任务队列,产生了nextTick和promise3两个微任务,并且nextTick优先级更高,依次输出nextTickpromise3,此时再次回到async1()执行剩余的代码,输出async1 end,接着再执行宏任务队列中的代码,setTimeout0和setImmediate时间都是0,并且setTimeout0优先级更高,依次输出setTimeout0setImmediate,最后输出setTimeout3。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多