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等。它实现ECMAScriptWebAssembly,并可在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); //undefined

var str = 'code'

console.log(str); //code

function foo() {
  var age = 10
  console.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); //undefined

var str = 'code'

console.log(str); //code

foo() //undefined或报错? 答案:正常执行,打印出10
function foo() {
  var age = 10
  console.log(age);
}

foo() //10

        答案: foo函数会正常执行,原因是在全局预解析时,GO对象中会记录foo函数,并且指向一块内存空间;当foo函数执行时,同样会创建一个函数指向上下文FEC,并压入调用栈ECS中;接着进行函数预解析从上往下执行函数体中的代码。(流程和案例一的函数执行流程一样)

        案例二中代码执行流程只是比案例一中代码流程多执行一次foo函数,其他流程不变。

   2.3 案例三:全局定义foo函数,foo函数中定义bar函数,并在foo函数中调用bar函数

        提问:foo函数执行后,两次打印结果分别是多少?

var age = 10

function foo() {
  var age = 20
  console.log(age);//输出? 20

  function 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 = 10

function foo() {
  var age = 20
  console.log(age);//输出? 20
  bar()
}
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进行操作。

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值