一、认识浏览器

二、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进行操作。

浏览器渲染前端的整个流程图如下:

浏览器运行前端项目整体流程图

JS高级——浏览器运行前端项目的原理及流程相关推荐

  1. node.js require 自动执行脚本 并生成html,利用node.js实现自动生成前端项目组件的方法详解...

    本文主要给大家介绍了关于利用node.js实现自动生成前端项目组件的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍: 脚本编写背景 写这个小脚本的初衷是,项目本身添加一个组件太 ...

  2. 运行前端项目之html

    运行html三种方式: 直接双击运行 vscode中运行 idea中运行 1.直接双击index.html 注意:默认起始页面为index.html,如果命名为其它文件名也可以 2.在vscode中快 ...

  3. html js css如何关联_会html+css+js就能把前端项目发布到多个平台

    在这篇文章中,小编将给大家分享如何让自己的前端代码发布到多个常用的平台. 看完这篇文章以后,你就知道了如何让你的前端代码发布到多个平台,如:安卓应用程序,小程序,iOS应用程序,Windows,Mac ...

  4. Linux nginx 安装 部署运行前端项目

    (1)nginx介绍         Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器 [13]  ,同时也提供了IMAP/POP3/SMTP服务.Nginx是由伊戈尔· ...

  5. MyEclipse2014用外部的浏览器运行web项目

    首先还是先打开我们的MyEclipse 2014 编辑器 然后选择Windows选项 下的preferences(首选项)选项 然后在弹出的首选项设置界面 选择General选项 打开 然后选择Web ...

  6. 前端项目搭建部署全流程(一):搭建React项目

    1.前言 前段时间突发一个想法,想尝试从零开始搭建一个React项目模板,发布到GitHub,再编写脚手架命令拉取模板以及编写脚本命令快速生成业务模块,然后再用这个模板结合之前的一套组件库,完成编译打 ...

  7. A065_运行前端_跨域_列表_删除

    目录 内容介绍 1.运行后台管理前端项目 1.1.前端项目运行流程 1.1.1. 准备前端项目(使用父级模块) 1.1.2. 升级elmentui版本 1.1.3. 运行前端项目 1.1.4. 修改主 ...

  8. html5实现浏览器自动全屏,[JavaScript] 用html5 js实现浏览器全屏

    项目中需要将后台浏览器的窗口全屏,也就是我们点击一个按钮要实现按F11全屏的 效果. 在HTML5中,W3C制定了关于全屏的API,就可以实现全屏幕的效果,也可以 让页面中的图片,视频等全屏目前只有g ...

  9. JS高级 之 深入浏览器的渲染原理

    在浏览器中输入网址按回车后发生了什么, 其实,发生的事情很简单,主要有三大步 找到资源 下载资源 解析资源渲染到页面 目录 一.DNS解析,找到资源 1. 查询浏览器缓存 2. 查询操作系统缓存 4. ...

最新文章

  1. 给图片加上带版权的水印
  2. 深度学习最近发现详细分析报告
  3. 制药行业智能化发展现状趋势及建议
  4. linux基础学习7
  5. VS Code(Visual Studio Code)编辑器的常用设置
  6. pytorch 不同设备下保存和加载模型,需要指定设备
  7. 一个基于nodejs开发的微服务脚手架应用,架构和CRM WebUI很像
  8. lacp静态和动态区别_lacp静态与动态区别
  9. Bug : Bash on Ubuntu on Windows scp work on window but not in shell file
  10. Spring 3 MVC异常处理程序
  11. 在gitlab 中使用webhook 实现php 自动部署git 代码
  12. 宽容随和 不失勤恳 充满信心--对工作、生活的一些感悟
  13. python主要数据变量_python的数据类型和变量
  14. 【poj1284-Primitive Roots】欧拉函数-奇素数的原根个数
  15. JavaScript 物体的运动
  16. On intelligence by Jeff Hawkins
  17. 词法分析器(不讲武德java版)
  18. CNN——基于CNN的车牌号识别
  19. CuraEngine三维切片源码编译与解读
  20. 7款浏览器新标签页扩展让你的Chrome耳目一新

热门文章

  1. C语言:编写代码实现,模拟用户登录情景,并且只能登录三次。(只允许输入三次密码,如果密码正确则提示登录成,如果三次均输入错误,则退出程序。)
  2. 标致雪铁龙诊断软件diagbox 安装说明视频下载链接
  3. Java中类、抽象类、接口的联系与区别
  4. 【数值分析】插值法:拉格朗日插值、牛顿插值
  5. dota自走棋寻找不到服务器,《DOTA自走棋》服务器不对怎么办 服务器不对解决方法介绍...
  6. css 宽度为百分比, 高度和宽度相等的设置
  7. 2022年阿里全球数学竞赛中的集福活动(附代码解答)
  8. 1、几种进程间的通信方式
  9. np.arange()和 range()的用法及区别
  10. MaxCompute SQL示例解析