浏览器里,从用户输入url到用户看到页面结果的过程,发生了什么?

1、域名解析

域名解析的过程:

  (1)查询浏览器自身DNS缓存

  (2)若上面没有找到,则搜索操作系统自身的DNS缓存

  (3)若上面没有找到,则尝试读取hosts文件

  (4)若上面没有找到,向本地配置的首选DNS服务器发送请求

  (5)win系统 若上面没有找到,操作系统查找NetBIOS name cache

  (6)win系统 若上面没有找到,查询wins服务器

  (7)win系统 若上面没有找到,广播查找

  (8)win系统 若上面没有找到,读取LMHOSTS文件

 若以上都没有找到,解析失败。

2、TCP三次握手

img

3、浏览器向服务器发送http请求

一旦建立了TCP连接,web浏览器就会向web服务器发送请求命令。例如:

GET/sample/hello.jsp HTTP/1.1

4、浏览器发送请求头信息

  浏览器发送请求命令之后,还要以头信息的形式向web服务器发送一些别的信息,之后浏览器发送了一空白行来通知服务器,它已经结束了该头信息的发送。

5、服务器处理请求

  服务器收到http请求,确定执行什么(asp.net、php、ruby、java等)来处理他。读取参数并进行逻辑操作后,生成指定的数据。

6、服务器做出应答

  客户机向服务器发出请求后,服务器会向客户机回送应答,HTTP/1.1 200 OK,应答的第一部分是协议的版本号和应答状态码

7、服务器发送应答头信息

  正如客户端会随同请求发送关于自身的信息一样,服务器也会随同应答向用户发送关于它自己的数据及被请求的文档。

8、服务器发送数据

  web服务器向浏览器发送头信息后,它会发送一个空白行来表示头信息的发生到此为结束,接着,它就以Content-Type 应答头信息所描述的格式发送用户所请求的实际数据。

9、tcp连接关闭

  一般情况下,一旦web服务器向浏览器发送了请求数据,它就要关闭tcp连接,然后如果浏览器或者服务器在其头信息加入了这行代码:

  Connection keep-alive

  TCP连接在发送后将仍然保持打开状态,于是,浏览器可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。

浏览器渲染页面的过程

  • 根据 HTML 结构生成 DOM Tree
  • 根据 CSS 生成 CSSOM
  • DOMCSSOM 整合形成 RenderTree
  • 根据 RenderTree 开始渲染和展示
  • 遇到 <script> 时,会执行并阻塞渲染,因为 Javascript 代码有权利改变DOM

浏览器如何渲染网页

要了解浏览器渲染页面的过程,首先得知道一个名词——关键渲染路径。关键渲染路径是指浏览器从最初接收请求来的HTML、CSS、javascript等资源,然后解析、构建树、渲染布局、绘制,最后呈现给用户能看到的界面这整个过程。

用户看到页面实际上可以分为两个阶段:页面内容加载完成和页面资源加载完成,分别对应于DOMContentLoadedLoad

  • DOMContentLoaded事件触发时,仅当DOM加载完成,不包括样式表,图片等
  • load事件触发时,页面上所有的DOM,样式表,脚本,图片都已加载完成

浏览器渲染的过程主要包括以下五步:

  1. 浏览器将获取的HTML文档解析成DOM树。
  2. 处理CSS标记,构成层叠样式表模型CSSOM(CSS Object Model)。
  3. 将DOM和CSSOM合并为渲染树(rendering tree),代表一系列将被渲染的对象。
  4. 渲染树的每个元素包含的内容都是计算过的,它被称之为布局layout。浏览器使用一种流式处理的方法,只需要一次绘制操作就可以布局所有的元素。
  5. 将渲染树的各个节点绘制到屏幕上,这一步被称为绘制painting

需要注意的是,以上五个步骤并不一定一次性顺序完成,比如DOM或CSSOM被修改时,亦或是哪个过程会重复执行,这样才能计算出哪些像素需要在屏幕上进行重新渲染。而在实际情况中,JavaScript和CSS的某些操作往往会多次修改DOM或者CSSOM。

webkit渲染引擎流程

浏览器渲染网页的具体流程

构建DOM树

当浏览器接收到服务器响应来的HTML文档后,会遍历文档节点,生成DOM树。
需要注意以下几点:

  • DOM树在构建的过程中可能会被CSS和JS的加载而执行阻塞
  • display:none的元素也会在DOM树中
  • 注释也会在DOM树中
  • script标签会在DOM树中

无论是DOM还是CSSOM,都是要经过Bytes→characters→tokens→nodes→object model这个过程。

3534846-770586311bb543d6

当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。

构建CSSOM规则树

浏览器解析CSS文件并生成CSSOM,每个CSS文件都被分析成一个StyleSheet对象,每个对象都包含CSS规则。CSS规则对象包含对应于CSS语法的选择器和声明对象以及其他对象。
在这个过程需要注意的是:

  • CSS解析可以与DOM解析同时进行。
  • CSS解析与script的执行互斥 。
  • 在Webkit内核中进行了script执行优化,只有在JS访问CSS时才会发生互斥。

构建渲染树(Render Tree)

通过DOM树和CSS规则树,浏览器就可以通过它两构建渲染树了。浏览器会先从DOM树的根节点开始遍历每个可见节点,然后对每个可见节点找到适配的CSS样式规则并应用。
有以下几点需要注意:

  • Render Tree和DOM Tree不完全对应
  • display: none的元素不在Render Tree中
  • visibility: hidden的元素在Render Tree中

渲染树生成后,还是没有办法渲染到屏幕上,渲染到屏幕需要得到各个节点的位置信息,这就需要布局(Layout)的处理了。

渲染树布局(layout of the render tree)

布局阶段会从渲染树的根节点开始遍历,由于渲染树的每个节点都是一个Render Object对象,包含宽高,位置,背景色等样式信息。所以浏览器就可以通过这些样式信息来确定每个节点对象在页面上的确切大小和位置,布局阶段的输出就是我们常说的盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小。需要注意的是:

  • float元素,absoulte元素,fixed元素会发生位置偏移。
  • 我们常说的脱离文档流,其实就是脱离Render Tree。

渲染树绘制(Painting the render tree)

在绘制阶段,浏览器会遍历渲染树,调用渲染器的paint()方法在屏幕上显示其内容。渲染树的绘制工作是由浏览器的UI后端组件完成的。

浏览器渲染网页的那些事儿

浏览器主要组件结构

浏览器主要组件

渲染引擎主要有两个:webkit和Gecko
Firefox使用Geoko,Mozilla自主研发的渲染引擎。Safari和Chrome都使用webkit。Webkit是一款开源渲染引擎,它本来是为linux平台研发的,后来由Apple移植到Mac及Windows上。
虽然主流浏览器渲染过程叫法有区别,但是主要流程还是相同的。

渲染阻塞

JS可以操作DOM来修改DOM结构,可以操作CSSOM来修改节点样式,这就导致了浏览器在遇到<script>标签时,DOM构建将暂停,直至脚本完成执行,然后继续构建DOM。如果脚本是外部的,会等待脚本下载完毕,再继续解析文档。现在可以在script标签上增加属性defer或者async。脚本解析会将脚本中改变DOM和CSS的地方分别解析出来,追加到DOM树和CSSOM规则树上。

每次去执行JavaScript脚本都会严重地阻塞DOM树的构建,如果JavaScript脚本还操作了CSSOM,而正好这个CSSOM还没有下载和构建,浏览器甚至会延迟脚本执行和构建DOM,直至完成其CSSOM的下载和构建。所以,script标签的位置很重要。

JS阻塞了构建DOM树,也阻塞了其后的构建CSSOM规则树,整个解析进程必须等待JS的执行完成才能够继续,这就是所谓的JS阻塞页面。

由于CSSOM负责存储渲染信息,浏览器就必须保证在合成渲染树之前,CSSOM是完备的,这种完备是指所有的CSS(内联、内部和外部)都已经下载完,并解析完,只有CSSOM和DOM的解析完全结束,浏览器才会进入下一步的渲染,这就是CSS阻塞渲染。

CSS阻塞渲染意味着,在CSSOM完备前,页面将一直处理白屏状态,这就是为什么样式放在head中,仅仅是为了更快的解析CSS,保证更快的首次渲染。

需要注意的是,即便你没有给页面任何的样式声明,CSSOM依然会生成,默认生成的CSSOM自带浏览器默认样式。

当解析HTML的时候,会把新来的元素插入DOM树里面,同时去查找CSS,然后把对应的样式规则应用到元素上,查找样式表是按照从右到左的顺序去匹配的。

例如:div p {font-size: 16px},会先寻找所有p标签并判断它的父标签是否为div之后才会决定要不要采用这个样式进行渲染)。
所以,我们平时写CSS时,尽量用idclass,千万不要过渡层叠。

回流和重绘(reflow和repaint)

我们都知道HTML默认是流式布局的,但CSS和JS会打破这种布局,改变DOM的外观样式以及大小和位置。因此我们就需要知道两个概念:replaintreflow

reflow(回流)

当浏览器发现布局发生了变化,这个时候就需要倒回去重新渲染,这个回退的过程叫reflowreflow会从html这个root frame开始递归往下,依次计算所有的结点几何尺寸和位置,以确认是渲染树的一部分发生变化还是整个渲染树。reflow几乎是无法避免的,因为只要用户进行交互操作,就势必会发生页面的一部分的重新渲染,且通常我们也无法预估浏览器到底会reflow哪一部分的代码,因为他们会相互影响。

repaint(重绘)

repaint则是当我们改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸和位置没有发生改变。

需要注意的是,display:none会触发reflow,而visibility: hidden属性则并不算是不可见属性,它的语义是隐藏元素,但元素仍然占据着布局空间,它会被渲染成一个空框。所以visibility:hidden只会触发repaint,因为没有发生位置变化。

另外有些情况下,比如修改了元素的样式,浏览器并不会立刻reflowrepaint一次,而是会把这样的操作积攒一批,然后做一次reflow,这又叫异步reflow或增量异步reflow。但是在有些情况下,比如resize窗口,改变了页面默认的字体等。对于这些操作,浏览器会马上进行reflow

引起reflow

现代浏览器会对回流做优化,它会等到足够数量的变化发生,再做一次批处理回流。

  • 页面第一次渲染(初始化)
  • DOM树变化(如:增删节点)
  • Render树变化(如:padding改变)
  • 浏览器窗口resize
  • 获取元素的某些属性

浏览器为了获得正确的值也会提前触发回流,这样就使得浏览器的优化失效了,这些属性包括offsetLeft、offsetTop、offsetWidth、offsetHeight、 scrollTop/Left/Width/Height、clientTop/Left/Width/Height、调用了getComputedStyle()

引起repaint

reflow回流必定引起repaint重绘,重绘可以单独触发。
背景色、颜色、字体改变(注意:字体大小发生变化时,会触发回流)

减少reflow、repaint触发次数

  • transform做形变和位移可以减少reflow
  • 避免逐个修改节点样式,尽量一次性修改
  • 使用DocumentFragment将需要多次修改的DOM元素缓存,最后一次性append到真实DOM中渲染
  • 可以将需要多次修改的DOM元素设置display:none,操作完再显示。(因为隐藏元素不在render树内,因此修改隐藏元素不会触发回流重绘)
  • 避免多次读取某些属性
  • 通过绝对位移将复杂的节点元素脱离文档流,形成新的Render Layer,降低回流成本

几条关于优化渲染效率的建议

结合上文有以下几点可以优化渲染效率。

  • 合法地去书写HTML和CSS ,且不要忘了文档编码类型。
  • 样式文件应当在head标签中,而脚本文件在body结束前,这样可以防止阻塞的方式。
  • 简化并优化CSS选择器,尽量将嵌套层减少到最小。
  • DOM 的多个读操作(或多个写操作),应该放在一起。不要两个读操作之间,加入一个写操作。
  • 如果某个样式是通过重排得到的,那么最好缓存结果。避免下一次用到的时候,浏览器又要重排。
  • 不要一条条地改变样式,而要通过改变class,或者csstext属性,一次性地改变样式。
  • 尽量用transform来做形变和位移
  • 尽量使用离线DOM,而不是真实的网页DOM,来改变元素样式。比如,操作Document Fragment对象,完成后再把这个对象加入DOM。再比如,使用cloneNode()方法,在克隆的节点上进行操作,然后再用克隆的节点替换原始节点。
  • 先将元素设为display: none(需要1次重排和重绘),然后对这个节点进行100次操作,最后再恢复显示(需要1次重排和重绘)。这样一来,你就用两次重新渲染,取代了可能高达100次的重新渲染。
  • position属性为absolutefixed的元素,重排的开销会比较小,因为不用考虑它对其他元素的影响。
  • 只在必要的时候,才将元素的display属性为可见,因为不可见的元素不影响重排和重绘。另外,visibility : hidden的元素只对重绘有影响,不影响重排。
  • 使用window.requestAnimationFrame()window.requestIdleCallback()这两个方法调节重新渲染。

箭头函数和普通函数的区别

  1. 箭头函数没有自己的this,继承外层代码块的this。
  2. 不能当做构造函数,也就是说不可以使用new命令,否则会报错的。
  3. 不能使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  4. 不能使用yield命令,因此箭头函数不能用作 Generator(生成器) 函数。
  5. 因为没有this,所以不能使用call、bind、apply来改变this的指向。

数组常用方法

Array对象属性

  1. constructor 属性返回对创建此对象的数组函数的引用。

  2. prototype 属性使您有能力向对象添加属性和方法。

  3. length 属性可设置或返回数组中元素的数目。

设置 length 属性可改变数组的大小。如果设置的值比其当前值小,数组将被截断,其尾部的元素将丢失。如果设置的值比它的当前值大,数组将增大,新的元素被添加到数组的尾部,它们的值为 undefined。

一、改变原来数组的方法

  1. pop() 获取数组最后一个元素(改变原数组)、删除数组最后一个元素

arrayObject.pop()

js内置的pop()方法可用于删除并并返回数组的最后一个元素,注意这里获取了数组的最后一个元素的同时也将原数组的最后一个元素删除了。如果数组为空,则该方法不改变数组,并返回undefined

1
2
3
4
let list = [1,2,3,4]
let last = list.pop()
console.log(list) // [1, 2, 3]
console.log(last) // 4
  1. push 数组尾部添加元素(返回数组的新长度)

push() 方法可向数组的末尾添加一个或多个元素,并返回新的长度。

1
arrayObject.push(newelement1,newelement2,....,newelementX)
1
2
3
4
5
6
7
8
// 尾部添加数组元素
let arr6 = [1,2,3]
let arr7 = arr6.push(4,5)
console.log(arr6,arr7) // [1, 2, 3, 4, 5] 5
arr6.push(6)
console.log(arr6) // [1, 2, 3, 4, 5, 6]
arr6 = arr6.push(7)
console.log(arr6) // 7
  1. reverse 颠倒数组的顺序(改变原数组)

arrayObject.reverse()

reverse() 方法用于颠倒数组中元素的顺序。该方法会改变原来的数组,而不会创建新的数组。

1
2
3
let arr8 = [1,2,3,4,5,6]
let arr9 = arr8.reverse()
console.log(arr8,arr9) // [6, 5, 4, 3, 2, 1] [6, 5, 4, 3, 2, 1]
  1. shift 获取数组的第一个元素(改变原数组)、删除数组第一个元素

arrayObject.shift()

shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。如果数组是空的,那么 shift() 方法将不进行任何操作,返回 undefined 值。请注意,该方法不创建新数组,而是直接修改原有的 arrayObject。

1
2
3
4
// 删除第一个元素,并返回第一个元素的值
let arr10 = [1,2,3,4]
let arr11 = arr10.shift()
console.log(arr10,arr11) // [2, 3, 4] 1
  1. splice 向数组中添加或删除元素(改变原数组),然后返回被删除的元素
1
arrayObject.splice(index,howmany,item1,.....,itemX)

index:必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。

howmany:必需。要删除的项目数量。如果设置为 0,则不会删除项目。

item1, …, itemX:可选。向数组添加的新项目。

splice() 方法可删除从 index 处开始的零个或多个元素,并且用参数列表中声明的一个或多个值来替换那些被删除的元素。

如果从 arrayObject 中删除了元素,则返回的是含有被删除的元素的数组。

所有主流浏览器都支持 splice() 方法。

注释:请注意,splice() 方法与 slice() 方法的作用是不同的,splice() 方法会直接对数组进行修改。

1
2
3
4
//  从数组中添加/删除项目,然后返回被删除的项目
let arr19 = [1,2,3,4,5,6]
let arr20 = arr19.splice(1,4)
console.log(arr20,arr19) // [2, 3, 4, 5] [1, 6]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let arr21 = [1,2,3,4,5,6]
let arr22 = arr21.splice(1,4,222,333,666)
console.log(arr22,arr21) // [2, 3, 4, 5] [1,222,333,666,6]

var arr = new Array(6)
arr[0] = "George"
arr[1] = "John"
arr[2] = "Thomas"
arr[3] = "James"
arr[4] = "Adrew"
arr[5] = "Martin"

document.write(arr + "<br />")
arr.splice(2,0,"William")
document.write(arr + "<br />")

George,John,Thomas,James,Adrew,Martin
George,John,William,Thomas,James,Adrew,Martin
  1. unshift 向数组的开头添加一个或更多元素(改变原数组),并返回新的长度
1
arrayObject.unshift(newelement1,newelement2,....,newelementX)

第一个参数必填。向数组添加的元素

unshift() 方法将把它的参数插入 arrayObject 的头部,并将已经存在的元素顺次地移到较高的下标处,以便留出空间。

该方法的第一个参数将成为数组的新元素 0,如果还有第二个参数,它将成为新的元素 1,以此类推。

请注意,unshift() 方法不创建新的创建,而是直接修改原有的数组。

注释:该方法会改变数组的长度。

注释:unshift() 方法无法在 Internet Explorer 中正确地工作

  1. sort 对数组元素进行排序(改变原数组)

arrayObject.sort(sortby)

对数组的引用。请注意,数组在原数组上进行排序,不生成副本。

参数可选。规定排序顺序。必须是函数。

如果调用该方法时没有使用参数,将按字母顺序对数组中的元素进行排序,说得更精确点,是按照字符编码的顺序进行排序。

要实现这一点,首先应把数组的元素都转换成字符串(如有必要),以便进行比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 数组的排序
let arr = new Array(6)
arr[0] = 'George'
arr[1] = 'John'
arr[2] = 'Thomas'
arr[3] = 'James'
arr[4] = 'Adrew'
arr[5] = 'Martin'
let arr16 = arr.sort()
console.log(arr16, arr)
// ["Adrew", "George", "James", "John", "Martin", "Thomas"]
// ["Adrew", "George", "James", "John", "Martin", "Thomas"]

let arr17 = new Array(6)
arr17[0] = '10'
arr17[1] = '5'
arr17[2] = '40'
arr17[3] = '25'
arr17[4] = '1000'
arr17[5] = '1'
console.log(arr17.sort())
// ["1", "10", "1000", "25", "40", "5"]

如果想按照其他标准进行排序,就需要提供比较函数,该函数要比较两个值,然后返回一个用于说明这两个值的相对顺序的数字。

比较函数应该具有两个参数 a 和 b,其返回值如下:

若 a 小于 b,在排序后的数组中 a 应该出现在 b 之前,则返回一个小于 0 的值。

若 a 等于 b,则返回 0。

若 a 大于 b,则返回一个大于 0 的值。

1
2
3
4
5
6
7
8
9
10
11
function sortNumber(a,b){
return a-b
}
let arr18 = new Array(6)
arr18[0] = '10'
arr18[1] = '5'
arr18[2] = '40'
arr18[3] = '25'
arr18[4] = '1000'
arr18[5] = '1'
console.log(arr18.sort(sortNumber)) // ["1", "5", "10", "25", "40", "1000"]

二、不改变原来数组的方法

  1. concat 连接多个数组或元素(不改变原数组)

arrayObject.concat(arrayX,arrayX,……,arrayX)

concat() 方法用于连接两个或多个数组,该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。

该参数可以是具体的值,也可以是数组对象。可以是任意多个。

1
2
3
4
5
6
7
8
9
let arr1 = [1,2,3]
let arr2 = [4,5]
// 1. 数组的合并
let arr3 = arr1.concat(arr2)
console.log(arr1,arr2,arr3)
// [1,2,3] [4,5] [1,2,3,4,5]

arr3 = arr3.concat(6,7)
console.log(arr3) // [1, 2, 3, 4, 5, 6, 7]
  1. join 将数组用分隔符连接成字符串(不改变原数组)

arrayObject.join(separator)

join() 方法用于把数组中的所有元素放入一个字符串。

元素是通过指定的分隔符进行分隔的。

该参数可选

指定要使用的分隔符,如果省略该参数,则使用逗号作为分隔符。

1
2
3
4
5
6
//   数组转换成字符串,并使用分隔符连接
let arr4 = [2019,3,14]
let arr5 = arr4.join()
console.log(arr4,arr5) // [2019, 3, 14] "2019,3,14"
arr4 = arr4.join('.')
console.log(arr4) // 2019.3.14
  1. slice 截取原数组中某段元素(不改变原数组)

arrayObject.slice(start,end)

slice() 方法可从已有的数组中返回选定的元素。

返回一个新的数组,包含从 start 到 end (不包括该元素)的 arrayObject 中的元素。

start:必需。规定从何处开始选取。如果是负数,那么它规定从数组尾部开始算起的位置。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推。

end:可选。规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。

请注意,该方法并不会修改数组,而是返回一个子数组。如果想删除数组中的一段元素,应该使用方法 Array.splice()

1
2
3
4
5
6
7
// 从原数组中返回选定的元素
let arr12 = [1,2,3,4]
let arr13 = arr12.slice(1)
console.log(arr12,arr13) // [1, 2, 3, 4] [2, 3, 4]
let arr14 = [1,2,3,4]
let arr15 = arr14.slice(-1)
console.log(arr14,arr15) // [1, 2, 3, 4] 4
  1. toString 把数组转换为字符串,并返回结果(不改变原数组)

arrayObject.toString()

toString() 方法可把数组转换为字符串,并返回结果。

arrayObject 的字符串表示。返回值与没有参数的 join() 方法返回的字符串相同。

当数组用于字符串环境时,JavaScript 会调用这一方法将数组自动转换成字符串。但是在某些情况下,需要显式地调用该方法。

注释:数组中的元素之间用逗号分隔

1
2
3
4
// 将数组转换成字符串
let arr23 = [1,2,3,4]
let arr24 = arr23.toString()
console.log(arr23,arr24) // [1, 2, 3, 4] "1,2,3,4"
  1. toLocaleString 把数组转换为本地字符串(不改变原数组)

arrayObject.toLocaleString()

arrayObject 的本地字符串表示。首先调用每个数组元素的 toLocaleString() 方法,然后使用地区特定的分隔符把生成的字符串连接起来,形成一个字符串。

1
2
3
4
5
6
7
// 把数组转换为本地字符串  返回格式化对象后的字符串,该字符串格式因语言不同返回值也会不同,可以通过传参决定返回的语言及具体的表现
let arr25 = [1,2,3,4]
let arr26 = arr25.toLocaleString()
console.log(arr25,arr26) // [1, 2, 3, 4] "1,2,3,4"

let a = 12222
console.log(a.toLocaleString()) // 12,222 (整数部分每三位加一个逗号)