Javascript是如何运行的?(1)引擎、调用栈、事件循环

写在前面

从公众号建号至今,发了不少技术文,基本上每篇都有提到基础的重要性。

不久前我发了个朋友圈,内容是这样的「学习,应该先学习更好的思维,而不是更多的知识,在一个落后的思维模式里,增加再多的信息量,也只是低水平的重复。」

我向来强调基础知识的重要性,基础打牢了,框架,真的非常容易学

好了,不啰嗦了,步入正题

作为JavaScript使用人员,V8引擎作为一个概念,我想大多数人都听说过,而且绝大多数人也知道JavaScript是一个单线程语言,或者知道JavaScript使用的是回调队列的形式

在这篇文章中,我们来详细解释这些概念,从而来解释JavaScript是如何运行的

希望详细了解了这些内容后,你可以写出更好的非阻塞的代码,并且可以正确的使用JavaScript的API

JavaScript引擎

Google的V8引擎是现在最流行的JavaScript引擎,以Chrome为代表的浏览器,还有Node.js都是使用的V8引擎。

那什么是JavaScript引擎呢?

JavaScript引擎是执行JavaScript代码的程序或解释器。 JavaScript引擎可以实现为标准解释器,或即时编译器,它以某种形式将JavaScript编译为字节码。

咱们不纠结于这个概念,纠结概念不是咱们应该做的事情。但是概念必须熟记,因为概念是长篇大论的论证后得出的精髓。后面的文章中咱们再娓娓道来

下面主要简单描述下JavaScript引擎

一个非常简单的视图,看看引擎包含了什么:



V8引擎由两个主要的组件组成

  • Memory Heap(内存堆)--内存分配的任务就在这里面完成
  • Call Stack(调用栈)--这是代码执行时堆栈调用的位置

JavaScript运行时

几乎所有的JavaScript开发人员都使用过浏览器中的API(例如:setTimeout)。

其实这些API都不是由引擎提供的。

那么,这些API是从哪里来的呢?是由宿主提供的。

宿主是指JavaScript的运行环境。运行在浏览器上那么宿主就是指浏览器,运行在Node.js上宿主就是Node

就像前端界大牛winter说的那样,我们应该形成感性的认知:一个JavaScript引擎会常驻在内存中,它等待着宿主把JavaScript代码或者函数传递给它执行

那宿主把JavaScript传递给引擎后,引擎是怎么处理的呢?这就牵扯到调用栈与循环队列了,继续往下看,一个一个的说

调用栈(Call Stack)

先来说说调用栈

JavaScript是一个单线程语言,这意味着它只有一个堆栈,因此,它每次只能完成一件事情。

这个堆栈调用,是一种数据结构,它记录了程序中的位置,如果我们进入函数,如果我们进入一个函数,就将这个函数放在这个栈的顶部,如果我们从函数返回(return),那么就将这个函数从栈的顶部弹出。

我们现在给引擎一串JavaScript代码,看看引擎是怎么执行我们的代码的

function multiply(x, y) {
    return x * y;
}
function printSquare(x) {
    var s = multiply(x, x);
    console.log(s);
}
printSquare(5);

当JavaScript引擎开始执行这个代码时,先会清空栈,栈将会如下图执行:

从图中Step1,我们可以看出,我们之前定义的函数在没有使用的时候,并没有进入栈中。所以未被调用的函数并不会存在栈中。

继续看Step1,我们使用了printSquare,那么就要进入这个函数进去看看它里面到底实现了什么。此时我们就将我们进入的这个函数放到栈的最上方(虽然只有它自己但我们也要这样描述)

我们进入这个函数后,发现这货还调用了别的函数multiply,没办法,只好再进入multiply,也就到了Step2,将multiply放入栈的最顶部

执行完multiply后,将它从栈的顶部丢出去,继续console。log(s),也就可以看到是Step3

以此类推,直到调用栈清空。JavaScript引擎则将我们的代码执行完毕。

上图栈中的每一步,均被称为堆栈帧(Stack Frame),每一帧都代表着栈的变动

我们再来看看,当抛出错误时,堆栈如何构造跟踪的

当异常发生时,基本上是调用堆栈的状态

function foo() {
    throw new Error('SessionStack will help you resolve crashes :)');
}
function bar() {
    foo();
}
function start() {
    bar();
}
start();

在Chrome浏览器中执行上述代码,将产生以下堆栈跟踪

这个错误信息怎么看呢?从上往下看,这就是一个调用栈的内容,现在处于顶部的函数抛出异常,无法正常的从栈顶部弹出

再来说说堆栈溢出,当达到最大调用栈大小时,就非常容易发生「堆栈溢出」

function foo() {
    foo();
}
foo();

当JavaScript引擎开始执行上述代码时,它开始调用函数foo,并且这个函数还会自己调用自己,还没有任何终止条件。

所以,在执行的每个步骤中,函数会一遍又一遍的添加到调用栈中,就像下图这样:


但是,有些时候,调用堆栈中的函数调用超过调用堆栈的实际大小时,浏览器会采取措施,抛出错误,如下图这样:

在单线程上运行代码非常简单,因为不必考虑多线程环境中出现的复杂场景,比如死锁、读写一致等

但是在单个线程上运行也是非常有限的,由于JavaScript只有一个调用栈,当调用的某个函数,执行的非常缓慢时,我们又该怎么办呢?

并发与事件循环

如果在调用堆栈中有的函数需要花费大量的时间才能处理时,那后面的内容不就卡死了么?

比如说在JavaScript中进行一些复杂的图像处理。问题就在调用栈在执行这个图像处理函数时,它是无法再做任何别的事情的。

这意味着浏览器无法渲染,无法运行任何其它的代码,看起来它就像是卡住了

并且这还不是唯一的问题,一旦浏览器在调用栈中开始处理大量的任务,浏览器可能就会停止响应,并且大量的浏览器会报错,告诉你当前页面崩溃了

网页都崩掉了,还有用户体验可言么?

如果想网页流程,那么就需要避免此类问题。

那么,我们如何在不阻塞UI并使浏览器无响应的情况下执行繁重的代码呢?这依靠的就是JavaScript的「异步回调」

这时候就牵扯到JavaScript引擎的异步事件循环

在这儿,我觉得GitHub上用户「@Mavericker-1996」的回答已经说的非常详细到位,在此我就不重复造轮子了,只是略加修改

任务队列

首先我们需要明白以下几件事情:

  • JS分为同步任务和异步任务
  • 同步任务都在主线程上执行,形成一个调用栈
  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件
  • 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行

根据规范,事件循环是通过任务队列的机制来进行协调的。

一个 Event Loop 中,可以有一个或者多个任务队列(task queue)。

一个任务队列便是一系列有序任务(task)的集合,每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。 setTimeout/Promise 等API便是任务源,而进入任务队列的是他们指定的具体执行任务。

宏任务

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

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

(macro)task->渲染->(macro)task->...

(macro)task主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)

微任务

microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。

所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。

microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)

运行机制

在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

流程图如下:

Promise和async中的立即执行

我们知道Promise中的异步体现在thencatch中,所以写在Promise中的代码是被当做同步任务立即执行的。而在async/await中,在出现await出现之前,其中的代码也是立即执行的。那么出现了await时候发生了什么呢?

await做了什么?

从字面意思上看await就是等待,await 等待的是一个表达式,这个表达式的返回值可以是一个promise对象也可以是其他值。

很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码。

实际上await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。

由于因为async await 本身就是promise+generator的语法糖。所以await后面的代码是microtask。所以对于本题中的

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

等价于

async function async1() {
    console.log('async1 start');
    Promise.resolve(async2()).then(() => {
                console.log('async1 end');
        })
}

写在最后

了解原理,了解执行机制不像学习框架的使用那样简单,是一件略微困难的事情。

因为框架都将这些内容给封装起来了,并不需要我们去进行处理,但是就算我们使用框架,也需要了解执行机制,这样我们书写的代码逻辑顺序才不会出错,得到的结果才能与预期一致。

基于JavaScript的框架,均是建立在此基础之上

本文牵扯内容点较多,有些内容一笔带过了,但并不代表不重要,后面我会逐一细写。

如果对后面的并发与事件循环觉得内容较难理解,可以先看看我之前写的白话入门篇:6分钟看懂Node.js武功精髓

关注微信公众号「闹闹吃鱼」更多有趣的内容等着你哦

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容