浏览器事件循环

  1. 同步代码,放入函数调用栈执行。异步任务,放入队列。
  2. 异步任务又分为宏任务和微任务。遇到宏仁务放入宏仁务队列,遇到微任务放入微任务队列。
  3. 宏仁务主要是setTimeout ,setInterval
  4. 微任务主要是Promise.then, MutationObserver

同步代码全部执行完毕了之后,会先去微任务队列中执行相关的任务,微任务队列清空之后,再从宏仁务队列中拿出一个任务执行,执行完之后再去执行微任务队列,然后再从宏仁务队列中拿出另一个任务执行。如此循环。

如果有async/await
await关键字后面的方法也是立即执行,执行完之后会继续执行全局同步代码,等同步代码执行完毕, 才会将await语句后面的代码放入微任务队列。不是立即放入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
console.log(1)
setTimeout(()=> {
console.log(2)
}, 1000)
setTimeout(() => {
var p = new Promise((resolve, reject) => {
console.log(3)
resolve()
}).then(() => {
console.log(4)
})
Promise.resolve().then(() => {
console.log(5)
})
}, 0)
setTimeout(() => {
Promise.resolve().then(() => {
console.log(6)
})
}, 0)
new Promise((resolve, reject) => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
Promise.resolve().then(() => {
console.log(9)
})
async function async1() {
await async2()
console.log(10)
}
async function async2() {
await async3()
console.log(11)
}
async function async3() {
console.log(12)
}
async1()
console.log(13)

// 1, 7, 12, 13, 8, 9, 11, 10, 3, 4, 5, 6, 2

用户输入 url 到页面呈现的过程

  1. 用户输入url
  2. 域名解析(DNS解析),浏览器查找域名的 IP 地址
  3. 找到 IP 地址后,TCP 三次握手,与目标服务器建立连接
  4. 建立连接后,浏览器向目标主机发送 http 请求
  5. 服务器处理收到的请求,将数据返回至浏览器
  6. 浏览器收到 HTTP 响应报文,关闭连接,开始解析文档
    1. 解析HTML生成DOM树
    2. 解析CSS生成CSSOM树,构建Style Rules
    3. DOM和CSSOM树合成渲染树,RenderTree
    4. Layout,布局,节点的大小和位置信息
    5. painting,绘制,渲染在屏幕上

怎么判断页面是否加载完成?

  • DOMContentLoaded事件在页面文档加载解析完毕之后马上执行, 不需要等待 CSS,JS,图片加载。
  • load事件在页面所有资源被加载进来之后才会触发,在DOMContentLoaded事件触发之后。

浏览器渲染过程

  1. 解析HTML,生成DOM树
  2. 解析CSS,生成CSSOM树
  3. 将DOM树和CSSOM树结合,生成渲染树(Render Tree)
  4. Layout:根据生成的渲染树,进行布局,得到节点的几何信息(位置,大小)
  5. Painting:将元素渲染在屏幕上

回流/重排(Reflow)和 重绘(Repaint)

回流:当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流,回流也被称为重排,即重新计算布局。
重绘:
当页面元素样式改变不影响元素在文档流中的位置时(如background-color,border-color,visibility),浏览器只会将新样式赋予元素并进行重新绘制操作。

回流必定会发生重绘,重绘不一定会引发回流。

何时发生回流和重绘

回流:

  • 浏览器窗口大小发生改变
  • 元素尺寸或位置发生改变
  • 元素内容变化(文字数量或图片大小等等)
  • 添加或者删除可见的DOM元素
  • 激活CSS伪类(例如::hover)
  • 查询某些属性或调用某些方法
    如 clientWidth、clientHeight、clientTop、clientLeft
    offsetWidth、offsetHeight、offsetTop、offsetLeft
    scrollWidth、scrollHeight、scrollTop、scrollLeft

重绘:
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等)

减少重绘和回流

  1. CSS:

    • 避免使用table布局。
    • 尽可能在DOM树的最末端改变class。
    • 避免设置多层内联样式。
    • 将动画效果应用到position属性为absolute或fixed的元素上
    • 避免使用CSS表达式(例如:calc())
    • 使用CSS3硬件加速(GPU加速)
  2. JavaScript:

    • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性
    • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      frag = document.creatDocumentFragment();

      p = document.creatElement('p');
      t = document.creatTextNode('fist paragraph');
      p.appendChild(t);
      farg.appendChild(p);

      p = document.creatElement('p');
      t = document.creatTextNode('second paragraph');
      p.appendChild(t);
      farg.appendChild(p);

      document.body.appendChild(frag)
    • 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘

    • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来
    • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流

浏览器的渲染进程和执行进程

1
2
document.body.style = 'background: red';
document.body.style = 'background: black';

上述代码执行浏览器会由红变黑吗?

不会,直接变黑,因为浏览器的js执行进程和DOM渲染进程是不同的,js执行的时候会阻塞浏览器的渲染,所以js执行完的结果就是黑,浏览器直接渲染黑

如何变成复合图层(硬件加速)

将元素变成一个复合图层,就是传说中的硬件加速技术。
一般一个元素开启硬件加速后会变成复合图层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能。
使用硬件加速时,尽可能的使用index,防止浏览器默认把比它高的元素也弄到复合层中渲染。提升渲染性能。

满足下列条件会变为复合层

  • 3D transforms:translate3d、translateZ 等
  • video、canvas、iframe 等元素
  • 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition

什么是浏览器同源策略?

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。同源是指:协议、域名、端口都相同。

同源策略限制内容有:

  • AJAX 请求
  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • DOM 节点

但是有三个标签是允许跨域加载资源:

  • <script src=XXX>
  • <img src=XXX>
  • <link href=XXX>

跨域

由于同源策略的限制,如果访问一个协议 || 域名 || 端口号不同的资源,即为跨域访问。
跨域只存在于浏览器端,不存在于安卓/ios/Node.js/python/java等其它环境

简单请求和非简单请求

只要同时满足以下两大条件,就属于简单请求。

  1. 请求方法是:GET,POST或者HEAD
  2. 请求头中没有自定义header。即头信息不超出以下几种字段
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type(只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain)

Content-Type字段的类型是application/json的是非简单请求

请求到底发出去了没有

跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。

  1. 对于简单请求
    • 浏览器直接发出CORS请求,在头信息之中,增加一个Origin字段。
    • 服务器去检查这个值,如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个CORS头信息字段。Access-Control-Allow-Origin, Access-Control-Allow-Credentials, Access-Control-Expose-Headers, Content-Type等。
    • 如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应,但是不包含上述头信息。浏览器发现,这个回应的头信息没有包含CORS相关头信息,如Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获并打印在控制台。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
  2. 对于非简单请求
    • 先发送一个OPTIONS预检请求,头信息中包含Origin、Access-Control-Request-Method和Access-Control-Request-Headers等字段
    • 如果服务器确认允许该请求,响应信息会包含对应的CORS头
    • 如果服务器否定了此预检请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。浏览器会认为不允许并抛错。

解决跨域的方法

  1. 服务端开启跨域资源共享Cross-Origin Resource Sharing(CORS),设置 Access-Control-Allow-Origin
  2. 客户端使用JSONP,服务器端也要支持对应的response
  3. 设置代理服务器(Node中间件代理,Nginx反向代理)

JSONP的实现原理

利用 <script> 标签没有跨域限制的漏洞,缺点是只能发get请求,不安全可能会遭受XSS攻击

  1. 声明一个回调函数,函数形参为要获取的目标数据(服务器返回的data)
  2. 创建一个<script>标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该回调函数名
  3. 服务器收到请求后,需要把传进来的回调函数名和真正的数据data拼接成一个字符串,然后返回给客户端
  4. 客户端会自动执行对应的回调函数,并将真正的data当做参数传进去
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <script>
    function jsonp(data) {
    console.log(data)
    }
    </script>
    <script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>

    // 服务器响应
    "jsonp({\"userid\":0,\"username\":\"null\"})"

代理服务器的原理

  • 正向代理:客户端将请求发给自己的代理server,server再转发给目标server.
  • 反向代理:客户端直接发请求给目标server. 但是目标server其实是一个代理服务器。他代理某个安全的或内网的server的请求。对于客户端并不知道自己访问的是一个代理。但此时代理服务器对外就表现为一个服务器。作用:
    1. 保证内网的安全
    2. 负载均衡,反向代理服务器收到请求,转发到不同的内网server去处理请求。

Node中间件代理:webpack的devserver就是一个正向代理
Nginx反向代理:在目标服务器上设置一个nginx服务器,nginx服务器接收所有的请求并转发到真正的服务器。

浏览器的缓存机制

浏览器缓存其实就是浏览器保存通过HTTP获取的所有资源,是浏览器将网络资源存储在本地的一种行为。

三级缓存原理

  1. 先查找内存,如果内存中存在,从内存中加载;
  2. 如果内存中未查找到,选择硬盘获取,如果硬盘中有,从硬盘中加载;
  3. 如果硬盘中未查找到,那就进行网络请求;
  4. 加载到的资源缓存到硬盘和内存;

强缓存和协商缓存

浏览器在加载资源时,会先根据本地缓存资源的 header 中的 expires 和 cahe-control 信息判断是否命中强缓存,如果命中则直接使用缓存中的资源不会再向服务器发送请求。

命中表示:有缓存且没过期。

  • Expires:这是 HTTP 1.0 的字段,表示缓存到期时间,是一个绝对的时间 (当前时间+缓存时间)。如 Expires: Thu, 10 Nov 2017 08:45:11 GMT
  • Cache-Control:在HTTP/1.1中,增加了一个字段Cache-control,该字段可以设置资源缓存的最大有效时间,它是一个相对时间,在该时间内,客户端不需要向服务器发送请求。如 Cache-Control: max-age=36000
    1. no-cache:需要进行协商缓存,发送请求到服务器确认是否使用缓存。
    2. no-store:禁止使用缓存,每一次都要重新请求数据。
    3. public:可以被所有的用户缓存,包括终端用户和 CDN 等中间代理服务器。
    4. private:只能被终端用户的浏览器缓存,不允许 CDN 等中继缓存服务器对其缓存。

Cache-Control 与 Expires 可以在服务端配置同时启用,同时启用的时候 Cache-Control 优先级高。

协商缓存:当强制缓存失效(超过规定时间)时,就需要使用协商缓存,由服务器决定缓存内容是否失效。浏览器先请求缓存数据库,返回一个缓存标识。之后浏览器拿这个标识和服务器通讯。如果缓存未失效,则返回 HTTP 状态码 304 表示继续使用,于是客户端继续使用缓存;如果失效,则返回新的数据和缓存规则,浏览器响应数据后,再把规则写入到缓存数据库。协商缓存由Last-Modify/If-Modify-Since 和 ETag/If-None-Match实现

  • Last-Modified & If-Modified-Since

    1. 服务器通过 Last-Modified 字段告知客户端,资源最后一次被修改的时间,例 Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
    2. 浏览器将这个值和内容一起记录在缓存数据库中。
    3. 下一次请求相同资源时时,浏览器从自己的缓存中找出“不确定是否过期的”缓存。因此在请求头中将上次的 Last-Modified 的值写入到请求头的 If-Modified-Since 字段
    4. 服务器会将 If-Modified-Since 的值与 Last-Modified 字段进行对比。如果相等,则表示未修改,响应 304;反之,则表示修改了,响应 200 状态码,并返回数据。

      缺点:
      短时间内资源发生了改变,Last-Modified 并不会发生变化。
      周期性变化。如果这个资源在一个周期内修改回原来的样子了,我们认为是可以使用缓存的,但是 Last-Modified 可不这样认为,因此便有了 ETag。

  • Etag & If-None-Match
    • 与 Last-Modify/If-Modify-Since 不同的是,Etag/If-None-Match 返回的是一个校验码。ETag 可以保证每一个资源是唯一的,资源变化都会导致 ETag 变化。服务器根据header上的 If-None-Match 值来判断是否命中缓存。

Etag 的优先级高于 Last-Modified。Last-Modified 与 ETag 是可以一起使用的,服务器会优先验证 ETag,一致的情况下,会继续比对 Last-Modified,两个都匹配,返回304,否则返回200 和最新的响应体。

常见的浏览器内核有哪些?

webkit内核,v8引擎

浏览器的主要组成部分是什么?

  • 用户界面 - 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其他显示的各个部分都属于用户界面。
  • 浏览器引擎 - 在用户界面和呈现引擎之间传送指令。
  • 呈现引擎 - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
  • 网络 - 用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。
  • 用户界面后端 - 用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。
  • JavaScript 解释器。用于解析和执行 JavaScript 代码。
  • 数据存储。这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。