浅谈浏览器V8引擎的解析过程,理解js执行顺序-[基础篇]

浏览器V8的解析过程

一、词法分析

浏览器加载出网页文件信息以后,会第一步进行词法分析。
词法分析的过程也就是讲代码字符分解成为词法单元的过程。包括关键词,变量,运算,符号等的解析。会将代码字符分解为token也就是一个词法单元。每一个单元对应一个token类型和token值。
基本工作流程:
- 读取代码,分解为一个个字符单元
- 遍历所有字符单元判断是否为开始字符(会跳过空格,换行,空白符等),如果是则为token首字符,否则将跳过该字符,直到读取玩结束字符(结束标志,可以包括空格,括号,分号等)
- 将token转为token对象,添加到token流中
- 不断循环上面步骤,遍历完所有数据
- 最后用于组成语法树
列:

let a = 1;

// 分解token流:(注意空格没有存为token流)
// 处理token分为关键字、标识符、运算符、数字和分号 ,最终会组合成语法树
Token(type:'keyword',value:'let');
Token(type:'identifier',value:'a');
Token(type:'operator',value:'=');
Token(type:'number',value:'1');
Token(type:'delimiter',value:';');

二、语法分析

语法分析,就是浏览器根据token流分析组成语法树的过程
语法树是一个由节点构成的树形结构,每个节点代表一个语言结构,例如函数调用、赋值语句、if语句等等。
基本工作流程:
- 从起始位置开始遍历所有以及形成的token流
- 将所有符合语法结构的token单元加入到语法树的节点之中
- 根据语法规则,将每一个循环分析完毕的节点,根据语法规则绑定为父子关系,也就是创建子节点,或者归纳到该节点的父节点下
- 不断循环上述操作,直到构建完整的语法树
列:

function add(a, b) {
  return a + b;
}

// 树结构可以用js代码大体可以表示:
{
  type: 'FunctionDeclaration',
  id: {
    type: 'Identifier',
    name: 'add'
  },
  params: [
    {
      type: 'Identifier',
      name: 'a'
    },
    {
      type: 'Identifier',
      name: 'b'
    }
  ],
  body: {
    type: 'BlockStatement',
    body: [
      {
        type: 'ReturnStatement',
        argument: {
          type: 'BinaryExpression',
          operator: '+',
          left: {
            type: 'Identifier',
            name: 'a'
          },
          right: {
            type: 'Identifier',
            name: 'b'
          }
        }
      }
    ]
  }
}

简单来讲就是从开始到结束的一个代码表示。用key,value的形式,描述函数体,节点等操作

三、预解析阶段 (很重要)

预解析就是浏览器执行代码前的一项工作,预解析主要工作是查找和声明变量、函数以及它们的作用域,并将它们保存在内存中。
js执行前的预编译过程主要会产生 两个环境对象(AO和GO),也就是活动对象(AO)和全局对象(GO)。

  1. 全局对象(GO): 全局对象(GO)是浏览器中的根对象,它是由浏览器在首次执行JavaScript代码时自动创建的。它是js执行过程中最顶层的作用域,他可以被全局作用域中所有的代码访问到。
    可以简单理解为 window的作用域。

  2. AO对象:函数执行前的一个阶段所产生的对象,包含了函数还行过程中变量、函数参数和内部函数,简单来讲就是函数执行的前一刻,会将扫描的代码进行规整,然代码按照规则执行。
    过程:

    • 创建AO对象 比如:AO = {}

    • 收集所有的内部变量和函数,根据声明顺序,为变量和函数创建标识符,并在AO对象中建立对应的属性/方法,同时赋一个默认值undefined。如: AO= {a:undefined}

    • 初始化函数参数为AO对象的一个属性,同时赋参数传递进来的值。 (形参,实参统一)
      a 值分为 函数值和非函数值,先是赋予非函数值,然后最后查找函数值进行赋值

    • 如果参数的实际传值小于形参的数量,在AO对象中创建对应数量的undefined属性。
      列:

function creatFun(name) {
  let message = "Hello, ";
  function initFun() {
    let person = name.toUpperCase();
    console.log(message + person);
  }
  initFun();
}
creatFun("John");
//解析过程
1. 创建一个空的AO (函数的AO对象);
2. 创建一个参数变量name , message , initFun将其添加到AO中,并赋值为undefined ;
3. 然后形参与实参统一,并赋值;
4. 创建函数initFun和函数内部的AO ;
5. 在initFun的AO中创建一个变量person,并赋值为undefined6. 形参与实参统一
最后进入运行阶段,从上到下

注:es5 中也是一样,实参统一的时候,函数是最后进行查找,所以最后会是函数覆盖
如:
function creatFun(name){
    console.log(msg); 	// 这里的是函数
	var msg = 'name';
	function msg(){
		console.log('hellow')
	}
	console.log(msg); //这里msg是 name
}
creatFun();

四、执行阶段

经过函数预编译后,上下文会被引擎归纳,放入执行的上下文中。然后按顺序执行,我们这里主要探讨一下宏观任务和微观任务的执行过程。

首先什么是宏观任务和微观任务
引用winter老师的概念: 我们把宿主发起的任务称为宏观任务,把**javascript引擎发起的任务称为微观任务*。

个人简单理解就是:宏观任务就是宿主环境的方法,微观任务就是js语言本身的方法。
javascript引擎执行的时候是一种事件循环的执行,在底层的C/C++代码中,这个时间循环是跑在独立线层中的循环,我们用伪代码表示,大概就是这样的:

while(true){
	r = wait();
	execute(r);
}

可以看到整个循环做的事情基本上就是反复的‘等待 - 执行’。当然,实际代码中没有那么简单,还有其他的判断。

所以理解为就是宏观任务就相当于一个事件的循环,等待着执行。
而宏观任务中,javascript 的Promise 还会产生异步代码,javascript必须保证这些代码在一个宏观任务中完成,
因此每个宏观任务又包含了一些微观任务,这些微观任务以列队的形式等待执行。

Promise

promise 是javascript语言提供的一种标准化的异步管理方式。
他的总体思想是需要进行io,等待或者其他异步操作的函数,不返回真实结果,而返回一个’承诺’,函数调用方可以在合适的时机,
选择等待这个承诺兑现(Promise的then方法的回调)
首先基本用法:

 var r = new Promise(function(resolve, reject){
    console.log("a");
    resolve()
  });
  setTimeout(()=>console.log("d"), 0)
  r.then(() => console.log("c"));
  console.log("b")
  
  执行结果: a , b , c

分析:r是一个promise 对象,进入console.log(b)之前,r已经拿到了resolve,但是因为异步操作的问题,所以c 无法出现在b之前

promise 与 setTimeout的合用

var r = new Promise(function(resolve, reject){
    console.log("a");
    resolve()
  });
  setTimeout(()=>console.log("d"), 0)
  r.then(() => console.log("c"));
  console.log("b")
  //结果是: a , b , c ,d

分析:首先我们分析这里有多少个宏观任务,我们上面提到过,宏观任务的定义,和javascript的执行是队列执行,前面没执行完后面需要等待。
首先这里有两个宏观任务:
第一个宏观任务包括了console.log(a) 和console.log(b) 以及宏观里的微观console.log©
第二个宏观任务也就是浏览器宿主发起的setTimeout,它包含的console.log(d)
这里就不难理解输出的结果了

下面收集了知乎的一道题

  setTimeout(function(){console.log(4)},0); 
  new Promise(function(resolve){ 
    console.log(1) 
    for( var i=0 ; i<10000 ; i++ ){
       i==9999 && resolve() 
    } 
    console.log(2) 
  }).then(function(){ 
    console.log(5) 
  }); 
  console.log(3);
  
 // 输出结果 1 ,2 ,3 ,5 ,4

首先想想我们上面提到的宏观和微观任务的分辨,和javascript执行的队列机制。
分析:

  1. 这道题有两个宏观任务,第一个为主环境默认的,第二个为setTimeout
  2. 根据微观任务一定先与宏观任务执行的特点,和队列机制 顺序执行的特点去分析
    首先执行 console.log(1) 然后是 console.log(2) 再是onsole.log(3),两则执行完以后,再去执行异步的resolve() 也就是then中的 console.log(5)

第一个宏观任务以及 宏观任务中的微观任务栈 全部执行完毕,再去执行队列中的第二位宏观任务,打印console.log(4)

根据上面引发的自我思考

  1. 宏观任务是存储在 任务队列中的按照顺序的方式去执行,如果碰到异步的宏观则会调到下一个进行执行(类似多线程)
setTimeout(function(){console.log('a')},2000)
setTimeout(function(){console.log('b')},0)
//因为两个都是宏观任务,但是因为定时器的原因为异步,所以这个时候就是延顺到下一个,由时间去判断执行哪一个
  1. 微观任务是存储是在执行栈中的,也是按照顺序依次执行(这是由javascript特性所决定的),微观任务一定是优于宏观任务执行的。
  2. 最初页面预编译的时候会将,宏观任务和微观任务进行分别的依次的储存,以入栈的形式。
  3. 在执行过程中,微观任务中包含了宏观任务时,会将宏观任务入栈到 任务队列中,然后依次按照任务队列进行执行
setTimeout(function(){console.log(4)},0); 
Promise.resolve().then(function(){
	  console.log(1) 
	 setTimeout(function(){console.log(2)},0)
	}).then(function(){ 
	  console.log(5) 
})
console.log(3);
  
此时输出:3 ,1 ,5 ,4 ,2
分析:
首先预编译将宏观任务 和 微观任务进行 区分,
1. 然后执行第一个宏观任务的 微观任务:
console.log(3);
console.log(1);->因为then的异步,所以放在第二位
到了这里发现了一个宏观任务 setTimeout ,将这个 宏观任务放入到 任务队列中,此时队列已经有了一个setTimeout,所以这个放入到它的下面一位
console.log(5) 
2. 执行完第一个以后,执行任务队列的第二个宏观任务
setTimeout(function(){console.log(4)},0); 
3.执行完第二个以后,执行任务队列的第三个宏观任务
 setTimeout(function(){console.log(2)},0)

async / await

async / await 是es6的新特性,它提供了for,if代码的结构方式。运行的基础是Promise,所以async函数必然返回的是Promise,
我们把所有返回的Promise的函数都可以认为是异步函数。

  1. 自动将常规函数转换成Promise,返回值也是一个Promise对象
async function foo(val){
   let a = 1;
	return val + a
}
foo(1);
//输出  Promise {<resolved>: 2}
所以
foo(1).then(function(e){console.log(e)})  //2

所以对于这里我们完全可以看
  1. 只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数
  2. 异步函数内部可以使用await

await

  1. await 放置在Promise调用之前,await 强制后面点代码等待,直到Promise对象resolve,得到resolve的值作为await表达式的运算结果
function sleep(duration) {
    return new Promise(function(resolve, reject) {
        setTimeout(()=>{console.log('111')},duration);
    })
}
async function foo(){
    console.log("a")
    await sleep(2000)
    console.log("b")
}
foo();  //输出a ,Promise {<pending>} , (过2秒) 111
b不输出
  1. await只能在async函数内部使用,用在普通函数里就会报错
function sleep(duration) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve,duration);
    })
}
async function foo(){
    console.log("a")
    await sleep(2000)
    console.log("b")
}
foo();
//输出a , Promise {<pending>} ,(过2秒)b

  1. 重点很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。
    await后面的函数会先执行一遍,然后就会跳出整个async函数来执行后面js栈(后面会详述)的代码。
    等本轮事件循环执行完了之后又会跳回到async函数中等待await。
function foo1(){
	console.log('111')
}
async function foo(val){
   let a = 1;
	console.log('000');
   await foo1();
   console.log('222'); 
}
foo(1) 
console.log('333');

打印:000 111 333 222

接下来做一道题结束。

这是一道收集来的面试题        来源:网上流传的前端面试题

async function async1() {
    console.log('async1 start');
    await async2();
    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

  1. 首先有两个宏观任务 1.全局环境(自我认知) 2.setTimeout
  2. 所以第一个执行 console.log(‘script start’); 再执行 async1();
  3. 接下来就会打印 console.log(‘async1 start’); 立马去执行一次await的函数 打印console.log(‘async2’);
  4. 然后跳出执行其他js代码 也就是 下面new Promise 打印 console.log(‘promise1’);
  5. 然后因为异步所以打印console.log(‘script end’);
  6. 再跳转回去async1 里面 执行console.log(‘async1 end’);
  7. 再执行then的方法 console.log(‘promise2’);
  8. 最后执行第二个宏观任务 setTimeout 打印 console.log(‘setTimeout’);

五、回收阶段

当代码执行完成以后,浏览器会根据标记清除和内存压缩等算法,自动回收垃圾机制,通过标记和扫描程序不再使用的内存空间,并释放该空间以及内存碎片,以保持程序的内存使用效率。

总结一下:
浏览器V8引擎会解析全局代码为token单元,并根据规则生成语法树,然后扫描全局代码,进入代码的预编译过程,创建完整的执行上下文环境,然后顺序执行,执行过程根据执行方法的行为形成,排列组成执行栈的队列(也就是宏任务栈队列),进行顺序执行,最后执行完成后根据浏览器回收算法进行内存的释放。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值