资讯专栏INFORMATION COLUMN

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

AlphaGooo / 3016人阅读

摘要:的单线程,与它的用途有关。事件循环事件循环是指主线程重复从消息队列中取消息执行的过程。到此为止,就完成了工作线程对主线程的通知,回调函数也就得到了执行。

一. 区分进程和线程

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

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

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

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

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

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

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

提示:

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

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

二. 浏览器是多进程的

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

浏览器是多进程的

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

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

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

图中打开了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响应后,也就没必要通知主线程,从而也没必要往消息队列放消息。

用图表示这个过程就是:

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

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

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

一次事件循环:先运行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》)

上图大致描述就是:

主线程运行时会产生执行栈,栈中的代码调用某些api时,它们会在事件队列中添加各种事件(当满足触发条件后,如ajax请求完毕)

而栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调

如此循环

注意,总是要等待栈中的代码执行完毕后才会去读取事件队列中的事件

五. 最后

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

参考:

http://www.ruanyifeng.com/blo...

https://mp.weixin.qq.com/s/vI...

https://mp.weixin.qq.com/s?__...

https://mp.weixin.qq.com/s/k_...

我不是大神,也不是什么牛人,写这个号的目的是为了记录我自学 web全栈 的笔记。

全栈修炼 有兴趣的朋友可以扫下方二维码关注我的公众号

我会不定期更新有价值的内容,长期运营。

关注公众号并回复 福利 可领取免费学习资料,福利详情请猛戳: Python、Java、Linux、Go、node、vue、react、javaScript

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/96378.html

相关文章

  • 10分钟理解JS引擎的执行机制

    摘要:深入理解引擎的执行机制灵魂三问为什么是单线程的为什么需要异步单线程又是如何实现异步的呢中的中的说说首先请牢记点是单线程语言的是的执行机制。 深入理解JS引擎的执行机制 1.灵魂三问 : JS为什么是单线程的? 为什么需要异步? 单线程又是如何实现异步的呢? 2.JS中的event loop(1) 3.JS中的event loop(2) 4.说说setTimeout 首先,请牢记2...

    zzbo 评论0 收藏0
  • JS高级入门教程

    摘要:解析首先简称是由欧洲计算机制造商协会制定的标准化脚本程序设计语言。级在年月份成为的提议,由核心与两个模块组成。通过引入统一方式载入和保存文档和文档验证方法对进行进一步扩展。其中表示的标记位正好是低三位都是。但提案被拒绝了。 JS高级入门教程 目录 本文章定位及介绍 JavaScript与ECMAScript的关系 DOM的本质及DOM级介绍 JS代码特性 基本类型与引用类型 JS的垃...

    zsy888 评论0 收藏0
  • JavaScript事件循环

    摘要:事件循环当主线程中的任务执行完毕后,会从任务队列中获取任务一个个的放在栈中执行去执行,这个过程是循环不断的,所以整个的这种运行机制又称为事件循环。 写在前面 说起javascript(以下简称js)这门语言,相信大家已经非常熟悉了,不管是前端开发还是后端开发几乎无时无刻都要跟它打交道。虽说开发者每天几乎都要操作js,但是你真的确定你掌握了js的运行机制吗!下面我们就来聊聊这话题。 Ja...

    Corwien 评论0 收藏0
  • [译文] JavaScript工作原理:引擎、运行时、调用栈概述

    摘要:调用栈是单线程编程语言,意味着它只有单一的调用栈。调用栈是一种数据结构,基本记录了程序运行的位置。举个例子,先来看如下所示的代码当引擎开始执行这段代码时,调用栈将是空的。这正是抛出异常时栈追踪的构造过程这基本上就是异常抛出时调用栈的状态。 原文 How JavaScript works: an overview of the engine, the runtime, and the c...

    PAMPANG 评论0 收藏0
  • 探秘JS的异步单线程

    摘要:对于通常的特别是那些具备并行计算多线程背景知识的来讲,的异步处理着实称得上诡异。而这个诡异从结果上讲,是由的单线程这个特性所导致的。的特性之一是单线程,也即是从头到尾,都在同一根线程下运行。而这两者的不同,便在于单线程和多线程上。 对于通常的developer(特别是那些具备并行计算/多线程背景知识的developer)来讲,js的异步处理着实称得上诡异。而这个诡异从结果上讲,是由js...

    cooxer 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<