原文地址: How JavaScript works in browser and node?javascript
有很是多满怀激情的开发者,他们搞前端或者搞后端,为JavaScript奉献本身青春和血汗。JavaScript是一种很是容易理解语言,毫无疑问它是前端开发中一个很是关键的部分。可是和其余语言不一样的是, 它是单线程的,这就意味着,同一时间只能有一个代码片断在执行。由于代码执行是线性的,若是当中有任何代码执行很长时间,将会阻塞后面须要被执行的代码。所以有时候你会在Google Chrome中看到这样的界面:前端
当你在浏览器打开一个网站时,它会使用一个JavaScript执行线程。这个线程负责响应一切操做,好比页面滚动、页面渲染、监听DOM事件(好比用户点击按钮)等等。可是若是JavaScript执行被阻塞了,那浏览器就什么事情也作不了,即意味着浏览器会呈现为卡死,没法响应的现象。java
不信你就在控制台输入试试:node
while(true){}
// ...
复制代码
你会上面语句以后的任何代码都不会被执行,这个‘死循环’会霸占着系统资源, 让浏览器没法响应用户操做. 无限递归调用也会出现这种状况, 不过下文会介绍,Javascript引擎对调用栈长度进行限制,无限递归会抛出RangeError异常, 而不会无休止地运行。git
感谢现代浏览器,如今不是全部打开的标签页都依赖于一个JavaScript线程。而是每一个标签页或者域名都会有独立的JavaScript线程。这样每一个标签页之间不会互相阻塞。好比你能够在Chrome中打开多个标签页,在某个标签页下执行上面的死循环,你会发现只有执行了上面语句的标签卡死,其余不受影响。github
为了可视化JavaScript 如何执行程序,咱们首先要理解JavaScript运行时。编程
和其余编程语言同样,JavaScript运行时有一个栈(Stack)和一个堆(Heap)存储器。后端
上图来源于[Fhinkel](Confused about Stack and Heap?)文章,关于栈和堆之间的差别讲得比较清晰. 举个例子:浏览器
在Java或者C#中, 值类型(primitives原始类型)存储在栈中,而引用类型(reference)则存储在堆中。C++规范没有规定栈和堆的内存分配,而是使用自动存储(automatic)期
和动态存储(dynamic)期
来做区分,局部变量是自动存储期,编译器会将它们存储在栈中。而动态分配的对象则一般保存在堆中。放在栈中的数据会在函数执行完毕后自动回收,而放在堆中的对象,若是没有释放就会形成内存泄露缓存
本文不会深刻解释Heap,你能够看这里. 在本文咱们感兴趣的是栈,栈是一个LIFO(后进先出)的数据结构,用来保存程序当前的函数执行上下文, 换句话说,它表示的是当前程序执行的位置. 每次开始执行一个函数,就会将该函数推入栈中,当函数返回时从栈中弹出。 当栈为空时表示没有程序正在执行。因此栈经常也称为‘调用栈’。
function baz(){
console.log('Hello from baz')
}
function bar() {
baz();
}
function foo() {
bar();
}
foo();
复制代码
所以, 当上面的程序加载进内存时,会开始执行第一个函数,即foo
。 所以第一个栈元素就是foo()
, 由于foo
函数会调用bar
函数,第二个栈元素就是bar()
; 同理bar
函数会调用baz
,第三个栈元素就是baz()
. 最后,baz
调用console.log
,最后一个栈元素就是console.log('Hello from baz')
栈会在函数执行完毕时(到达函数底部或者调用return)弹出。而后继续执行函数调用后续的语句:
每一个栈元素中,元素的状态也被称为栈帧(Stack Frame). 若是在函数调用抛出错误,JavaScript会输出栈跟踪记录(Stack trace),表示代码执行时的栈帧的快照。
function baz(){
throw new Error('Something went wrong.');
}
function bar() {
baz();
}
function foo() {
bar();
}
foo();
复制代码
上面的程序,咱们在baz
中抛出错误,JavaScript会打印出栈跟中记录,指出错误发生的地方和错误信息。
栈的大小不是无限的。例如Chrome就会限定栈的最大为16,000帧。因此无限递归会致使Chrome抛出Maximum Call Stack size exceeded
:
由于JavaScript是单线程的,因此它只有一个栈和堆。所以,若是其余程序想要执行一些东西,须要等待上一个程序执行完毕
对比其余语言,这多是一个糟糕的设计,可是JavaScript的定位就是通用编程语言,而不是用于很是复杂的场景
考虑这样一个场景。假设浏览器发送一个HTTP请求到服务器,加载图片并展现到页面。浏览器会卡死等待请求完成吗?显然不会,这样用户体验太差了
浏览器经过JavaScript引擎来提供JavaScript运行环境。好比Chrome使用V8 引擎。可是浏览器内部可不仅有JavaScript引擎。下面是浏览器的底层结构:
看起来很复杂,可是它也很好理解。JavaScript引擎须要和其余2个组件协做,即事件循环(EventLoop)和回调队列(CallbackQueue),回调队列也被称为消息队列或任务队列。
除了JavaScript引擎,浏览器还包含了许多不一样的应用来作各类各样的事情,好比HTTP请求、DOM事件监听、经过setTimeout、setInterval延迟执行、缓存、数据存储等等。这些特性能够帮助咱们建立丰富的Web应用。
想一下,若是浏览器只使用同一个JavaScript线程来处理上面这些特性,用户体验会有多糟糕。由于用户即便只是简单的滚动页面,背后是须要处理不少事情的, 单个Javascript线程压根忙不过来。所以浏览器会使用低级的语言,好比C++,来执行这些操做,并暴露简洁的JavaScript API给开发者。这些API统称为Web API。
这些Web API一般是异步的。这意味着,你能够命令这些API在'后台'(独立线程)去作一些事情,完成任务以后再通知Javascript运行时. 在此同时,Javascript引擎会继续执行剩下的JavaScript代码. 在命令这些API在后台作事情时,咱们一般须要给它们提供一个回调。这个回调的职责就是在Web API完成任务后执行JavaScript代码。让咱们将上述的全部东西整合起来理解一下:
当你调用一个函数时,它会被推动栈中。若是这个函数中包含了Web API调用,JavaScript会代理Web API的调用, 通知Web API执行任务,接着继续执行下一行代码直到函数返回。一旦函数到达return语句或者函数底部,这个函数就会从调用栈中弹出来。
与此同时,若是Web API在后台完成了它的工做,且有一个回调和这个工做绑定,Web API会将消息结果和回调进行绑定,并推入到消息队列中(或者称为回调队列).
事件循环, 就像一个无限循环,它的惟一工做是检查回调队列,一旦回调队列中有待处理的任务,就将该回调推送到调用栈。不过由于Javascript是单线程的, 事件循环一次只能推送一个回调到调用栈,栈将会执行回调函数,一旦调用栈为空,事件循环才会将下一个回调函数推送到调用堆。
事件循环的伪代码大概以下:
while(true) {
let task
while(task = popCallbackQueue()) {// 弹出回调队列任务
executeTask(task) // 执行任务, 这里面可能会触发新的Web API调用
}
if (hasAnyPendingTask()) {
sleep() // 睡一觉,有新任务推送到回调队列时时再唤醒我哦
} else {
break // 终止程序, 没什么好干的拜拜了
}
}
复制代码
咱们经过setTimeout Web API这个例子一步一步看看上述的一切是怎么运做的。setTimeout Web API主要用于延时执行一些操做,可是回调真正被执行, 须要等待当前程序执行完毕(即栈为空), 也就是说,setTimeout函数回调执行时间未必等于你指定的延时时间。setTimeout的语法以下:
setTimeout(callbackFunction, timeInMilliseconds);
复制代码
callbackFunction是一个回调函数,它将会在timeInMilliseconds以后执行. 咱们修改上面的代码来调用setTimeout:
function printHello() {
console.log('Hello from baz');
}
function baz() {
setTimeout(printHello, 3000);
}
function bar() {
baz();
}
function foo() {
bar();
}
foo();
复制代码
上面的代码延时调用了console.log. 栈仍是会像以前同样,如foo() => bar() => baz()
, 当baz开始执行并到达setTimeout时,Javascript会将回调函数传递给Web API,而且继续执行下一行。 由于这里没有下一行了,栈会弹出baz,接着弹出bar和foo。
在这期间,Web API正在进行3s等待,当时间到达时,它会将回调推动回调队列中。 由于这时候调用栈为空,事件循环会将这个回调推动栈中,并执行这个回调。
🎉🎉Philip Robers建立了一个神奇的在线工具Loupe,来可视化Javascript的底层运行。上面的实例能够查看这个连接🎉🎉
因此说咱们Javascript是单线程的,可是不少Web API的执行是多线程的。也就是说Javascript的单线程指的是‘Javascript代码’的执行是单线程.
经过Node.js咱们能够作更多的事情, 而不只限于浏览器的端。那么它是怎么运做的?
Node.js 和Chrome同样,一样使用Google的V8引擎来提供Javascript运行时. 它使用libuv(C++编写)来和V8的事件循环配合,扩展更多能够在后台执行的东西, 好比文件系统I/O, 网络I/O。Node的标准库API遵循了浏览器Web API的相似回调风格。
若是你比较了浏览器和node的结构图,你会发现二者很是类似。右侧的部分相似于Web API,一样包含事件队列(或者称为回调队列/消息队列)和事件循环。
V八、事件循环、事件队列都在单线程中运行,最右侧还有工做线程(Worker Thread)负责提供异步的I/O操做。这就是为何说Node.js拥有非阻塞的、事件驱动的异步I/O架构。
上面的内容都来源于Philip Roberts30min的高光演讲(五年前)
本文完