按照渲染的时间顺序,流水线可分为以下几个子阶段:构建 DOM 树
、样式计算
、布局阶段
、分层
、栅格化
和显示
。css
浏览器从网络或硬盘中得到HTML字节数据后会通过一个流程将字节解析为DOM树,先将HTML的原始字节数据转换为文件指定编码的字符,而后浏览器会根据HTML规范来将字符串转换成各类令牌标签,如html、body等。最终解析成一个树状的对象模型,就是dom树;html
获取css,获取style标签内的css、或者内嵌的css,或者当HTML代码碰见标签时,浏览器会发送请求得到该标签中标记的CSS,当渲染引擎接收到 CSS 文本时,会执行一个转换操做,将 CSS 文本转换为浏览器能够理解的styleSheets前端
建立布局树,遍历 DOM 树中的全部可见节点,并把这些节点加到布局中;而不可见的节点会被布局树忽略掉,如 head 标签下面的所有内容,再好比 body.p.span 这个元素,由于它的属性包含 dispaly:none,因此这个元素也没有被包进布局树。最后计算 DOM 元素的布局信息,使其都保存在布局树中。布局完成过程当中,若是有js操做或者其余操做,对元素的颜色,背景等做出改变就会引发重绘,若是有对元素的大小、定位等有改变则会引发回流。
git
由于页面中有不少复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 作 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还须要为特定的节点生成专用的图层,并生成一棵对应的图层树。github
渲染引擎实现图层的绘制,把一个图层的绘制拆分红不少小的绘制指令而后再把这些指令按照顺序组成一个待绘制列表,当图层的绘制列表准备好以后,主线程会把该绘制列表提交给合成线程,合成线程会将图层划分为图块,而后按照视口附近的图块来优先生成位图(实际生成位图的操做是由栅格化来执行的。所谓栅格化,是指将图块转换为位图)web
一旦全部图块都被光栅化,合成线程就会生成一个绘制图块的命令,而后将该命令提交给浏览器进程,浏览器最后进行显示。浏览器
回流:
当咱们对 DOM 的修改引起了 DOM 几何尺寸的变化(好比修改元素的宽、高或隐藏元素等)时,浏览器须要从新计算元素的几何属性(其余元素的几何属性和位置也会所以受到影响),而后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。缓存
重绘:
当咱们对 DOM 的修改致使了样式的变化、却并未影响其几何属性(好比修改了颜色或背景色)时,浏览器不需从新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。这个过程叫作重绘。 由此咱们能够看出,重绘不必定致使回流,回流必定会致使重绘。微信
will-change
#divId { will-change: transform; } 复制代码
优势
markdown
注意:
部分浏览器缓存了一个 flush 队列,把咱们触发的回流与重绘任务都塞进去,待到队列里的任务多起来、或者达到了必定的时间间隔,或者“不得已”的时候,再将这些任务一口气出队。可是当咱们访问一些即便属性时,浏览器会为了得到此时此刻的、最准确的属性值,而提早将 flush 队列的任务出队。
层叠上下文
是HTML元素的三维概念,这些HTML元素在一条假想的相对于面向(电脑屏幕的)视窗或者网页的用户的z轴上延伸,HTML元素依据其自身属性按照优先级顺序占用层叠上下文的空间。
拥有层叠上下文属性:
这里的剪裁指的是,假如咱们把 div 的大小限定为 200 * 200 像素,而 div 里面的文字内容比较多,文字所显示的区域确定会超出 200 * 200 的面积,这时候就产生了剪裁,渲染引擎会把裁剪文字内容的一部分用于显示在 div 区域。出现这种裁剪状况的时候,渲染引擎会为文字部分单首创建一个层,若是出现滚动条,滚动条也会被提高为单独的层。
块级做用域就是经过词法环境的栈结构来实现的,而变量提高是经过变量环境来实现,经过这二者的结合,JavaScript 引擎也就同时支持了变量提高和块级做用域了。
词法环境跟函数上下文,都是经过栈结构实现的。函数内部经过 var 声明的变量,在编译阶段全都被存放到变量环境(函数上下文)中,而经过let和const申明的变量会被追加到词法环境中,当这个块执行结束以后,追加到词法做用域的内容又会销毁掉。
举个例子:
function foo() { var test = 1 let myname= 'LuckyWinty' { console.log(myname) let myname= 'winty' } console.log(test,'---',myname) } foo() //思考一下会输出什么? 复制代码
执行到第一个console.log
前的执行上下文是这样的:
从图中看,第一个console.log
理论上应该输出 undefined
。可是语法规定了一个"暂时性死区(TDZ,当进入它的做用域,它不能被访问(获取或设置)直到执行到达声明)"
,也就是说虽然经过let声明的变量已经在词法环境中了,可是在没有赋值以前,访问该变量JavaScript引擎就会抛出一个错误。
所以,第一个console.log
会抛错,[Uncaught ReferenceError: Cannot access 'myname' before initialization]。抛错则函数会中断执行,为了能让咱们的代码继续分析,咱们先加个 try-catch ,而后继续分析:
function foo() { var test = 1 let myname= 'LuckyWinty' try{ { console.log(myname) let myname= 'winty' } }catch(ex){ console.error(ex) } console.log(test,'---',myname) } foo() //思考一下会输出什么? 复制代码
执行到第二个console.log
前的执行上下文是这样的:
此时,{}
块做用域中的内容已执行完毕,被销毁掉了。第二个console.log
会输出1 "---" "LuckyWinty"
。
在 JavaScript 中,原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。
在 JavaScript 的执行过程当中, 主要有三种类型内存空间,分别是代码空间
、栈空间
、堆空间
。 其中的代码空间主要是存储可执行代码的,原始类型(Number、String、Null、Undefined、Boolean、Symbol、BigInt)的数据值都是直接保存在“栈”中的,引用类型(Object)的值是存放在“堆”中的。所以在栈空间中(执行上下文),原始类型存储的是变量的值,而引用类型存储的是其在"堆空间"中的地址,当 JavaScript 须要访问该数据的时候,是经过栈中的引用地址来访问的,至关于多了一道转手流程。
在编译过程当中,若是 JavaScript 引擎判断到一个闭包,也会在堆空间建立换一个“closure(fn)”
的对象(这是一个内部对象,JavaScript 是没法访问的),用来保存闭包中的变量。因此闭包中的变量是存储在“堆空间”中的。
JavaScript 引擎须要用栈来维护程序执行期间上下文的状态,若是栈空间大了话,全部的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。一般状况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,因此这一类数据会被存放到堆中,堆空间很大,能存放不少大的数据,不过缺点是分配内存和回收内存都会占用必定的时间。所以须要“栈”和“堆”两种空间。