[译] JS在浏览器和Node下是如何工作的?

原文:https://itnext.io/how-javascript-works-in-browser-and-node-ab7d0d09ac2f

在 JavaScript 王国的土地上,无数开发者在前端或后端领域热情耕耘着。JS 易于理解,也是前端开发中不可或缺的部分。但不同于其他编程语言,这玩意是单线程的,也就是说代码要依次执行。因此一旦有代码占用时间过长,就会阻塞其他需要执行的代码 -- 所以以下画面在 Google Chrome 中时不时会出现。

1. 浏览器中的情况

假设你在浏览器中打开一个页面,其使用了一个单独的 JS 执行线程。该线程负责处理所有事,如滚动页面、打印页面上的某些东西、监听 DOM 事件(比如点击)等等。但当 JS 被 “阻塞” 后,浏览器就会停止干这些活,这也意味着它被冻结并毫无反应了。

用这句无尽的 while 循环就可以看到这种效果。

while(true){}

以上语句之后的任何代码都不会执行,循环将一直执行直至系统资源耗尽;无限的递归调用也会引发这种效果。

还好有现代浏览器 -- 并非所有打开的浏览器 tabs 都依赖同一个 JS 线程,相反每个 tab 或每个域名都有各自的 JS 线程。比如 Google Chrome,你可以用多个 tabs 打开不同的网站,并运行以上的 while 循环,而被冻结的只有运行该循环的那个 tab,其他 tabs 则一切如常。当然,由于 Chrome 还实现了 one-process-per-site 策略,打开同一域名页面的不同 tabs 间也会共享同样的 JS 执行线程,所以这样的 tabs 也将被冻结。

要形象化的了解 JS 如何执行一段程序,需要理解其运行时:

和其他任何编程语言一样,JS 运行时包含一个栈(stack)和一个堆(heap)存储。关于堆的解释不展开了,我们说说  。栈作为一种 LIFO (后入先出) 的数据存储结构,保存着当前程序的函数执行上下文。当程序被载入内存,从第一个函数调用 foo() 那里先开始执行。

第一条栈记录是 foo(),由其调用的 bar() 为第二条,继而依次被调用的 baz() 和 console.log() 则是第三第四条。

直到一个函数 return 了什么东西(在其执行的时候)之前,它都不会被从栈中弹出。栈所做都就是一边在记录(也就是函数)返回值后将它们一个接一个的弹出,一边继续等待其他函数的执行。

在每条记录(entry)上,栈的状态也称做 栈帧(stack frame)。若是哪个栈帧上的函数调用发生了错误,JS 会将其代码执行快照打印成 堆栈追踪(stack trace)

function baz(){
   throw new Error('Something went wrong.');
}

function bar() {
   baz();
}

function foo() {
   bar();
}

foo();

上例中,我们在 baz 函数中抛出错误,则 JS 会打印以下堆栈追踪,从而指出什么出错了以及从哪发生的。

由于 JS 是单线程的,它只有一个栈和一个堆。因此,如果其他程序想执行点什么,就得等着上一个程序被执行完毕。

这对于任何编程语言来讲都是糟糕的,但 JS 就是被设计成一种通用目的编程语言而非用来处理过于复杂的事务的。

所以让我们设想一个场景。如果浏览器发送一个加载数据或图片的 HTTP 请求会怎样呢?浏览器会在那个请求完成之前假死吗?真那么样的话,用户体验可太糟了。

浏览器有一个 JS 引擎,用来提供 JS 运行时环境。譬如,chrome 用的是 Google 自个儿开发的 V8 JavaScript engine。但你猜怎么着,浏览器不只有这一个 JS 引擎呢,其底层机制大概是这样的:

看着眼花缭乱其实很好理解。JS 运行时包含的若干组件中实际上也就 2 个最重要 -- 事件循环(event loop) 和 回调队列(callback queue),后者有时也称作 消息队列(message queue) 或 任务队列(task queue)

除了 JS 引擎之外,浏览器中还包含诸如发送 HTTP 请求、监听 DOM 事件、延迟执行 setTimeout 或 setInterval、缓存、数据存储等各种应用逻辑,正是这些特性帮助了我们创建富 web 应用。

但如果浏览器不得不用同一个 JS 引擎执行以上这些特性,则用户体验将不堪设想。因为即便只是用户滚动一下页面,也会在后台触发许多事情。因此浏览器使用了 C++ 等低级语言去执行这些操作,并提供整洁有效的 JavaScript API,这些 APIs 正是 Web APIs

这些 Web APIs 是 异步的,意味着我们可以一边命令这些 APIs 在后台默默做事并在完成后返回数据,一边继续执行更多的 JS 代码。与这些工作在后台的 APIs 相搭配的是,我们要提供一个 回调(callback)函数,用以负责在 Web API 一旦完成后执行相应的 JS 代码。我们需要了解所有这些概念是怎么揉合在一块儿的:

  1. 当调用一个函数时,就把它推入运行时中的栈中

  2. 若该函数中包含 Web API 调用,则 JS 将其控制权连同一个 callback 委派给 Web API 后移动到该函数中的下一行;一旦该函数中碰到了 return 语句,该函数就被移出栈,并进入下一个栈帧

  3. 同时,Web API 在后台执行其关联了 callback 的任务;任务一完成,Web API 就将执行结果和 callback 绑定后发布一个消息到 回调队列(所以也叫做消息队列)

  4. 事件循环 唯一的工作就是盯着 -- 回调队列上一有待执行(pending)的 callback 函数,就将其推入栈中;而这一动作发生的时间点,是 栈一旦为空的时候

  5. 稍倾,栈将会执行 callback 回调函数

下面来看看当我们具体使用 setTimeout Web API 时,所有事情是如何一步接一步工作的。setTimeout Web API 通常用来执行一些若干秒钟之后执行的事情,该执行过程发生在程序中的所有代码都完事那一刻(栈一旦为空的时候)。setTimeout 的语法如下:

setTimeout(callbackFunction, timeInMilliseconds);

callbackFunction 是一个回调函数,将晚于 timeInMilliseconds 毫秒之后执行。让我们用这个 API 改写一下之前的例子:

function printHello() {
    console.log('Hello from baz');
}

function baz() {
    setTimeout(printHello, 3000);
}

function bar() {
    baz();
}

function foo() {
    bar();
}

foo();

对程序作出的唯一更改是:将 console.log 延迟了 3 秒钟 执行。在本例中:

  1. 栈仍会以 foo() => bar() => baz() 的顺序构建

  2. 但当 baz() 开始执行并碰到 setTimeout API 调用时,JS 会将回调函数 printHello传递给 Web API,然后尝试移动到接下来的一行

  3. 在此,并没有下一行,栈就会将 baz() 弹出,并依此将 bar() 和 foo() 也一一弹出

  4. 同时,Web API 在等待中度过 3 秒钟后,将回调 printHello 推入回调队列

  5. 因为这时栈也为空了,事件循环也将把这个回调函数取回栈中,并在此被执行。

Philip Robers 已经创建了一个令人赞叹的在线工具以可视化 JS 底层的工作机理。上面的例子运行如下:

2. 在 Node.js 中会怎样

当同样的事情发生在 Node.js 中时,就得做的更多些了 -- 因为 node 所承诺的能力也更强。在浏览器中,我们被能在后台做什么掣肘。但在 node 中,能在后台做到几乎大部分的事情,尽管那只是个简单的 JS 程序。但是,这是如何做到的呢?

Node.js 也使用了 Google’s V8 engine 提供 JS 运行时,却没有局限于其事件循环;而是使用 libuv库 (用 C 写的) 与 V8 的事件循环一同工作,从而扩展了可以在后台所做之事。Node 遵循了类似于 Web APIs 的回调机制,并以和浏览器相似的方式工作。

如果比较一下浏览器那张图和上面这张 node 的图,可以看到其相似之处。图中整个右半边看起来就像 Web API,但其既包含 事件队列 (event queue,或称 callback queue / message queue) 又包含 事件循环 (event loop);不同于 V8 的是,这二者虽然还是在单一线程上运行,而独立的 worker 线程则承担了提供异步 I/O 操作的功能。这就是为什么 Node.js 号称是 非阻塞事件驱动异步 I/O 架构 的原因了。



--End--

搜索 fewelife 关注公众号

转载请注明出处

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值