手记

JavaScript单线程与任务队列

多线程

在运行时理论的手记中,我们讲解的重点是 V8 引擎,也就是 JavaScript 引擎。如果我们放眼整个浏览器的" 运行时"理论,JavaScript 引擎只不过是其中的一个线程而已。浏览器还存在其他非常重要的线程,例如 GUI 渲染线程,负责页面的布局和绘制。如果我们再往上看,在浏览器中每打开一个新窗口,就会创建一个进程。进程是比线程更大的单位,一个进程总是会由一个或多个线程组成,进程之间相互独立( 涉及到跨域 ),但同一进程下的各个线程之间共享内存和数据。

单线程

众所周知,JavaScript 是基于单线程的语言。这种单线程和浏览器的多线程并不矛盾,要注意区分。JavaScript 的单线程主要体现在 JavaScript 引擎( 主线程 )不支持手动创建子线程,始终只有这一个能够编译和执行 JavaScript 代码的线程。但其他语言,例如 Java 就支持多线程开发,可以在代码书写阶段,明显的标示出来,哪一段代码需要在子线程中执行。

任务队列

单线程语言还有一个特点,需要维护一个" 任务队列 "。JavaScript 引擎只负责执行代码,而维护任务队列的工作都是交由其他线程去完成,首先,负责监听队列的线程叫作事件循环线程,一旦执行栈中的所有代码执行完毕,它就会将队列中的任务推进执行栈去执行。然后是定时器线程 ( setTimeout 等 )、事件线程 ( onclick 等 )和异步请求线程,他们负责向队列中添加任务。到了 ES6 时代,JavaScript 引擎不再只是负责执行代码,而是也可以向队列中添加 Promise 的回调函数。因此,ES6 标准将主线程以及定时器线程、事件线程和异步请求线程都归属到宏任务中,而宏任务中每个线程发布的回调函数都归属到微任务中,微任务也就是 ES5 时代的任务队列。从 ES5 到 ES6 ,JavaScript 的单线程运行原理并没有实质性的改变,ES6 只是让异步模式变得更加规范化。

示意图:

单线程执行过程

例子:

console.log(1);
setTimeout(function() {
    console.log(2);
}, 0);
new Promise(function(resolve) {
    console.log(3);
    for (var i = 0; i < 1000; i++) {
        if (i = 999) {
            resolve();
        }
    }
    console.log(4);
}).then(function() {
    console.log(5);
});
console.log(6);
//输出:1 3 4 6 5 2

第一步,执行第一个宏任务,也就是主线程中的同步任务,打印出数字 1 。

console.log(1);

第二步,执行中遇到了 setTimeout ,创建定时器线程,并分配到宏任务队列尾部等待执行。

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

第三步,执行中遇到了创建 Promise 实例,打印出数字 3 和 4,同时主线程发布自己的任务(回调函数)到微任务队列尾部等待执行。

new Promise(function(resolve) {
    console.log(3);
    for (var i = 0; i < 1000; i++) {
        if (i = 999) {
            resolve();
        }
    }
    console.log(4);
}).then(function() {
    console.log(5);
});

第四步,另一个同步任务在主线程上开始执行,打印出数字 6 。

console.log(6);

第五步,到此为止,第一个宏任务执行完毕,立即执行当前微任务队列中的所有任务,打印出数字 5 。

function() {
    console.log(5);
}

第六步,计时完成,宏任务中的定时器线程会发布自己的任务(回调函数)到微任务队列尾部等待执行。

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

第七步,到此为止,第二个宏任务执行完毕,立即执行当前微任务队列中的所有任务,打印出数字 2 。

function() {
    console.log(2);
}

运行时流程图

综合整个运行时手记,JavaScript 的运行时完整流程图如下:


如有错误,欢迎指正,本人不胜感激。

1人推荐
随时随地看视频
慕课网APP