JS单线程和任务队列

1、为何JavaScript必须是单线程

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

2、任务队列

任务队列是指 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();等计算完成后再更新为"计算完成"。

3、宏认为和微任务

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

① 示例1promise

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');
})
首先总体代码首先产生了一、二、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,而后执行微任务的过程当中又产生了一个微任务,而后继续执行微任务输出promise4,此时微任务清空完毕,执行最后一个宏任务,输出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-一、then1-2,在执行内层第一个then的过程当中又产生了一个微任务,继续添加到微任务队列,而后输出then2-2

4、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,故最终结果为三、四、六、八、七、五、二、1。

5、常见示例

① 示例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执行完毕,接着须要清空微任务队列,微任务队列中有三、四、五、6,因为process.nextTick优先级高于Promise.then,因此先输出3和10,而后再输出5和12,故最终输出结果为一、七、六、八、二、四、九、十一、三、十、五、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、setTimeout三、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。
相关文章
相关标签/搜索