Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JS 是单线程,你了解其运行机制吗? #8

Open
biaochenxuying opened this issue Sep 21, 2018 · 1 comment
Open

JS 是单线程,你了解其运行机制吗? #8

biaochenxuying opened this issue Sep 21, 2018 · 1 comment
Assignees
Labels
JavaScript JavaScript 相关知识点

Comments

@biaochenxuying
Copy link
Owner

biaochenxuying commented Sep 21, 2018

一. 区分进程和线程

很多新手是区分不清线程和进程的,没有关系。这很正常。先看看下面这个形象的比喻:

进程是一个工厂,工厂有它的独立资源-工厂之间相互独立-线程是工厂中的工人,多个工人协作完成任务-工厂内有一个或多个工人-工人之间共享空间

如果是 windows 电脑中,可以打开任务管理器,可以看到有一个后台进程列表。对,那里就是查看进程的地方,而且可以看到每个进程的内存资源信息以及 cpu 占有率。

image

所以,应该更容易理解了:进程是 cpu 资源分配的最小单位(系统会给它分配内存)

最后,再用较为官方的术语描述一遍:

  • 进程是 cpu 资源分配的最小单位(是能拥有资源和独立运行的最小单位)

  • 线程是 cpu 调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

提示:

  • 不同进程之间也可以通信,不过代价较大

  • 现在,一般通用的叫法:单线程与多线程,都是指在一个进程内的单和多。(所以核心还是得属于一个进程才行)

二. 浏览器是多进程的

理解了进程与线程了区别后,接下来对浏览器进行一定程度上的认识:(先看下简化理解)

  • 浏览器是多进程的

  • 浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)

  • 简单点理解,每打开一个 Tab 页,就相当于创建了一个独立的浏览器进程。

关于以上几点的验证,请再第一张图:

image

图中打开了 Chrome 浏览器的多个标签页,然后可以在 Chrome 的任务管理器中看到有多个进程(分别是每一个 Tab 页面有一个独立的进程,以及一个主进程)。

感兴趣的可以自行尝试下,如果再多打开一个 Tab 页,进程正常会 +1 以上(不过,某些版本的 ie 却是单进程的)

**注意:**在这里浏览器应该也有自己的优化机制,有时候打开多个 tab 页后,可以在 Chrome 任务管理器中看到,有些进程被合并了(所以每一个 Tab 标签对应一个进程并不一定是绝对的)

三、为什么 JavaScript 是单线程 ?

JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript 不能有多个线程呢 ?这样能提高效率啊。

JavaScript 的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript 就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。

四. JavaScript是单线程,怎样执行异步的代码 ?

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
js 引擎执行异步代码而不用等待,是因有为有 消息队列和事件循环。

  • 消息队列:消息队列是一个先进先出的队列,它里面存放着各种消息。
  • 事件循环:事件循环是指主线程重复从消息队列中取消息、执行的过程。

实际上,主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消息、再执行。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。

事件循环用代码表示大概是这样的:

while(true) {
    var message = queue.get();
    execute(message);
}

那么,消息队列中放的消息具体是什么东西?消息的具体结构当然跟具体的实现有关,但是为了简单起见,我们可以认为:

消息就是注册异步任务时添加的回调函数。

再次以异步 AJAX 为例,假设存在如下的代码:

$.ajax('http://segmentfault.com', function(resp) {
    console.log('我是响应:', resp);
});

// 其他代码
...
...
...

主线程在发起 AJAX 请求后,会继续执行其他代码。AJAX 线程负责请求 segmentfault.com,拿到响应后,它会把响应封装成一个 JavaScript 对象,然后构造一条消息:

// 消息队列中的消息就长这个样子
var message = function () {
    callbackFn(response);
}

其中的 callbackFn 就是前面代码中得到成功响应时的回调函数。

主线程在执行完当前循环中的所有代码后,就会到消息队列取出这条消息(也就是 message 函数),并执行它。到此为止,就完成了工作线程对主线程的通知,回调函数也就得到了执行。如果一开始主线程就没有提供回调函数,AJAX 线程在收到 HTTP 响应后,也就没必要通知主线程,从而也没必要往消息队列放消息。

用图表示这个过程就是:

image

从上文中我们也可以得到这样一个明显的结论,就是:

异步过程的回调函数,一定不在当前这一轮事件循环中执行。

事件循环进阶:macrotask 与 microtask

一张图展示 JavaScript 中的事件循环:

image

一次事件循环:先运行 macroTask 队列中的一个,然后运行 microTask 队列中的所有任务。接着开始下一次循环(只是针对 macroTask 和 microTask,一次完整的事件循环会比这个复杂的多)。

JS 中分为两种任务类型:macrotask 和 microtask,在 ECMAScript 中,microtask 称为 jobs,macrotask 可称为 task。

它们的定义?区别?简单点可以按如下理解:

macrotask(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)

每一个 task 会从头到尾将这个任务执行完毕,不会执行其它

浏览器为了能够使得 JS 内部 task 与 DOM 任务能够有序的执行,会在一个 task 执行结束后,在下一个 task 执行开始前,对页面进行重新渲染
(task -> 渲染 -> task ->...)

microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务

也就是说,在当前 task 任务后,下一个 task 之前,在渲染之前

所以它的响应速度相比 setTimeout(setTimeout是task)会更快,因为无需等渲染

也就是说,在某一个 macrotask 执行完后,就会将在它执行期间产生的所有 microtask 都执行完毕(在渲染前)

分别很么样的场景会形成 macrotask 和 microtask 呢 ?

macroTask: 主代码块, setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering(可以看到,事件队列中的每一个事件都是一个 macrotask)

microTask: process.nextTick, Promise, Object.observe, MutationObserver

补充:在 node 环境下,process.nextTick 的优先级高于 Promise,也就是可以简单理解为:在宏任务结束后会先执行微任务队列中的 nextTickQueue 部分,然后才会执行微任务中的 Promise 部分。

另外,setImmediate 则是规定:在下一次 Event Loop(宏任务)时触发(所以它是属于优先级较高的宏任务),(Node.js 文档中称,setImmediate 指定的回调函数,总是排在 setTimeout 前面),所以 setImmediate 如果嵌套的话,是需要经过多个 Loop 才能完成的,而不会像 process.nextTick 一样没完没了。

实践:上代码

我们以 setTimeout、process.nextTick、promise 为例直观感受下两种任务队列的运行方式。

console.log('main1');

process.nextTick(function() {
    console.log('process.nextTick1');
});

setTimeout(function() {
    console.log('setTimeout');
    process.nextTick(function() {
        console.log('process.nextTick2');
    });
}, 0);

new Promise(function(resolve, reject) {
    console.log('promise');
    resolve();
}).then(function() {
    console.log('promise then');
});

console.log('main2');

别着急看答案,先以上面的理论自己想想,运行结果会是啥?

最终结果是这样的:

main1
promise
main2
process.nextTick1
promise then
setTimeout
process.nextTick2

process.nextTick 和 promise then在 setTimeout 前面输出,已经证明了macroTask 和 microTask 的执行顺序。但是有一点必须要指出的是。上面的图容易给人一个错觉,就是主进程的代码执行之后,会先调用 macroTask,再调用 microTask,这样在第一个循环里一定是 macroTask 在前,microTask在后。

但是最终的实践证明:在第一个循环里,process.nextTick1 和 promise then 这两个 microTask 是在 setTimeout 这个 macroTask 里之前输出的,这是为什么呢 ?

因为主进程的代码也属于 macroTask(这一点我比较疑惑的是主进程都是一些同步代码,而 macroTask 和 microTask 包含的都是一些异步任务,为啥主进程的代码会被划分为 macroTask,不过从实践来看确实是这样,而且也有理论支撑:【翻译】Promises/A+ 规范)。

主进程这个 macroTask(也就是 main1、promise 和 main2 )执行完了,自然会去执行 process.nextTick1 和 promise then 这两个 microTask。这是第一个循环。之后的 setTimeout 和process.nextTick2 属于第二个循环。

别看上面那段代码好像特别绕,把原理弄清楚了,都一样 ~

requestAnimationFrame、Object.observe(已废弃) 和 MutationObserver 这三个任务的运行机制大家可以从上面看到,不同的只是具体用法不同。重点说下 UI rendering。在 HTML 规范:event-loop-processing-model 里叙述了一次事件循环的处理过程,在处理了 macroTask 和 microTask 之后,会进行一次 Update the rendering,其中细节比较多,总的来说会进行一次 UI 的重新渲染。

事件循环机制进一步补充

这里就直接引用一张图片来协助理解:(参考自 Philip Roberts 的演讲《Help, I’m stuck in an event-loop》)

image

上图大致描述就是:

  • 主线程运行时会产生执行栈,栈中的代码调用某些 api 时,它们会在事件队列中添加各种事件(当满足触发条件后,如 ajax 请求完毕)
  • 而栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调
  • 如此循环
  • 注意,总是要等待栈中的代码执行完毕后才会去读取事件队列中的事件

五. 最后

看到这里,应该对 JS 的运行机制有一定的理解了吧。

参考:

  1. JavaScript 运行机制详解:再谈Event Loop

  2. 从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

  3. 总是一知半解的Event Loop

  4. JavaScript:理解同步、异步和事件循环

@biaochenxuying biaochenxuying added the JavaScript JavaScript 相关知识点 label Sep 21, 2018
@biaochenxuying biaochenxuying self-assigned this Sep 21, 2018
@Ercury
Copy link

Ercury commented Nov 5, 2021

good job!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
JavaScript JavaScript 相关知识点
Projects
None yet
Development

No branches or pull requests

2 participants