1.组成原理基础复习
微命令:
控制门电路的开关,一个微命令只能控制一个门电路。
举例:控制总线内容是否能够进入寄存器,存储器内容是否能够进入总线等待。
微指令:
由[操作码] + [测试码] + [下一条地址]构成。一条微指令能够控制多条微命令,执行连贯动作。
举例:读取存储器中xxx地址的数据并送入寄存器A,可以通过操纵多条微命令控制门电路来完成。
机器指令:
由[操作码字段] + [地址码字段] 构成,地址码字段可以有多个。每条机器指令都对应着一个由多条微指令构成的微程序。
举例:二地址码的加指令,首先需要从内存中读取数据的微指令,然后需要控制算数逻辑单元运算的微指令,需要把结果存储的微指令等等。
汇编语言:
建立在机器指令基础上,机器指令用二进制表示难以实际使用,汇编语言是给机器指令提供了助记符。实际上汇编语言还是机器指令。
举例:add a b就可以代表机器指令中的加指令,mov x y就可以表示机器指令中的移动指令。
高级程序语言:
建立在汇编语言基础上,把汇编语言的多条操作汇聚成一条操作。
举例:a + b就可能涉及到十几条汇编指令,先读取,再移动到寄存器,再运算等待。
2.解释器
介绍
-
解释器的输入是代码,将代码一行一行执行,输出是代码的结果
-
解释器结构:
- 使用字节码的解释器:源代码 -> 字节码 -> 执行
- 带有编译器的解释器:源代码 -> 编译器 -> 目标代码 -> 执行
3.编译器
介绍
-
编译器是把源代码转化成机器可执行的代码
-
编译器结构:
- 前端:源代码 -> 中间代码(可以理解成虚拟机使用的机器指令)
- 后端:中间代码 -> 机器码(上面所述的机器指令)
4.JavaScript运行过程
了解发生在词法分析之前的事件:
参考"浏览器渲染原理":浏览器渲染进程下的主线程以字节流的形式收到HTML文件,首先根据编码格式将字节流处理成字符流。之后HTML Parser和Preload Scanner开始工作。HTML Parser遇到脚本后被阻塞,JavaScript引擎开始解析脚本(这里指同步脚本)。JavaScript引擎将会根据收到的字符流完成后续过程。
1. 词法分析
JavaScript引擎将字符流(char stream)转换为记号流(token stream),将字符转换成可辨识的词法单元token。下面的例子是"var answer = 1 + 2"
[
{
"type": "Keyword",
"value": "var"
},
{
"type": "Identifier",
"value": "answer"
},
{
"type": "Punctuator",
"value": "="
},
{
"type": "Numeric",
"value": "1"
},
{
"type": "Punctuator",
"value": "+"
},
{
"type": "Numeric",
"value": "2"
}
]
注:生成器地址:https://esprima.org/demo/parse.html#
2. 语法分析
Javascript引擎将token转换成AST(Abstract Syntax Tree)抽象语法树。下面的例子是"var answer = 1 + 2"
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "answer"
},
"init": {
"type": "BinaryExpression",
"operator": "+",
"left": {
"type": "Literal",
"value": 1,
"raw": "1"
},
"right": {
"type": "Literal",
"value": 2,
"raw": "2"
}
}
}
],
"kind": "var"
}
],
"sourceType": "script"
}
注:生成器地址:https://esprima.org/demo/parse.html#
3. 预解析
JavaScript代码可以看做是不同的函数,在对应函数运行之前会有下列操作。
1. 创建作用域对象
每个作用域都关联一个作用域对象,作用域对象的创建在"浏览器渲染原理"里提到了详细过程
创建作用域对象
在作用域中寻找var声明的变量和形参声明的变量,把它们作为属性加入作用域对象,属性值为undefined
将实参值带入,作用域对象中形参属性的属性值为实参值
将声明式函数也作为作用域对象的属性,属性值为undefined
2. 创建作用域链
概念:
作用域链可以看做是一个链表,用于变量的查询。链表的第一个元素(非头结点)是当前函数的作用域,第二个元素是当前函数的父作用域,第三个元素是当前函数的祖父作用域,以此类推直到全局作用域。
举例:
在此举一个异步迭代器的手动迭代的例子(之前异步编程总结忘记写了)
// 使用异步迭代器使得串行Promise实现方法更多样,同时也给了流读写支持 ;(async function() { let p1 = new Promise(resolve => setTimeout(resolve.bind(this, "p1"), 3000)) let p2 = new Promise(resolve => setTimeout(resolve.bind(this, "p2"), 2000)) let ar = [p1, p2] function asyncIteratorMaker(ar) { let index = 0 return { [Symbol.asyncIterator]() { return this }, next() { if(index === ar.length) return new Promise(resolve => resolve({ done: true, value: undefined })) else if(ar[index] instanceof Promise) return new Promise((resolve, reject) => { ar[index ++].then(res => { resolve({ done: false, value: res }) }, reject) }) else return new Promise(resolve => resolve({ done: false, value: ar[index ++]})) } } } ;(function autoMaker(asyncIter) { let result = asyncIter.next(); ;(function next() { result.then(res => { if(res.done !== true) { console.log(res.value) result = asyncIter.next() next() } }) })(); })(asyncIteratorMaker(ar)); })();
描述:
讨论对象是异步迭代器自动执行函数"autoMaker"中的result.then方法中的第一个参数"onFulfilled"函数。假如此时要执行该函数并且第一步作用域对象已经创建完毕,那么该创建作用域链。上图给出了onFulfilled函数的作用域链,onFulfilled函数的作用域对象为链表第一个元素,一直向后添加父作用域对象,直到全局作用域对象。
问:
onFulfilled函数中使用的result变量,是来自哪里?
答:
在创建onFulfilled函数的作用域对象时由于result并不是形参,var声明,函数声明。同时也不是执行时运行的let或const声明语句的到的变量。当代码运行时发现result并不是上述几种情况,那么就会沿着上图的作用域链向上查找,发现到达next函数的作用域对象时存在该变量,那么就使用之。
2021/11/17补充例子–1
var x = 1; function f(x, y = function () { x = 3; console.log(x); }) { console.log(x) var x = 2 y() console.log(x) } f() console.log(x) // 输出结果是undefined 3 2 1 // 第一次作答我的答案是undefined 3 2 3
解答:
给函数参数赋初始值,在上面提到的预解析阶段实际就是提供了实参。函数中在执行y()时,会执行y函数中x = 3这个操作,之前根据权威指南所说“不论在函数或块中嵌套多深,只要是不带声明符声明变量,那么直接会成为全局变量”所以此处认为y()执行完的后果是创建了全局变量属性x。但是实际上当执行x = 3时会沿着作用域链向上查找,如果没有x这个变量才会创建之,而这里在沿着作用域链查找时发现f的作用域中有x这个参数,于是将其在f的作用域中的值修改为3.
2021/11/17补充例子–2
var x = 1; function f(x, y = function () { x = 3; console.log(x); }) { console.log(x) y() console.log(x) } f() console.log(x) // 输出结果是 undefined 3 3 1
解答:
有了第一题的基础解决此情况就轻松多了,根据上述提到的预解析过程,第一个输出为undefined毋庸置疑。在执行y()时,x = 3把f作用域中的x的值修改为了3,第二个输出为3,第三个输出查找f作用域中的x,输出为3,最后输出为1毋庸置疑。
2021/11/17补充例子–3
var x = 1; function f(xx, y = function () { x = 3; console.log(x); }) { console.log(x) var x = 2 y() console.log(x) } f() console.log(x) // 输出是undefined 3 2 3
2021/11/17补充例子–4
var x = 1; function f(x = 4, y = function () { x = 3; console.log(x); }) { console.log(x) var x = 2 y() console.log(x) } f() console.log(x) // 输出是4 3 2 1
3. 绑定this
解释:
this在预解析阶段就已经绑定,在运行时this的值不会发生变化,也不允许被修改。
非严格模式下this的绑定:
箭头函数中的this永远绑定为父作用域的this值
函数中被对象调用时this绑定为对象
函数未被对象调用时this绑定为全局对象
构造函数中this绑定为新创建的对象
使用apply,call,bind绑定的this
严格模式下this的绑定:
箭头函数中的this为undefined
函数被对象调用时this绑定为对象
函数未被对象调用时的this为undefined
构造函数中的this绑定为新创建的对象
使用apply,call,bind绑定的this
事件处理函数中的this绑定为事件目标对象 (不论是严格模式还是非严格模式都是如此,事件处理程序和事件监听器来处理事件时,函数都是在事件目标对象的作用域下执行)
内联事件处理函数比较特殊,会给处理语句外包裹一个函数(见犀牛书),并带有event参数。这个函数默认不是严格模式,默认this绑定到事件目标对象。如果手动在其中开启严格模式,那么将会绑定为undefined(参考mdn)
4. 执行阶段
JavaScript在执行时是使用解释器一行一行执行,在v8引擎中采用了JIT技术混合编译器和解释器来优化这种执行方式。
5.JIT优化
参考:https://hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/
1.JIT优化介绍
- (场景介绍) 假设有以下场景
function sumHandler(array) {
let sum =0
for(let i = 0; i < array.length; i++)
sum += array[i]
return sum
}
正常来说,在做完词法分析,语法分析等一系列操作后,上述代码将交付解释器一行一行地解释执行。每一次解释执行都是将语句编译后执行。在上述情况中这种做法的效率将会大打折扣,每一次循环解释器都需要重新编译执行相同的语句,而对比之下编译器工作时只需要编译一次。
- (JIT引入) JIT专门针对这种重复语句进行优化,设计思路是将重复语句进行编译储存,用时不再编译。JIT全称是Just In Time,是混合使用编译器和解释器进行优化。
2.基线编译器 (BaseLine Compiler)
-
(warm标记) 运行时发现某些代码段运行次数很多,会被标记为"warm",这时代码段将会交付给基线编译器。
-
(根据决策树编译存储) 根据变量不同的类型的情况进行编译并存储,如下图。总共变量类型有8中可能的情况(情况可以用决策树表示),此时基线编译器会根据每一种情况进行一次编译,把行号和变量类型作为索引,通过这个索引可以找到对应情况的编译代码。注意!!!基线编译器编译时间不会太久,如果变量太多,可能的情况将会以指数级增长,对于warm标记的代码段花过量时间编译就不划算了。所以情况复杂时可能不会存储所有的编译结果
-
(优化执行) 再次执行时通过行号和变量类型作为索引,去寻找对应的编译过的代码。
3.优化编译器 (Optimizing Compiler)
-
(hot标记) 运行时发现某个代码段执行次数过多,比标记为warm的次数还要多的多时,就会被标记为hot,这时代码会被交付给优化编译器。
-
(根据决策树推测编译存储) 由于决策树分支很多,优化编译器会推测其中的一种情况作为理想情况。之后根据这种情况进行编译存储,并且会把整个函数进行编译存储。这样的推测可能不适合所有优化情况,但是能够让80%的情况得到不错的优化已经是巨大的成功。
- (优化执行) 优化编译器编译了整个函数,在下次执行时,在函数入口就会进行类型检查,如果类型不符合优化编译器推测类型,如果类型和行号符合,那么直接使用编译结果。如果不匹配,那么会进行"去优化"操作。此时将会回退到基线编译器,如果基线编译器没有存储决策树的所有可能情况(决策树分支过多),并且此时的变量类型的情况恰好基线编译器没有提前编译存储,那么继续将会回退到原始的解释器重新编译执行阶段。
注意:优化编译器可能会陷入"优化-去优化"的恶性循环,造成效率低下。实际应用中会规定去优化的次数,比如如果去优化次数超过10次,那么就不再使用优化编译器。