本文是以做者本身理解的思路拆分的JS运行机制,看法有限不免疏漏,欢迎留言勘误、交流。javascript
浏览器的主要功能是向服务器发送请求,在窗口中展现目标网络资源。
伴随着浏览器的的普及,Javascript是做为浏览器的附属工具诞生的,当初主要是为了作浏览器端的简单校验。前端
浏览器的主要功能能够总结为java
浏览器的内核是支持浏览器运行的最核心的程序。 对应着上述浏览器的主要功能, 主要有两个部分:node
渲染引擎: 将HTML、CSS、Javascript文本及相应的资源转换成图像结果。
其主要做用解析资源文件并渲染在屏幕上。
同时,它也是咱们一般所说的狭义的浏览器内核
web
想一想看,说起浏览器内核
咱们立马想到的是什么?chrome
它们的主要工做内容就是根据咱们HTML、CSS、JS的定义,绘制出相应的页面结构及展示形式。api
Javascript 是解释型语言
,在代码运行以前不会进行编译工做,将源码转换成字节码等中间代码或者是机器码,
而是在执行的过程当中实时编译,边编译边执行。浏览器
所以须要一个功能模块作编译转换相关的工做,Javascript引擎
就是来作这些事儿的。
总结来讲就是: 运行过程当中解析JS源代码并将其转换成可执行的机器码并执行。服务器
解释型语言 && 编译型语言
解释型: 代码运行以前,不须要编译,而是在执行过程当中现编译,边编译边执行
编译型: 代码在运行以前,须要先用编译器将其转换成机器语言或者中间字节码,而后再执行
一般状况下,说解释型语言慢,就是由于编译过程发生在执行过程当中网络
常见的Javascript引擎以下:
BUT, 仅仅依靠Javascript引擎
是不能胜任浏览器的JS处理工做的,事实上Javascript引擎
只是浏览器处理JS相关的模块中的一个小积木.
在Web开发中,咱们一般不会直接用到Javascript引擎。 事实上,Javascript引擎是工做在一个环境(容器)内的, 这个环境提供了一些额外的功能(API),咱们的代码在执行的时候可使用这些特性。
Stack overflow 上有一个高赞的回答,关于Javascript引擎和Javascript Runtime。
在这个观点中,不像其余编译型语言,编译以后成为机器码能够直接在主机上运行。 JS是运行在一个宿主环境中的(一个能够识别而且执行JS代码的程序),这个容器通常必需要作2件事情:
作第一部分工做的就是Javascript引擎
作第二部分功能的实际上就是咱们本节所说的 Javascript Runtime
, 所以咱们能够粗略地理解为, Javascript Runtime 就是JS宿主环境建立的一个scope, 在这个scope内JS能够访问宿主环境提供的一系列特性
常见的JS宿主环境有什么?
那么在这两个环境中,对应的引擎和runtime分别以下:
宿主环境 | JS引擎 | 运行时特性 |
---|---|---|
浏览器 | chrome V8引擎 | DOM、 window对象、用户事件、Timers等 |
node.js | chrome V8引擎 | require对象、 Buffer、Processes、fs 等 |
相同的JS引擎,只是在不一样的环境下,提供了不一样的能力。
总之, 最初的js被设计出来,只是为了作网页校验的,可是经过不一样环境下的Javascript Runtime
JS能够作更多的工做。
至此,关于JS运行时,咱们有一个简单的了解。
接下来,咱们再以浏览器环境中代码执行的逻辑为思路,看一看JS的运行机制。
根据上文的描述,JS源代码是须要通过JS引擎解析、转化以后才执行的。
一般认为,JS引擎主要有两部分组成:
源代码进入JS引擎以后,顺序读取代码,按照变量声明、函数执行等不一样规则,分配到堆、或者栈内。
存储项目引用类型数据的地方, 系统分配的内存,
JS中的引用类型数据,实际值是零散地存在这里面的
事实上,引用类型的存储是分为2部分存储的:
平时代码中的引用类型赋值,就是仅仅把栈内存储的内存地址赋给新变量,就至关因而告诉新变量该值在内存中的位置,须要的时候去取就行,并非把真正的值传递过去,内存中该值是只有一份的。这也是引发引用问题的缘由
执行栈,是代码中实际逻辑语句执行的地方,同时项目运行过程当中产生的基本类型的值也是存在此处。
引擎会把代码分红一个个可执行单元,而后依次进入执行栈,被执行
那么可执行单元是什么?
可执行单元,标准的说法是执行上下文
JS中,执行上下文
能够是如下几种:
这些东西有什么共同点?
全局代码能够看做是一个IIFE(当即执行函数),
函数就是通俗意义上的函数
eval 是能够把传入字符串执行的函数
函数啊, 所有都是函数啊
所以咱们能够粗略的理解为: 执行栈里面的东西,都是一个个函数调用
。
所以, JS引擎的示意图能够更新为以下:
JS是单线程的。 地球人都知道。
什么意思?
JS引擎中,代码执行是在调用栈的里发生的。
栈是一种LIFO(last in first out 后进先出)的数据结构。
只有栈顶的函数会被处理, 处理完成以后弹出栈, 后面的进入栈顶,再被执行...
举个🌰:
以下的JS文件
function first() {
second();
}
function second() {
console.log('log fn');
}
first();
... // 后续操做
复制代码
JS引擎处理这段代码的步骤以下 (只关注函数调用)
所以JS单线程,指的是在JS引擎中,解析执行JS代码的调用栈是惟一的,全部的JS代码都在这一个调用栈里按照调用顺序执行,不能同时执行多个函数。
单线程意味着什么?
意味着,正常状况下,JS引擎会按照代码书写的逻辑,依次调用函数,在任意时间点,有且只能有一个函数被执行。外层函数必须等到内层函数处理完毕有返回值以后才能继续执行。
为何要单线程?
JS最初被设计使用在浏览器上,做为浏览器上的脚本语言,须要与用户的操做互动以及操做DOM, 若是时多线程的话,须要关注各个线程之间状态的同步问题。
想象一下,js引擎中能够同时执行2个函数,若是两个函数操做同一个对象,那么到底以哪一个为准?
而后,操做DOM结构,在线程A上已经删了某节点,线程B同时还在对该节点一顿操做,这就尴尬了。
而作多线程的状态同步又是得不偿失的,所以就直接用单线程了。
有什么问题?
假如,某个函数耗时比较久,那么调用它的外层函数必须安安静静的等待这个函数执行完成才能继续执行, 若是,这个函数出错了,那么外层的函数也没办法继续执行了。
function foo(){
bar()
console.log('after bar')
}
function bar(){
while(true) {}
}
foo()
复制代码
好比这个栗子,foo函数中调用了bar函数,那么foo函数必须等待bar函数执行完毕才能继续执行后续代码,然而bar函数是个无限执行的函数哟,回不来的,那么foo函数等到花儿都谢了也没办法执行后面的代码的。
固然这是比较极端的状况,可是在前端的业务场景中有几类常见的case,在此是有问题的:
这些就是咱们常见的异步操做
, 事件触发以后并不能当即获得结果,按照以前的运行模式,浏览器就会阻塞其余操做,等待相应结果,表如今页面中就是页面卡死,这是一个优秀的应用所不能容许发生的。
同步&异步
关于同步和异步操做,借用朴灵大神的说法:
通常操做能够分为两个步骤,
- 发起调用
- 获得结果
发起调用,立马能够获得结果的是为
同步
发起调用,没法当即获得结果,须要额外操做才能获得结果的是为异步
所以当前的模型在异步操做
中是有问题的
解决方案?
问题的本质是js引擎的单线程工做模式,只专一于一件事情, 必须【执行至完成】
而产生问题的那些操做每每不能直接获得 结果,必须通过额外操做才能获得结果 【异步问题】
思路:能够把这些异步操做
分发给其余模块
,获得处理结果以后再把回调函数一块放入主线程执行。
这就是 事件循环(Event Loop)的主体思路。
event loop 只是解决异步问题的一种思路 其余的思路还有:
- 轮询
- 事件
上一章节提到,异步操做
能够交给JS引擎以外的其余模块
处理, 在浏览器中其余模块
就是Web API模块
Web API
其实就是上述的 JS runtime
提供的一系列宿主环境的特性集合。
在浏览器中,主要包括如下能力:
完美的cover了上述产生异步问题的几类case
所以至此,咱们能够得出如下的视图:
JS引擎中,执行栈遇到同步函数调用,直接执行获得结果后,弹出栈,继续下一个函数调用
遇到异步函数调用,将函数分发给Web API
模块,而后该异步函数弹出栈,继续下一个函数调用,不会产生阻塞问题。
问题来了?
这些异步操做分发给Web API
模块处理以后,不能说无论了,主线程
仍是须要知道结果作后续操做的,Web API
获得结果以后怎么通知主线程呢?
这就须要其余的模块帮忙了。
这个地方提到了
主线程
,就意味着还有其余的辅助线程。
是的,JS是单线程执行
的,可是并不意味着浏览器内核是单线程的。
事实上,web api模块内就有多个线程,每一个异步操做处理模块都对应一个线程
http请求线程、事件处理线程、定时器处理线程等
回调队列, 也叫事件队列、消息队列。
这个模块就是用来帮助Web API
模块处理异步操做的。
在Web API
模块中,异步操做在相应的线程中处理完成获得结果以后,会把结果注入异步回调函数的参数中,而且把回调函数推入回调队列中。
可是,只推到回调队列里也不是个事儿,由于前面说到了,全部的JS执行都发生在主线程调用栈里面。这些异步操做拿到结果以后,带着回调函数推入了回调队列,须要在适当的时机进入主线程调用栈执行。
那么,谁知道何时是合适的时机呢?
Event Loop 知道。
回调队列
队列是一个FIFO,先进先出的存储结构,
这样意味着异步操做的回调函数会按照进入队列的顺序被执行,而不是调用的顺序被执行
Event Loop 不停地检查主线程调用栈和回调队列,当发现主线程空闲的时候,就把回调队列里第一个任务推入主线程执行。 以此不停地循环。
至此,一个异步操做,兜兜转转最终拿到告终果,成功执行而且没有阻塞其余的操做。
一个完整的图示以下:
至此,本文在宏观结构上按照浏览器执行js文件的步骤,分析了浏览器环境的一些简单机制。 更多的执行细节,好比词法分析、做用域构建等在后续文章中会继续深刻。
快狗打车前端团队专一前端技术分享,按期推送高质量文章,欢迎关注点赞。
文章同步发布在公众号哟,想要第一时间获得最新的资讯,just scan it !