浏览器事件循环
- 同步代码,放入函数调用栈执行。异步任务,放入队列。
- 异步任务又分为宏任务和微任务。遇到宏任务放入宏任务队列,遇到微任务放入微任务队列。
- 宏任务主要是 setTimeout ,setInterval
- 微任务主要是 Promise.then, MutationObserver
同步代码全部执行完毕了之后,会先去微任务队列中执行相关的任务,微任务队列清空之后,再从宏任务队列中拿出一个任务执行,执行完之后再去执行微任务队列,然后再从宏任务队列中拿出另一个任务执行。如此循环。
Promise 新建后立即执行, Promise.resolve()也是立即执行
await 关键字后面的方法也是立即执行,执行完之后会继续执行全局同步代码,等同步代码执行完毕, 才会将 await 语句后面的代码放入微任务队列。不是立即放入。
前端存储方式
-
cookie
cookie 中主要存储 sessionid,过期时间,域名,路径等。会在发送请求的时候自动携带。为了维持服务器的状态。大小只有 4k。 -
localStorage
localStorage 通过 key-value 存储一些值,大小为 5 兆,会永久存储在客户端。 -
sessionStorage
sessionStorage 跟 localStorage 相似,会话级别,关闭页面就会消失。两个同域的窗口不可共享,但是可复制,就是如果从一个窗口内部打开另一个同域的页面,sessionStorage 会拷贝父窗口的,作为新窗口的初始值。 -
IndexDB
前端的非关系型数据库,用于在客户端存储大量的数据.
浏览器的渲染进程和执行进程
document.body.style = "background: red";
document.body.style = "background: black";
上述代码执行浏览器会由红变黑吗?
不会,直接变黑,因为浏览器的 js 执行进程和 DOM 渲染进程是不同的,js 执行的时候会阻塞浏览器的渲染,所以 js 执行完的结果就是黑,浏览器直接渲染黑
JS 更新 DOM 后可以立即拿到节点内容吗?是同步还是在事件循环中? 那是什么时候渲染在浏览器的?
所有的 DOM 操作,其实也是 JS 代码,DOM 对象也是 JS 对象,代码是同步执行的,执行完可以拿到对象中的新内容。
但是 GPU 渲染到页面,用户看到页面改变,是由很多因素决定的,而且渲染的时候,跟任务队列没关系,但是必须 JS 同步代码已经执行完了。因为 JS 主线程和渲染线程是互斥的。
let div = document.createElement("div");
div.id = "tt";
div.innerText = "testststs";
document.body.append(div);
let newdiv = document.getElementById("tt");
newdiv.innerText = "1";
Promise.resolve().then(() => {
for (let i = 0; i < 10000; i++) {
console.log(1);
}
console.log("微任务任务代码中拿到的: ", newdiv.innerText);
});
setTimeout(() => {
console.log("宏任务代码中拿到的: ", newdiv.innerText);
}, 2000);
console.log("同步代码中拿到: ", newdiv.innerText); // 证明JS操作DOM是同步执行的
console.log("同步代码执行完"); // JS同步代码执行完后,在微任务和宏任务还没有执行完的时候,页面上已经更新了。说明渲染进程跟任务队列没关系。
怎么判断页面是否加载完成?
- DOMContentLoaded 事件在页面文档加载解析完毕之后马上执行, 不需要等待 CSS,JS,图片加载。
- load 事件在页面所有资源被加载进来之后才会触发,在 DOMContentLoaded 事件触发之后。
用户输入 url 到页面呈现的过程
- 用户输入 url
- 域名解析(DNS 解析),浏览器查找域名的 IP 地址
- 找到 IP 地址后,TCP 三次握手,与目标服务器建立连接
- 建立连接后,浏览器向目标主机发送 http 请求
- 服务器处理收到的请求,将 html 文档返回至浏览器
- 浏览器 html 文档,关闭连接,开始解析文档
- 解析 HTML 生成 DOM 树
- 解析 CSS 生成 CSSOM 树,构建 Style Rules
- DOM 和 CSSOM 树合成渲染树,RenderTree
- Layout,布局,计算节点的大小和位置信息
- painting,绘制,渲染在屏幕上
输入 lishuxue.site 的 URL 时,DNS 怎么解析
- 首先,浏览器会查询本地 DNS 缓存是否有该域名的 IP 地址记录。如果有,则直接返回该 IP 地址;否则,继续执行下一步。
- 浏览器会向本地 DNS 服务器发送查询请求。本地 DNS 服务器通常由网络服务提供商或者本地路由器等设备提供,可以通过 DHCP 自动获取到其地址信息。
- 如果本地 DNS 服务器缓存中有该域名的 IP 地址记录,则直接返回该 IP 地址;否则,本地 DNS 服务器会向根域名服务器(.com, .org, .net 等)发送查询请求,获取 lishuxue.site 域名所在的顶级域名服务器地址信息。
- 根域名服务器会返回包含 .site 顶级域名服务器地址的响应。本地 DNS 服务器在收到响应后,会向 .site 顶级域名服务器发送查询请求,获取 lishuxue.site 域名所在的次级域名服务器(.lishuxue)地址信息。
- 次级域名通常是由域名注册商或者 DNS 解析服务商提供的服务。次级域名服务器会返回包含该域名对应的 IP 地址信息的响应。本地 DNS 服务器在收到响应后,将该 IP 地址记录到缓存中,并返回该 IP 地址给浏览器。
回流/重排(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 等)
clientWidth 这些值既然设置的时候已经触发回流了, 为啥获取的时候要重新触发一遍
这些属性或方法都需要得到最新的布局信息,所以浏览器必须去回流执行。因此,在项目中,尽量避免使用上述属性或方法,如果非要使用的时候,也尽量将值缓存起来,而不是一直获取。
减少重绘和回流
JavaScript:
- 避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性
-
避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中
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 操作不会引发回流和重绘
- 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来
- 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流
CSS:
- 避免使用 table 布局。
- 尽可能在 DOM 树的最末端改变 class。
- 避免设置多层内联样式。
- 将动画效果应用到 position 属性为 absolute 或 fixed 的元素上
- 避免使用 CSS 表达式(例如:calc())
- 使用 CSS3 硬件加速(GPU 加速)
什么是浏览器同源策略?
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。
同源是指:协议、域名、端口都相同。
同源策略限制内容有:
- AJAX 请求
- Cookie、LocalStorage、IndexedDB 等存储性内容
- DOM 节点
但是有三个标签是允许跨域加载资源:
<script src=XXX><img src=XXX><link href=XXX>
跨域
| 由于同源策略的限制,如果访问一个协议 | 域名 | 端口号不同的资源,即为跨域访问。 |
跨域只存在于浏览器端,不存在于安卓/ios/Node.js/python/java 等其它环境。
简单请求和非简单请求
只要同时满足以下两大条件,就属于简单请求。
- 请求方法是:GET,POST 或者 HEAD
- 请求头中没有自定义 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 的是非简单请求
请求到底发出去了没有
跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。
- 对于简单请求
- 浏览器直接发出 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。
- 对于非简单请求
- 先发送一个 OPTIONS 预检请求,头信息中包含 Origin、Access-Control-Request-Method 和 Access-Control-Request-Headers 等字段
- 如果服务器确认允许该请求,响应信息会包含对应的 CORS 头
- 如果服务器否定了此预检请求,会返回一个正常的 HTTP 回应,但是没有任何 CORS 相关的头信息字段。浏览器会认为不允许并抛错。
为啥经常看不到预检请求
预检请求在 Chrome devtool 的 other 下,一般我们不去关注。如果点 All,也会看到。
模拟发送预检请求,先打开百度,打开 devtool,选择网络下面的 All:
const request = new XMLHttpRequest();
request.open("PUT", "https://lishuxue.site/blog-api/blog/list");
request.send();
解决跨域的方法
- 服务端开启跨域资源共享 Cross-Origin Resource Sharing(CORS),设置 Access-Control-Allow-Origin
- 客户端使用 JSONP,服务器端也要支持对应的 response
- 设置代理服务器(Node 中间件代理,Nginx 反向代理)
JSONP 的实现原理
利用 <script> 标签没有跨域限制的漏洞,缺点是只能发 get 请求,不安全可能会遭受 XSS 攻击
- 声明一个回调函数,函数形参为要获取的目标数据(服务器返回的 data)
- 创建一个
<script>标签,把那个跨域的 API 数据接口地址,赋值给 script 的 src,还要在这个地址中向服务器传递该回调函数名 - 服务器收到请求后,需要把传进来的回调函数名和真正的数据 data 拼接成一个字符串,然后返回给客户端
- 客户端会自动执行对应的回调函数,并将真正的 data 当做参数传进去
<script>
function jsonp(data) {
console.log(data)
}
</script>
<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script>
// 服务器响应
"jsonp({\"userid\":0,\"username\":\"null\"})"
代理服务器的原理
- 正向代理:客户端将请求发给自己的代理 server,server 再转发给目标 server.devserver 就是一个正向代理
- 反向代理:客户端直接发请求给目标 server. 但是目标 server 其实是一个代理服务器。他代理某个安全的或内网的 server 的请求。对于客户端并不知道自己访问的是一个代理。但此时代理服务器对外就表现为一个服务器。作用:
- 保证内网的安全
- 负载均衡,反向代理服务器收到请求,转发到不同的内网 server 去处理请求。
浏览器的缓存机制
三级缓存
- 先查找内存,如果内存中存在,从内存中加载;
- 如果内存中未查找到,选择硬盘获取,如果硬盘中有,从硬盘中加载;
- 如果硬盘中未查找到,那就进行网络请求;
- 加载到的资源缓存到硬盘和内存;
强缓存和协商缓存
强缓存
资源在第一次返回时 header 中有 expires 和 cache-control 信息,会将其缓存,下次请求时,如果命中则直接使用缓存中的资源不会再向服务器发送请求。命中表示:有缓存且没过期。
Cache-Control 与 Expires 可以在服务端配置同时启用,同时启用的时候 Cache-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- no-cache:需要进行协商缓存,发送请求到服务器确认是否使用缓存。
- no-store:禁止使用缓存,每一次都要重新请求数据。
- public:可以被所有的用户缓存,包括终端用户和 CDN 等中间代理服务器。
- private:只能被终端用户的浏览器缓存,不允许 CDN 等中继缓存服务器对其缓存。
协商缓存
当强制缓存失效(超过规定时间)时,就需要使用协商缓存,由服务器决定缓存内容是否失效。
资源在第一次返回时会携带 Last-Modified 或者 ETag 标识。下次请求时,浏览器会在请求头上加 If-Modified-Since 或者 If-None-Match。如果缓存有效,则返回 304 表示继续使用,于是客户端继续使用缓存;如果失效,则返回新的数据
短时间内资源发生了改变,Last-Modified 并不会发生变化。
周期性变化,如果这个资源在一个周期内修改回原来的样子了,我们认为是可以使用缓存的,但是 Last-Modified 可不这样认为,因此便有了 ETag。
Etag 的优先级高于 Last-Modified。Last-Modified 与 ETag 是可以一起使用的,服务器会优先验证 ETag,一致的情况下,会继续比对 Last-Modified,两个都匹配,返回 304,否则返回 200 和最新的响应体。
常见的浏览器内核有哪些?
浏览器内核又可以分成两部分:渲染引擎 和 JS 引擎。我们常说的 webkit 内核就是渲染引擎,v8 属于 js 引擎。JS 引擎越来越独立,内核就倾向于只指渲染引擎。
渲染引擎
- Trident(IE 内核)
- Gecko(firefox)
- webkit(Safari)
- Bink(Chrome)
js 引擎
- Mozilla 的 SpiderMonkey 引擎
- Apple 的 JavaScriptCore
- Google 的 V8 引擎
浏览器的主要组成部分是什么?
- 用户界面(包括地址栏、前进/后退按钮、书签菜单等。主窗口是显示的请求到的信息)
- 浏览器引擎(负责窗口管理、Tab 进程管理等)
- 渲染引擎(也叫内核,负责 HTML 解析、页面渲染)
- JS 引擎(JS 解释器,如 Chrome 和 Nodejs 采用的 V8)
- 网络(用于网络调用,比如 HTTP 请求)
- 数据存储。这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie,IndexDB 等。