JS高级——浏览器运行前端项目的原理及流程
一、认识浏览器
二、V8引擎
三、V8引擎中执行JS代码过程(涉及执行上下文、作用域提升)
四、浏览器事件循环-微任务和宏任务
一、认识浏览器
在生活中或者是工作中,我们对浏览器已经非常熟悉,比如谷歌浏览器、Microsoft浏览器、搜狗浏览器、360浏览器等,我们可以用它购物、阅读、聊天等等,但是浏览器是如何把网页渲染出来的呢?用户为何可以使用鼠标进行某些点击操作的呢?接下来,我们来了解浏览器的组成及其工作原理和渲染过程。
1、浏览器组成
浏览器是由浏览器内核和JS引擎组成。
(1)浏览器内核也称为浏览器排版引擎、页面渲染引擎或样板引擎,主要是对HTML文件和CSS文件进行解析,并进行排版、渲染,最终呈现出一个网页。
比如:Gecko、Trident、Webkit、Blink等浏览器内核。
(2)JS引擎主要是将JavaScript代码翻译成CPU指令(机器码),然后才能被CPU执行的。
比如:SpiderMonkey、Chakra、JavaScriptCore、V8等JS引擎。
2、浏览器工作原理
当我们在浏览器中输入一个IP地址或者是一个网页链接时,那么浏览器是如何访问的服务器资源的呢?
3、浏览器渲染过程
浏览器在获取相关文件资源时,如何进行渲染的呢?
二、V8引擎
在开发中,我们大部分开发人员会使用谷歌浏览器,这是因为谷歌浏览器的V8引擎,在解析和运行JS代码时的速度和渲染效率是很高的。接下来我们来详细讲解V8引擎的工作原理。
1、认识V8引擎
V8是用C ++编写的Google开源高性能JavaScript和WebAssembly引擎,它用于Chrome和Node.js等。它实现ECMAScript和WebAssembly,并可在Windows 、macOS、Linux系统上运行。同时V8可以独立运行,也可以嵌入到任何C++应用程序中。
2、V8引擎的结构
(1)Parse模块会将JavaScript代码转换成AST。如果函数没有被调用,那么是不会被转换成AST的
(2)Ignition是一个解释器,会将AST转换成ByteCode。同时会收集TurboFan优化所需要的信息;如果函数只调用一次,Ignition会执行解释执行ByteCode。
(3)TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码。如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能;同样机器码实际上也会被还原为ByteCode,如果后续执行函数的过程中,类型发生了变化,之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码。
(4)生成AST树后,会被Ignition转成字节码,之后的过程就是代码的执行过程。
三、V8引擎中执行JS代码过程
1、初始化全局对象、执行上下文栈(调用栈)
1.1 初始化全局对象
js引擎会在执行代码之前,会在堆内存中创建一个全局对象:GlobalObje(GO),特点:
(1)该对象的作用域为全局作用域
(2)包含Date、Arra、String、Number、setTimeOut等
(3)有一个window对象,指向自己,即:window = GO
1.2 执行上下文栈
js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈。
2、V8引擎中执行JS代码过程解析
JS引擎在执行代码前,创建了一个全局对象,同时会生成一个全局执行上下文Global Execution Context,简称:GEC,GEC会被放在调用栈中。那么此时在内存中,有以下结构,称为最初结构
下面根据具体代码来详细说明每一步做了什么:
2.1 案例一:简单代码执行
提问:为什么第一行代码结果为undefined?
console.log(str); //undefinedvar str = 'code'console.log(str); //codefunction foo() {var age = 10console.log(age);
}foo() //10
(1)第一步:进行预解析,会将全局定义的变量、函数等加入到GO中,但是并不会赋值(注意:定义和代码执行是两个概念,不要搞混淆了)。此时变量的值为undefined,函数会指向一块内存地址,这块地址是内存给其分配,用来保存函数执行体中的内容和指向父作用域的指针。
(2)第二步:从上往下执行函数
执行第1行代码时,str在GO为undefined,所以打印结果为undefined,这就是作用域提升的原因;
执行第3行代码时,给str赋值,此时GO中str=code;
执行第5行代码时,打印str结果就为code;
执行第12行代码时,是一个函数的执行,那么遇到函数时,如何执行呢?
函数执行过程:
a.会根据函数体创建一个函数执行上下文(Functional Execution Context, 简称FEC),并且压入到ECS中。
FEC中包含三部分内容:第一部分,创建一个Activity Obj(AO),AO中包含形参、函数定义、arguments、和指向函数对象、定义的变量;第二部分,作用域链:由VO(在函数中就是AO对象)和父作用域组成,查找变量时会一层层查找。第三部分:this绑定的值(暂时先不介绍)
b.对函数体中的代码进行预解析,此时变量age的值为undefined;
c.从上往下执行foo函数中的代码,此时age被赋值为10,然后打印出结果。
d.foo函数执行完后,被调用栈移除,对应的AO对象也会从内存中移除;
(3)当foo函数执行完毕后,整个JS代码也就执行完毕了,此时GEC从调用栈中被移除,GO所指向堆内存中的内容,也会被移除。
2.2 案例二:在案例一的代码中,定义函数foo之前调用一次foo函数
提问:在定义函数之前执行函数,会打印undefined的还是会报错呢?
console.log(str); //undefinedvar str = 'code'console.log(str); //codefoo() //undefined或报错? 答案:正常执行,打印出10
function foo() {var age = 10console.log(age);
}foo() //10
答案: foo函数会正常执行,原因是在全局预解析时,GO对象中会记录foo函数,并且指向一块内存空间;当foo函数执行时,同样会创建一个函数指向上下文FEC,并压入调用栈ECS中;接着进行函数预解析和从上往下执行函数体中的代码。(流程和案例一的函数执行流程一样)
案例二中代码执行流程只是比案例一中代码流程多执行一次foo函数,其他流程不变。
2.3 案例三:全局定义foo函数,foo函数中定义bar函数,并在foo函数中调用bar函数
提问:foo函数执行后,两次打印结果分别是多少?
var age = 10function foo() {var age = 20console.log(age);//输出? 20function bar() {console.log(age);//输出? 20}bar()
}foo()
接下来我们来画图分析:
(1)全局预解析,将全局定义的变量、函数添加到GO中
(2)从上往下执行全局代码
a.给全局变量age赋值10;
b.接下来执行foo函数,此时会创建一个函数执行上下文和AO对象;
c.foo函数预解析,由于foo函数中定义了一个函数bar,会分配一个内存空间来保存bar函数体中的内容和父作用域指针;
(3)开始执行foo函数体中的代码
a.给局部变量age赋值20;
b.执行打印命令console.log(age),此时会在foo函数自己的作用域查找age,能找到age,直接打印,输出值为20;
c.接下来执行bar函数,此时会创建一个函数执行上下文FEC和AO对象,将FEC压入调用栈ECS中;
d.bar函数预解析,bar函数中只有执行没有定义变量和函数,所以只有默认的形参;
(4)执行bar函数中的代码
a.执行console.log(age) ,首先会在bar函数作用域中查找age,此时bar函数作用域没有age变量,那么就会查找父作用域(foo函数作用域)中的age,发现父作用域中有age,那么打印输出,输出值为20;
(5)bar函数执行完毕,移除其FEC和AO;然后foo函数执行完毕,移除其FEC和AO;最后整个JS代码执行完毕,移除GEC和GO。
2.4 案例四:全局定义foo函数和bar函数,在foo函数调用bar函数
提问:foo函数执行后,两次打印结果又分别是多少?
var age = 10function foo() {var age = 20console.log(age);//输出? 20bar()
}
function bar() {console.log(age);//输出? 10
} foo()
此案例的流程分析就不画了,分析流程差不多,在执行bar函数时,查找age变量,在bar函数作用域中没有变量age,那么需要到其父作用域中查找age,此时bar函数的父作用域是全局作用域,而全局作用域中,变量age的值为10,所以输出值为10。感兴趣的小伙伴可以动手画一画流程分析图。
四、浏览器事件循环-微任务和宏任务
JS代码在浏览器中的执行过程上面已经介绍了,但是上面执行的代码都只是一般情况的代码执行,都是从上往下依次执行;实际上在开发中我们会经常使用网络请求(axios)、promiese、setTimeOut、setInterval等异步操作时,那么在执行代码时浏览器会按照什么样的执行顺序来执行呢?接下来让我们来了解浏览器的事件循环机制。
1、浏览器中的JavaScript线程
操作系统中的进程和线程,在这里就不过多解释了,不了解的小伙伴可以查询一下资料。
1.1 我们知道JavaScript是单线程的,它的容器进程是浏览器或Node。那么浏览器是单个进程吗?进程里面只有一个线程吗?
答案是目前多数的浏览器其实都是多进程的,当我们打开一个tab页面时就会开启一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出;每个进程中又有很多的线程,其中包括执行JavaScript代码的线程。
1.2 JavaScript的代码执行是在一个单独的线程中执行的
这就意味着JavaScript的代码,在同一个时刻只能做一件事;如果这件事是非常耗时的,就意味着当前的线程就会被阻塞,所以真正耗时的操作,实际上并不是由JavaScript线程在执行的,是由浏览器的其他线程来完成的,比如网络请求、定时器,我们只需要在特定的时候执行应该有的回调即可。
2、浏览器的事件循环
2.1 首先我们先来看下事件循环的流程图
(1)JavaScript线程执行JS代码,会将异步操作分发给浏览器其他线程进行操作;
(2)然后对异步操作进行分类,划分为微任务队列和宏任务队列;
(3)最后调用栈会对循环队列中的函数进行回调,在调用栈中执行;
那么现在问题来了,我们怎么知道异步操作是属于宏任务还是属于微任务?调用栈在调用循环队列中的函数时,调用的优先级是怎么样的呢?
2.2 宏任务和微任务
(1)宏任务队列:ajax、setTimeout、setInterval、DOM监听、UI Rendering等;
(2)微任务队列:Promise的then回调、 Mutation Observer API、queueMicrotask()等;
2.3 宏任务和微任务优先级
(1)优先级最高:编写的顶层JS代码,如图中除去setTimeOut函数中的其他代码;
(2)微任务优先级大于宏任务:在执行每个宏任务之前,要先查看微任务队列中是否有微任务需要执行,如果有则先执行微任务;如果没有则执行当前宏任务。
2.4 事件循环测试题
(1)测试题一
setTimeout(function () {console.log("setTimeout1");new Promise(function (resolve) {resolve();}).then(function () {new Promise(function (resolve) {resolve();}).then(function () {console.log("then4");});console.log("then2");});
});new Promise(function (resolve) {console.log("promise1");resolve();
}).then(function () {console.log("then1");
});setTimeout(function () {console.log("setTimeout2");
});console.log(2);queueMicrotask(() => {console.log("queueMicrotask1")
});new Promise(function (resolve) {resolve();
}).then(function () {console.log("then3");
});
我们来画图解析,首先画出三个框分别表示输出值、微任务列表、宏任务列表,顺序都是从上到下,开始都是空的;代码部分内容较多,圈出来使用标签代替;执行玩的部分划掉。如图:
(1)执行第1行,是一个setTimeOut,属于宏任务,所以将part1部分放入宏任务队列;
(2)执行第15行,是一个Promise,函数参数直接执行,所以main script中写入promise1;promise.then()属于微任务,所以将then1放入微任务队列;
(3)执行22行,setTimeOut属于宏任务,将setTimeOut2部分放入宏任务队列;
(4)执行26行,直接输出,main script放入2;
(5)执行28行,queueMicrotask属于微任务,将queueMicrotask1放入微任务队列;
(6)执行32行,promise.then 属于微任务,将then3放入微任务;
此时,直接执行代码已执行完,下面执行微任务队列和宏任务队列,微任务优先级大于宏任务。
(7)执行微任务then1,将then1放入main script;
(8)执行微任务queueMicrotask1,将queueMicrotask1放入main script;
(9)执行微任务then3,将then3放入main script;
此时,微任务队列为空,开始执行宏任务。
(10)执行宏任务part1,将setTimeOut1放入main script;Promise.then属于微任务,将part2放入微任务队列;
此时,微任务中有part2,宏任务中有setTimeOut2,由于微任务优先级大,则执行微任务。
(11)执行微任务part2,Promise.then属于微任务,将part3放入微任务列表;将then2放入main script;
此时,微任务中有part3,宏任务中有setTimeOut2,由于微任务优先级大,则执行微任务。
(12)执行微任务part2,将then4放入main script;
此时,微任务队列为空,开始执行宏任务。
(13)执行setTimeOut2,将setTimeOut2放入main script;
至此,所有代码执行完毕,输出结果顺序为main script中的内容。
(2)测试题二 过程就不画了,可以自己动手画一画
async function async1() {console.log('async1 start')await async2();//其后面执行的代码相当于放进then中,作为微任务console.log('async1 end')
}async function async2() {console.log('async2')
}console.log('script start')setTimeout(function () {console.log('setTimeout')
}, 0)async1();new Promise(function (resolve) {console.log('promise1')resolve();
}).then(function () {console.log('promise2')
})console.log('script end')// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
总结:浏览器中执行JS代码,最主要的步骤是在V8引擎中进行编译和运行,主要流程是将JS代码解析成抽象语法树(AST),AST经过解释器(Ignition)转化为字节码,然后编译为机器码,最后在调用栈中进行执行来对DOM进行操作。
浏览器渲染前端的整个流程图如下:
![](/assets/blank.gif)
JS高级——浏览器运行前端项目的原理及流程相关推荐
- node.js require 自动执行脚本 并生成html,利用node.js实现自动生成前端项目组件的方法详解...
本文主要给大家介绍了关于利用node.js实现自动生成前端项目组件的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍: 脚本编写背景 写这个小脚本的初衷是,项目本身添加一个组件太 ...
- 运行前端项目之html
运行html三种方式: 直接双击运行 vscode中运行 idea中运行 1.直接双击index.html 注意:默认起始页面为index.html,如果命名为其它文件名也可以 2.在vscode中快 ...
- html js css如何关联_会html+css+js就能把前端项目发布到多个平台
在这篇文章中,小编将给大家分享如何让自己的前端代码发布到多个常用的平台. 看完这篇文章以后,你就知道了如何让你的前端代码发布到多个平台,如:安卓应用程序,小程序,iOS应用程序,Windows,Mac ...
- Linux nginx 安装 部署运行前端项目
(1)nginx介绍 Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器 [13] ,同时也提供了IMAP/POP3/SMTP服务.Nginx是由伊戈尔· ...
- MyEclipse2014用外部的浏览器运行web项目
首先还是先打开我们的MyEclipse 2014 编辑器 然后选择Windows选项 下的preferences(首选项)选项 然后在弹出的首选项设置界面 选择General选项 打开 然后选择Web ...
- 前端项目搭建部署全流程(一):搭建React项目
1.前言 前段时间突发一个想法,想尝试从零开始搭建一个React项目模板,发布到GitHub,再编写脚手架命令拉取模板以及编写脚本命令快速生成业务模块,然后再用这个模板结合之前的一套组件库,完成编译打 ...
- A065_运行前端_跨域_列表_删除
目录 内容介绍 1.运行后台管理前端项目 1.1.前端项目运行流程 1.1.1. 准备前端项目(使用父级模块) 1.1.2. 升级elmentui版本 1.1.3. 运行前端项目 1.1.4. 修改主 ...
- html5实现浏览器自动全屏,[JavaScript] 用html5 js实现浏览器全屏
项目中需要将后台浏览器的窗口全屏,也就是我们点击一个按钮要实现按F11全屏的 效果. 在HTML5中,W3C制定了关于全屏的API,就可以实现全屏幕的效果,也可以 让页面中的图片,视频等全屏目前只有g ...
- JS高级 之 深入浏览器的渲染原理
在浏览器中输入网址按回车后发生了什么, 其实,发生的事情很简单,主要有三大步 找到资源 下载资源 解析资源渲染到页面 目录 一.DNS解析,找到资源 1. 查询浏览器缓存 2. 查询操作系统缓存 4. ...
最新文章
- 给图片加上带版权的水印
- 深度学习最近发现详细分析报告
- 制药行业智能化发展现状趋势及建议
- linux基础学习7
- VS Code(Visual Studio Code)编辑器的常用设置
- pytorch 不同设备下保存和加载模型,需要指定设备
- 一个基于nodejs开发的微服务脚手架应用,架构和CRM WebUI很像
- lacp静态和动态区别_lacp静态与动态区别
- Bug : Bash on Ubuntu on Windows scp work on window but not in shell file
- Spring 3 MVC异常处理程序
- 在gitlab 中使用webhook 实现php 自动部署git 代码
- 宽容随和 不失勤恳 充满信心--对工作、生活的一些感悟
- python主要数据变量_python的数据类型和变量
- 【poj1284-Primitive Roots】欧拉函数-奇素数的原根个数
- JavaScript 物体的运动
- On intelligence by Jeff Hawkins
- 词法分析器(不讲武德java版)
- CNN——基于CNN的车牌号识别
- CuraEngine三维切片源码编译与解读
- 7款浏览器新标签页扩展让你的Chrome耳目一新
热门文章
- C语言:编写代码实现,模拟用户登录情景,并且只能登录三次。(只允许输入三次密码,如果密码正确则提示登录成,如果三次均输入错误,则退出程序。)
- 标致雪铁龙诊断软件diagbox 安装说明视频下载链接
- Java中类、抽象类、接口的联系与区别
- 【数值分析】插值法:拉格朗日插值、牛顿插值
- dota自走棋寻找不到服务器,《DOTA自走棋》服务器不对怎么办 服务器不对解决方法介绍...
- css 宽度为百分比, 高度和宽度相等的设置
- 2022年阿里全球数学竞赛中的集福活动(附代码解答)
- 1、几种进程间的通信方式
- np.arange()和 range()的用法及区别
- MaxCompute SQL示例解析