1

JavaScript单线程起源:

JavaScript作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM,为了避免复杂性,诞生开始,JavaScript就是单线程语言。
比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程。

单线程及存在的问题:

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

问题的解决--同步、异步

synchronous(同步任务)和asynchronousk(异步任务)

  • 同步任务是调用立即得到结果的任务,同步任务在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
  • 异步任务是调用无法立即得到结果的任务,需要额外的操作才能预期结果的任务,异步任务不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

macro-task(宏任务)和micro-task(微任务)

  • 宏任务:macro-task 可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行,每一个宏任务会从头到尾将这个任务执行完毕,不会执行其它)包括整体代码script,setTimeout,setInterval等
    image.png
  • 微任务:micro-task可以理解是在当前 task 执行结束后立即执行的任务 包括Promise,process.nextTick等
    image.png

    Event Loop事件循环

    Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,其实宏任务队列和微任务队列的执行,就是事件循环的一部分。
    事件循环的具体流程如下:

  1. 从宏任务队列中,按照入队顺序,找到第一个执行的宏任务,放入调用栈,开始执行;
  2. 执行完该宏任务下所有同步任务后,即调用栈清空后,该宏任务被推出宏任务队列,然后微任务队列开始按照入队顺序,依次执行其中的微任务,直至微任务队列清空为止;
  3. 当微任务队列清空后,一个事件循环结束;
  4. 接着从宏任务队列中,找到下一个执行的宏任务,开始第二个事件循环,直至宏任务队列清空为止。

Js执行机制流程图

Js执行机制流程图
注意:

  • 当我们第一次执行的时候,解释器会将整体代码script放入宏任务队列中,因此事件循环是从第一个宏任务开始的;因此我在流程图中将主线程单独分离开。
  • 如果在执行微任务的过程中,产生新的微任务添加到微任务队列中,也需要一起清空;微任务队列没清空之前,是不会执行下一个宏任务的。

流程图解释:

  1. 在Js任务入栈后判断是否为同步任务,同步任务则立即执行;若为异步任务则判断微任务或宏任务加入相应队列等待处理。
  2. 在所有同步进程执行结束,立即执行当前微任务队列中的全部微任务(因为整体的Js代码视为一个宏任务)
  3. 当微队列清空后,执行宏任务队列中的下一个宏任务,执行结束后,判断微任务队列是否存在微任务,存在的话,则清空微任务队列,开启下一个循环。

Event Loop练习

console.log("a");

setTimeout(function () {
    console.log("b");
}, 0);

new Promise((resolve) => {
    console.log("c");
    resolve();
})
    .then(function () {
        console.log("d");
    })
    .then(function () {
        console.log("e");
    });

console.log("f");

/**
* 输出结果:a c f d e b
*/
async function async1() {
    console.log("a");
    const res = await async2();
    console.log("b");
}
async function async2() {
    console.log("c");
    return 2;
}
console.log("d");
setTimeout(() => {
    console.log("e");
}, 0);
async1().then(res => {
    console.log("f")
})
new Promise((resolve) => {
    console.log("g");
    resolve();
}).then(() => {
    console.log("h");
});
console.log("i");
/**
* 输出结果:d a c g i b h f e 
*/

这段代码中声明了两个async方法,

  1. 在整体的Script语句中,首先输出:d
  2. setTimeout中的语句加入宏任务队列;

    宏任务队列微任务队列
    整体的Script
    setTimeout
  3. async1()执行,输出:a;后async2()执行,输出:c;await阻断执行;console.log("b")暂不执行,而去执行主线程任务;
  4. 此时async1()未执行结束,.then()方法不会被加入微任务队列;

    宏任务队列微任务队列
    整体的Script
    setTimeout
  5. 执行new Promise输出:g;.then(() => {console.log("h");})加入微任务队列;

    宏任务队列微任务队列
    整体的Scriptconsole.log("h")
    setTimeout

    6.执行console.log("i");语句,输出:i;此时主线程的同步任务已经全部执行结束,返回阻断处继续执行;输出:b;.then(res => {console.log("f")})加入微队列;

    宏任务队列微任务队列
    setTimeoutconsole.log("h")
    console.log("f")

    7.清空微任务队列后,执行宏任务队列的下一个任务,开启下一个循环;输出hfe

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
/**
* 输出结果:1,7,6,8,2,4,3,5,9,11,10,12(node与浏览器可能略有不同)
*/

注:

  • 在node中,process.nextTick()优先级高于Promise.then、catch、finally;
  • 在node中,宏任务队列:setImmediate()队列优先级高于setTimeout()队列;
  • 在node14以后,也是每执行一个宏任务、则清空即时的微任务队列。

参考文档:


很白的小白
145 声望125 粉丝