浏览器加载出网页文件信息以后,会第一步进行词法分析。
词法分析的过程也就是讲代码字符分解成为词法单元的过程。包括关键词,变量,运算,符号等的解析。会将代码字符分解为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)。
全局对象(GO): 全局对象(GO)是浏览器中的根对象,它是由浏览器在首次执行JavaScript代码时自动创建的。它是js执行过程中最顶层的作用域,他可以被全局作用域中所有的代码访问到。
可以简单理解为 window的作用域。
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,并赋值为undefined。
6. 形参与实参统一
最后进入运行阶段,从上到下
注: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 是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执行的队列机制。
分析:
第一个宏观任务以及 宏观任务中的微观任务栈 全部执行完毕,再去执行队列中的第二位宏观任务,打印console.log(4)
根据上面引发的自我思考
setTimeout(function(){console.log('a')},2000)
setTimeout(function(){console.log('b')},0)
//因为两个都是宏观任务,但是因为定时器的原因为异步,所以这个时候就是延顺到下一个,由时间去判断执行哪一个
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 是es6的新特性,它提供了for,if代码的结构方式。运行的基础是Promise,所以async函数必然返回的是Promise,
我们把所有返回的Promise的函数都可以认为是异步函数。
async function foo(val){
let a = 1;
return val + a
}
foo(1);
//输出 Promise {: 2}
所以
foo(1).then(function(e){console.log(e)}) //2
所以对于这里我们完全可以看
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 {} , (过2秒) 111
b不输出
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 {} ,(过2秒)b
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
当代码执行完成以后,浏览器会根据标记清除和内存压缩等算法,自动回收垃圾机制,通过标记和扫描程序不再使用的内存空间,并释放该空间以及内存碎片,以保持程序的内存使用效率。
总结一下:
浏览器V8引擎会解析全局代码为token单元,并根据规则生成语法树,然后扫描全局代码,进入代码的预编译过程,创建完整的执行上下文环境,然后顺序执行,执行过程根据执行方法的行为形成,排列组成执行栈的队列(也就是宏任务栈队列),进行顺序执行,最后执行完成后根据浏览器回收算法进行内存的释放。