参考文献:
***** 浏览器渲染机制:https://segmentfault.com/a/1190000014018604
前言概念
- 同步:一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去。
- 异步:进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。
- 进程:狭义上,就是正在运行的程序的实例。广义上,进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
- 线程:线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位。指运行中的程序的调度单位。
- 单线程:单线程在程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行。单线程就是进程里只有一个线程。
- 多线程:在单个程序中同时运行多个线程完成不同的工作,称为多线程。
浏览器(多进程)
- 浏览器是多进程的,每打开一个Tab页,就相当于创建了一个独立的浏览器进程。
浏览器包含的进程:
Browser进程
浏览器的主进程(负责协调,主控),只有一个,作用有:
- 负责浏览器的界面显示,与用户交互,如前进,后退等
- 负责各个页面的管理,创建和销毁其它进程
- 将Rendered进程得到的内存中的Bitmap,绘制到用户界面上
- 网络资源的管理,下载
第三方插件进程
每种类型的插件对应一个进程,仅当使用该插件时才创建。
GPU进程
最多一个,用于3D绘制等。
浏览器渲染进程(浏览器内核)
(Render进程,内部是多线程的):默认每个Tab页面一个进程,互不影响。主要作用为:
- 页面渲染
- 脚本执行
- 事件处理等
浏览器组成结构
- 细分
外壳shell:
User Interface(用户界面)、Browser engine(浏览器引擎)、Networking(网络)、UI Backend(UI 后端)、Date Persistence(数据持久化存储)
- 用户界面 -包括地址栏、后退/前进按钮、书签目录等,也就是你-所看到的除了页面显示窗口之外的其他部分
- 浏览器引擎 -可以在用户界面和渲染引擎之间传送指令或在客户端本地缓存中读写数据等,是浏览器中各个部分之间相互通信的核心
- 网络 -用来完成网络调用或资源下载的模块
- UI 后端 -用来绘制基本的浏览器窗口内控件,如输入框、按钮、单选按钮等,根据浏览器不同绘制的视觉效果也不同,但功能都是一样的。
- 数据存储 -浏览器在硬盘中保存 cookie、localStorage等各种数据,可通过浏览器引擎提供的API进行调用
浏览器内核:
- 渲染引擎 :解析DOM文档和CSS规则并将内容排版到浏览器中显示有样式的界面,也有人称之为排版引擎,我们常说的浏览器内核主要指的就是渲染引擎
- JS引擎 :用来解释执行JS脚本的模块,如 V8 引擎、JavaScriptCore
之浏览器内核(单进程多线程)
谷歌,safari:webkit
常见浏览器及内核
一个进程
- 一个进程由多个线程组成
- 打开任务管理器,可以看到每个进程的内存资源信息以及cpu占有率。所以进程是cpu资源分配的最小单位(系统会给它分配内存),线程是cpu调试的最小单位
两个引擎(js引擎与渲染引擎)
各个主流浏览器的JS引擎和渲染引擎
由于js引擎越来越独立,内核就倾向于只指渲染引擎。
多个线程
- 浏览器是多进程的,每打开一个Tab页,就相当于创建了一个独立的浏览器进程。
浏览器进程包括的线程有:
- GUI渲染线程 负责渲染浏览器界面,解析HTML,CSS,构建DOM树
- JS引擎线程 也称为JS内核,负责解析Javascript脚本,运行代码。
- 事件触发线程 将对应任务添加到事件线程中,当事件符合触发条件被触发事件触发时才执行
- 定时触发器线程 传说中的setInterval与setTimeout所在线程
- 异步http请求线程 在XMLHttpRequest在连接后通过浏览器新开一个线程请求
线程之间的关系
GUI渲染线程与JS引擎线程互斥
- 因为JS引擎可以修改DOM树,那么如果JS引擎在执行修改了DOM结构的同时,GUI线程也在渲染页面,那么这样就会导致渲染线程获取的DOM的元素信息可能与JS引擎操作DOM后的结果不一致。
- 为了防止这种现象,GUI线程与JS线程需要设计为互斥关系,当JS引擎执行的时候,GUI线程需要被冻结,但是GUI的渲染会被保存在一个队列当中,等待JS引擎空闲的时候执行渲染。
- 由此也可以推出,如果JS引擎正在进行CPU密集型计算,那么JS引擎将会阻塞,长时间不空闲,导致渲染进程一直不能执行渲染,页面就会看起来卡顿卡顿的,渲染不连贯,所以,要尽量避免JS执行时间过长。
JS引擎线程与事件触发线程、定时触发器线程、异步HTTP请求线程
- 事件触发线程、定时触发器线程、异步HTTP请求线程三个线程有一个共同点,那就是使用回调函数的形式,当满足了特定的条件,这些回调函数会被执行。
- 这些回调函数被浏览器内核理解成事件,在浏览器内核中拥有一个事件队列,这三个线程当满足了内部特定的条件,会将这些回调函数添加到事件队列中,等待JS引擎空闲执行。
- 例如异步HTTP请求线程,线程如果检测到请求的状态变更,如果设置有回调函数,回调函数会被添加事件队列中,等待JS引擎空闲了执行。
- 但是,JS引擎对事件队列(宏任务)与JS引擎内的任务(微任务)执行存在着先后循序,当每执行完一个事件队列的时间,JS引擎会检测内部是否有未执行的任务,如果有,将会优先执行(微任务)。
-
渲染引擎
- 渲染引擎的主要作用是,将网页代码渲染为用户视觉可以感知的平面文档。
渲染引擎处理网页,通常分成四个阶段。
- 解析代码:HTML 代码解析为 DOM,CSS 代码解析为 CSSOM(CSS Object Model)。
- 对象合成:将 DOM 和 CSSOM 合成一棵渲染树(render tree)。
- 布局:根据渲染树来布局,计算每个节点的位置。(layout)。
- 绘制:调用 GPU 绘制,合成图层,显示在屏幕上。
- 以上四步并非严格按顺序执行,往往第一步还没完成,第二步和第三步就已经开始了。所以,会看到这种情况:网页的 HTML 代码还没下载完,但浏览器已经显示出内容了。
- 在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM 树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢
- 当 HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件。并且 CSS 也会影响 JS 的执行,只有当解析完样式表才会执行 JS,所以也可以认为这种情况下,CSS 也会暂停构建 DOM
重绘和重流
- 渲染树转换为网页布局,称为“布局流”(flow);布局显示到页面的这个过程,称为“绘制”(paint)
- 页面生成以后,脚本操作和样式表操作,都会触发“重流”(reflow)和“重绘”(repaint)。
- 回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。
触发原因
- 添加或者删除可见的DOM元素,
- 元素位置、尺寸、内容改变,
- 浏览器页面初始化,
- 浏览器窗口尺寸改变
优化技巧
- 读取 DOM 或者写入 DOM,尽量写在一起,不要混杂。不要读取一个 DOM 节点,然后立刻写入,接着再读取一个 DOM 节点。
- 不要一项一项地改变样式,而是使用 CSS class 一次性改变样式。
- 使用documentFragment操作 DOM
- 使用window.requestAnimationFrame(),因为它可以把代码推迟到下一次重流时执行,而不是立即要求页面重流。
- 使用虚拟 DOM(virtual DOM)库。
- 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
- 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
- 对于多次重排的元素,如动画,使用绝对定位脱离文档流,让他的改变不影响到其他元素
js引擎(单线程)
js引擎执行机制:https://juejin.im/post/59e85eebf265da430d571f89
- JavaScript 引擎的主要作用是,读取网页中的 JavaScript 代码,对其处理后运行。
由来
-
JavaScript 是一种解释型语言,也就是说,它不需要编译,由解释器实时运行。这样的好处是运行和修改都比较方便,刷新页面就可以重新解释;缺点是每次运行都要调用解释器,系统开销较大,运行速度慢于编译型语言。
-
为了提高运行速度,目前的浏览器都将 JavaScript 进行一定程度的编译,生成类似字节码(bytecode)的中间代码,以提高运行速度。
早期,浏览器内部对 JavaScript 的处理过程如下:
- 读取代码,进行词法分析(Lexical analysis),将代码分解成词元(token)。
- 对词元进行语法分析(parsing),将代码整理成“语法树”(syntax tree)。
- 使用“翻译器”(translator),将代码转为字节码(bytecode)。
- 使用“字节码解释器”(bytecode interpreter),将字节码转为机器码。
逐行解释将字节码转为机器码,是很低效的。为了提高运行速度,现代浏览器改为采用“即时编译”(Just In Time compiler,缩写 JIT),即字节码只在运行时编译,用到哪一行就编译哪一行,并且把编译结果缓存(inline cache)。通常,一个程序被经常用到的,只是其中一小部分代码,有了缓存的编译结果,整个程序的运行速度就会显著提升。
字节码不能直接运行,而是运行在一个虚拟机(Virtual Machine)之上,一般也把虚拟机称为 JavaScript 引擎。并非所有的 JavaScript 虚拟机运行时都有字节码,有的 JavaScript 虚拟机基于源码,即只要有可能,就通过 JIT(just in time)编译器直接把源码编译成机器码运行,省略字节码步骤。这一点与其他采用虚拟机(比如 Java)的语言不尽相同。这样做的目的,是为了尽可能地优化代码、提高性能。下面是目
定义
- 能够将 Javascript 代码处理并执行的运行环境。
JavaScript 语言是一种解释性脚本语言,因此在运行时,需要先将代码转变成抽象语法树,然后在抽象语法树上解释执行
包含部分
- 编译器。主要工作是将源代码编译成抽象语法树,在某些引擎可能还包含了将抽象语法树转换成中间表示(字节码)。
- 解释器。在某些引擎中,解释器主要是接收字节码,解释执行这个字节码,同时也依赖垃圾回收机制等。
- JIT 工具。一个能够 JIT 的工具,将字节码或者抽象语法树转换成本地代码。
- 垃圾回收器和分析工具。它们负责垃圾回收和收集引擎中的信息,帮助改善引擎的性能和功效。
- Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。
js引擎事件循环机制Event Loop
图解任务队列https://juejin.im/post/5a6309f76fb9a01cab2858b1
主线程角度
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程形成一个执行栈,异步的进入Web Api中的Web Worker Api所串讲的worker事件线程中进行执行。
- 当指定的事情完成时,事件触发线程将异步回调函数函数移入Event Queue(任务队列)。
- 主线程内的任务执行完毕为空,js引擎会去Event Queue查询读取对应的函数(异步任务),添加至执行栈。进入主线程执行。
- 上述过程会不断重复,也就是常说的Event Loop(事件循环
宏与微角度
- 执行同步代码,这属于宏任务
- 执行栈为空,查询是否有微任务需要执行
- 执行所有微任务
- 必要的话渲染 UI
- 然后开始下一轮 Event loop,执行宏任务中的异步代码
Web Worker
Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。
任务队列
- JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。)
- 任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
队列执行机制
- 执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的【事件队列】里 当前宏任务执行完成后,会查看微任务的【事件队列】,并将里面全部的微任务依次执行完 重复以上2步骤
macro-task
- script(整体代码),
- setTimeout
- setInterval
- I/O
- UI rendering。
micro-task【先执行】
- process.
- nextTick,
- Promise,
- MutationObserver(html5新特性)
异步编程
- JS 异步编程进化史:callback -> promise -> generator -> async + await
回调函数
- 回调函数的优点是简单、容易理解和实现
- 缺点是不利于代码的阅读和维护,各个部分之间高度耦合(coupling),使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。
事件监听
- 这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以“去耦合”(decoupling),有利于实现模块化。
- 缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。
发布/订阅模式 || 观察者模式
- 这种方法的性质与“事件监听”类似
- 但是明显优于后者。因为可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
Promise
- Promise 是回调函数的改进,使用then方法以后,异步任务的执行看得更清楚了。
- Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。
Generator
- Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
- 整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。
Async/Await
定义
- 是 Generator 函数的语法糖,async/await 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。
- async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。
- async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。
用法
- async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
async函数相对于Generator 函数的优势
- 内置执行器
Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。 - 更好的语义
async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。 - 更广的适用性
co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。 - 返回值是 Promise。
async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。
async函数相对于Promise的优势
- 处理 then 的调用链,能够更清晰准确的写出代码
- 并且也能优雅地解决回调地狱问题。
async函数的缺点
- 缺点在于滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。
- 解决办法:Promise.all
定时器
setTimeout、setInterval、requestAnimationFrame 各有什么特点?
-
setTimeout 和 setInterval 都不精确。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器 UI 线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行。
-
requestAnimationFrame 采用系统时间间隔,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。
setTimeout 倒计时为什么会出现误差?
-
setTimeout() 只是将事件插入了“任务队列”,必须等当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码消耗时间很长,也有可能要等很久,所以并没办法保证回调函数一定会在 setTimeout() 指定的时间执行。所以, setTimeout() 的第二个参数表示的是最少时间,并非是确切时间。
-
HTML5 标准规定了 setTimeout() 的第二个参数的最小值不得小于 4 毫秒,如果低于这个值,则默认是 4 毫秒。在此之前。老版本的浏览器都将最短时间设为 10 毫秒。另外,对于那些 DOM 的变动(尤其是涉及页面重新渲染的部分),通常是间隔 16 毫秒执行。这时使用 requestAnimationFrame() 的效果要好于 setTimeout()。
线程进程任务队列同步任务
https://www.cnblogs.com/leftJS/p/11070104.html
https://www.cnblogs.com/Jones-dd/p/8858995.html
https://www.cnblogs.com/jffun-blog/p/10211954.html
简述同步和异步的区别?
同步:浏览器访问服务器请求,用户看得到页面刷新,重新发请求,等请求完,页面刷新,新内容出现,用户看到新内容,进行下一步操作。
异步:浏览器访问服务器请求,用户正常操作,浏览器后端进行请求。等请求完,页面不刷新,新内容也会出现,用户看到新内容。
JS 延迟加载的方式有哪些?
defer 并行加载 js 文件,会按照页面上 script 标签的顺序执行;
async 并行加载 js 文件,下载完成立即执行,不会按照页面上 script 标签的顺序执行。
动态创建 DOM 方式(用得最多)
按需异步载入 JS。
下面这段代码输出结果是? 为什么?
var flag = true;
setTimeout(function() {
flag = false;
}, 0)
while(flag) {}
console.log(flag);
不输出任何东西,且控制台/浏览器卡死。
var flag = true,给 flag 赋值;
遇到 setTimeout,加入任务队列,跳过;
while 语句,这里的 flag 是 true,执行 {} 内循环体,循环体无内容无 break,陷入死循环。
下面这段代码输出结果是?为什么?
var a = 1;
setTimeout(function() {
a = 2;
console.log(a);
}, 0);
var a ;
console.log(a);
a = 3;
console.log(a);
输出结果:1 3 2。
原因:setTimeout 异步执行,从上到下执行完之后再执行 setTimeout 内的语句:
1. var a = 1:声明并将 1 赋值给 a;
2. setTimeout 方法加入任务队列不执行;
3. var a:声明 a,a 的值不变;
4. console.log(a) 输出为 1;
5. a = 3:将 3 赋值给 a;
6. console.log(a) 输出为 3;
7. 执行 setTimeout,a 被赋值为 2 并即时输出。