一. 浏览器架构与渲染
1.1 在进程和线程上执行程序
进程可以看成一个应用的执行程序,而线程是存在于进程内部来执行任意部分程序的。
Chrome浏览器使用多个进程来隔离不同的网页,在Chrome中打开一个网页相当于起了一个进程,每个tab网页都有由其独立的渲染引擎实例。因为如果非多进程的话,如果浏览器中的一个tab网页崩溃,将会导致其他被打开的网页应用。另外相对于线程,进程之间是不共享资源和地址空间的,所以不会存在太多的安全问题,而由于多个线程共享着相同的地址空间和资源,所以会存在线程之间有可能会恶意修改或者获取非授权数据等复杂的安全问题。
如果你想看看你的 Chrome 里有多少进程,可以点击右上角选择更多工具,然后选择任务管理器。
1.2 浏览器组件
- 界面控件 – 包括地址栏,前进后退,书签菜单等窗口上除了网页显示区域以外的部分
- 浏览器引擎 – 查询与操作渲染引擎的接口
- 渲染引擎 – 负责显示请求的内容。比如请求到HTML, 它会负责解析HTML、CSS并将结果显示到窗口中
- 网络 – 用于网络请求, 如HTTP请求。它包括平台无关的接口和各平台独立的实现
- UI后端 – 绘制基础元件,如组合框与窗口。它提供平台无关的接口,内部使用操作系统的相应实现
- JS解释器(引擎) - 用于解析执行JavaScript代码
- 数据存储持久层 - 浏览器需要把所有数据存到硬盘上,如cookies,storage。新的HTML5规范规定了一个轻量级的数据库
web databasetips: chrome浏览器的渲染任务交给了线程,即每个tab页下有一个独立的渲染线程。其他浏览器则是所有tab页公用一个渲染进程。
1.3 浏览器中的线程
## GUI 渲染线程
GUI渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。在Javascript引擎运行脚本期间,GUI渲染线程都是处于挂起状态的,也就是说被冻结了.
## JavaScript引擎线程
JS为处理页面中用户的交互,以及操作DOM树、CSS样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。如果JS是多线程的方式来操作这些UI DOM,则可能出现UI操作的冲突;如果JS是多线程的话,在多线程的交互下,处于UI中的DOM节点就可能成为一个临界资源,假设存在两个线程同时操作一个DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果,当然我们可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,JS在最初就选择了单线程执行。
GUI渲染线程与JS引擎线程互斥的,是由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JavaScript线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致。当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行。由于GUI渲染线程与JS执行线程是互斥的关系,当浏览器在执行JS程序的时候,GUI渲染线程会被保存在一个队列中,直到JS程序执行完成,才会接着执行。因此如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。
## 定时触发器线程
浏览器定时计数器并不是由JS引擎计数的, 因为JS引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。
## 事件触发线程
当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。
## 异步http请求线程
在XMLHttpRequest在连接后是通过浏览器新开一个线程请求,将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到JS引擎的处理队列中等待处理。
1.4 渲染过程
渲染流程有四个主要步骤:
- 解析HTML生成DOM树和CSSOM树 - 渲染引擎首先解析HTML文档,生成DOM树和CSSOM树
- 构建Render树 - 合并DOM树与CSSOM树生成另外一棵用于渲染的树-渲染树(Render tree),
- 布局Render树 - 然后对渲染树的每个节点进行布局处理,确定其在屏幕上的显示位置
- 绘制Render树 - 最后遍历渲染树并用UI后端层将每一个节点绘制出来
1.5 回流、重绘、重排
回流(reflow):简单理解为重新布局。当浏览器发现某个部分发生了点变化影响了布局,需要倒回去重新渲染。reflow 会从 <html>这个 root frame 开始递归往下,依次计算所有的结点几何尺寸和位置。reflow 几乎是无法避免的。现在界面上流行的一些效果,比如树状目录的折叠、展开(实质上是元素的显示与隐藏)等,都将引起浏览器的 reflow。鼠标滑过、点击……只要这些行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引起它内部、周围甚至整个页面的重新渲染。通常我们都无法预估浏览器到底会 reflow 哪一部分的代码,它们都彼此相互影响着。设置了脱离文档流的元素会在一个单独的图层渲染,回流时在该图层范围内,故因脱离文档流的元素而引起的回流,代价较小。
重绘(repaint):改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变。
每次Reflow,Repaint后浏览器还需要合并渲染层并输出到屏幕上。所有的这些都会是动画卡顿的原因。Reflow 的成本比 Repaint 的成本高得多的多。一个结点的 Reflow 很有可能导致子结点,甚至父点以及同级结点的 Reflow 。在一些高性能的电脑上也许还没什么,但是如果 Reflow 发生在手机上,那么这个过程是延慢加载和耗电的。可以在csstrigger上查找某个css属性会触发什么事件。
回流的产生:
- 页面第一次渲染 在页面发生首次渲染的时候,所有组件都要进行首次布局,这是开销最大的一次回流。
- 浏览器窗口尺寸改变
- 元素位置和尺寸发生改变的时候
- 新增和删除可见元素
- 内容发生改变(文字数量或图片大小等等)
- 元素字体大小变化。
- 激活CSS伪类(例如::hover)。
- 设置style属性
- 查询某些属性或调用某些方法。比如说:
- offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight
- 除此之外,当我们调用getComputedStyle方法,或者IE里的currentStyle时,也会触发回流,原理是一样的,都为求一个“即时性”和“准确性”。
重绘的产生:
当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如
visibility、outline、背景色等属性的改变。
阻塞渲染
-
CSS 被视为渲染 阻塞资源 (包括JS) ,这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕,才会进行下一阶段。
-
JavaScript 被认为是解释器阻塞资源,HTML解析会被JS阻塞,它不仅可以读取和修改 DOM 属性,还可以读取和修改 CSSOM 属性。 存在阻塞的 CSS 资源时,浏览器会延迟 JavaScript 的执行和 DOM 构建。另外:
-
当浏览器遇到一个 script 标签时,DOM 构建将暂停,直至脚本完成执行。
-
JavaScript 可以查询和修改 DOM 与 CSSOM。
-
CSSOM 构建时,JavaScript 执行将暂停,直至 CSSOM 就绪。除非script设置了defer或async
所以,script 标签的位置很重要。实际使用时,可以遵循下面两个原则:
- CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源。
- JavaScript 应尽量少影响 DOM 的构建,否则,如不依赖dom,尽量设置defer或async。
二. js编译与执行机制
我们通常讲js归类为一门解释性语言,同时,它也是一门编译语言。只不过与传统语言不同,js的编译过程不是发生在构建前,而是代码在浏览器中执行的前一瞬间(微秒级别甚至更短)。
2.1 编译过程
-
作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。在编译过程中生成,分为全局作用域,函数作用域,块级作用域(ES6引入)。
-
执行上下文 :是在函数执行时确定的,每次函数执行都会产生执行上下文,JavaScript引擎会以栈的方式来处理它们,这个栈,我们称其为函数调用栈(call stack)。栈底永远都是全局上下文,而栈顶就是当前处于活动状态的正在执行的上下文,也称为活动对象。
-
this :指向执行上下文的指针。绑定方式分为默认绑定,隐式绑定,显示绑定,new绑定
-
变量提升 : var声明的变量,第一步,变量定义会被提升到当前作用域顶端。第二步,赋值undefined。
-
函数提升 : function声明的函数,直接剪切到当前作用域顶端。且提升等级更高(提升到var声明的变量之前)。 一旦js引擎中进入一段执行上下文,便会进行如下三步的编译过程:
1.分词/词法分析 这些代码块被称为词法单元(token)。例如,var a = 2;。这段程序通常会被分解成为下面这些词法单元:[var、a、=、2 、;],称之为词法单元流。
2.解析/语法分析 这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)
3.代码生成 将AST转换为可执行代码的过程称被称为代码生成。
js编译过程中,最直观的体现就是变量提升。也就是说变量提升是发生在代码执行前,编译过程中。 js引擎每一次遇到声明(var),它就把声明传到作用域来创建一个绑定。并为变量分配内存(undefined)。只是分配内存而不是把代码修改成声明提升。 在这之后,引擎每一次遇到赋值或者取值,它都会通过作用域查找绑定。如果在当前作用域中没有查找到就接着向上级作用域查找直到找到为止。在执行代码语句之前,解释器就已经从作用域中找到变量的值了。接着引擎生成CPU可以执行的机器码。 最后,代码执行完毕。 所以变量提升不过是执行上下文的游戏,而不是代码修改。
function test(){
console.log(1);
}
function init(){
console.log(2);
if(false){
function test(){
console.log(3);
}
}
test();
console.log(4);
}
init();
2.2 js执行机制
由于js设计之初只是为了提交表单,所以简单的设计为单线程语言。此外,如果js是多线程,那么不同的js文件同时操作dom会导致比较麻烦的问题。HTML5允许使用webworker开启新线程来处理任务,但其中不允许访问DOM和BOM,也是为了避免这个问题。
-
同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
-
当指定的事情完成时,Event Table会将这个函数移入Event Queue。
-
主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
-
上述过程会不断重复,也就是常说的Event Loop(事件循环)。
setTimeout延时执行
setTimeout(function(){
console.log(1)
},1000)
Sleep(5000)
function Sleep (time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
页面是否能继续响应
function foo(){
return Promise.resolve().then(foo)
}
foo()
function foo(){
setTimeout(foo,0)
}
foo()
事件循环是js实现异步的一种方法,也是js的执行机制。
2.3 js执行过程
-
发现有代码调用了一个函数
-
在执行这个function之前,创建一个执行上下文(execution context),也可以叫执行环境。
-
进入创建阶段(VO创建)
a. 初始化作用域链(scope chain)通过[Scope]属性指向外层的VO来进行指向外层的AO对象,那么这样就形成了作用域链
b. 创建变量函数(variable object / VO)
c. 创建参数对象(arguments object,传进来的参数),检查上下文,初始化其名字和值,以及建立引用对象的拷贝。
d. 扫描上下文中的函数声明
e. 为每一个扫描到的函数声明在VO中创建一个属性,命名为函数的名字,指向了存储空间中的对应函数。
f. 如果函数名称已经存在了,这个引用指针将被重写为新的这一个。
g. 扫描上下文中的变量声明
h. 为每一个扫描到的变量声明在VO中创建一个属性,命名为变量的名字,初始化值为undefined。
i. 如果变量名在内存中已经存在了,就跳过。
j. 决定上下文中this的指向。 -
执行阶段(VO => AO)
a. 执行/解释上下文中的function,为变量赋值
b. 代码按行执行
AO和VO的关系:
AO可以理解为VO的一个实例,也就是VO是一个构造函数,所以VO提供的是一个函数中所有变量数据的模板。
对于同一个函数分多次执行,那么里面的变量、形参和定义的函数肯定是不同的函数,所以每次执行都会产生一个AO对象,即AO是VO的一个实例,但是这个实例并不是new 出来的,而是在同一段执行代码执行的时候放进来的。
VO是不能访问的(除了全局上下文的VO可以间接访问),但是可以访问AO的成员(属性)。 VO和AO其实是一个东西,只是处于不同的执行上下文生命周期。AO存在于执行上下文位于执行上下文堆栈顶部(就是上边说的’当控制进入函数代码的执行上下文时’)的时期。再粗暴点,就是函数调用时,VO被激活成了AO。
三. 类型转换
3.1 不同类型相互比较的规则
-
如果 Type ( x ) 与Type ( y ) 相同,则
-
如果 Type ( x ) 未定义,则返回true。
-
如果 Type ( x ) 为 Null,则返回true。
-
如果 类型( x ) 是数字,那么
- 如果 x是NaN,则返回false。
- 如果 y是NaN,则返回false。
- 如果 x与 y是相同的 Number 值,则返回true。
- 如果 x为 +0且y为**-** 0,则返回true。
- 如果 x为**-** 0 且y为 +0,则返回true。
- 返回 假。
-
如果 Type ( x ) 是字符串,则如果x和 y是完全相同的字符序列(相同长度和对应位置的相同字符),则返回true 。 否则,返回 false。
-
如果 Type ( x ) 是 Boolean ,如果x和y都为true或都为false ,则返回 true。否则,返回 false。
-
如果x和y引用同一个对象,则 返回 true 。 否则,返回false。
-
-
如果 x为null且y 未定义,则返回 true。
-
如果 x 未定义且y为null ,则返回 true。
-
如果 Type ( x ) 是 Number 并且Type ( y ) 是 String,则
返回比较结果x == ToNumber ( y )。 -
如果 Type ( x ) 是 String 并且Type ( y ) 是 Number ,则
返回比较结果ToNumber ( x ) == y。 -
如果 Type ( x ) 是 String 或 Number 并且Type ( y ) 是 Object,则
返回比较结果x == ToPrimitive ( y )。 -
如果 Type ( x ) 是 Object 并且Type ( y ) 是 String 或 Number ,
则返回比较ToPrimitive ( x ) == y的结果。 -
返回 假。
3.2 引用数据类型的隐式类型转换
- 先调用对象的
Symbol.toPrimitive这个方法,如果不存在这个方法 - 再调用对象的
valueOf获取原始值,如果获取的值不是原始值 - 再调用对象的
toString把其变为字符串 - 最后再把字符串基于
Number()方法转换为数字
最后来一道吊吊的面试题
let arr = [27.2, 0, '0013', '14px', 123];
arr = arr.map(parseInt);
console.log(arr);
如何使for of 可遍历对象
let result = 100 + true + 21.2 + null + undefined + 'Tencnet' + [] + null + 9 + false;
实现 a == 1 && a == 2 && a == 3