浏览器的常见考点

问题一览

  • 加载页面和渲染过程
  • 渲染线程和 JS 引擎线程
  • 重绘和回流
  • 页面生命周期
  • property 和 attribute 区别
  • cookie、localStorage 以及 sessionStorage
  • AJAX && 跨域

加载页面和渲染过程

题目:浏览器从加载页面到渲染页面的过程。

① 加载过程

要点如下:

  1. DNS服务器解析域名的IP地址
  2. 建立TCP握手连接
  3. IP指向的服务器发送HTTP请求
  4. 服务器收到、处理并返回HTTP请求
  5. 浏览器获取返回内容

② 渲染过程

要点如下:

  1. 根据HTML代码生成DOM
  2. 根据CSS生成CSSDOM
  3. 将 DOM 树和 CSSOM 整合成 RenderTree
  4. 根据 RenderTree 开始渲染和展示
  5. 遇到script标签,会阻塞渲染

这个过程要注意<link>标签位置,以及script标签位置和HTML提供的async defer属性

渲染线程和 JS 引擎线程

浏览器中常见的线程有:渲染线程、JS 引擎线程、HTTP 线程等等。

例如,当我们打开一个 Ajax 请求的时候,就启动了一个 HTTP 线程。

同样地,我们可以用线程的只是解释:为什么直接操作 DOM 会变慢,性能损耗更大?因为 JS 引擎线程和渲染线程是互斥的。而直接操作 DOM 就会涉及到两个线程互斥之间的通信,所以开销更大。

除此之外,这还能解释为什么script标签为什么会阻塞 DOM 树渲染,毕竟 JS 是可以修改 DOM 的,如果 JS 执行的时候 UI 也工作,就有可能导致不安全的渲染

重绘和回流

重绘(repaint)和回流(reflow)会在样式节点变动时候出现,回流所需要的成本更高,回流一定会引重绘。

重绘是只一些元素更新属性,这些属性只影响外观,不影响布局。比如背景颜色、字体颜色等等。

回流是元素的尺寸、布局、可见等属性发生改变。会导致渲染树重新构造。比如窗口字体大小变化、样式表改动、元素内容(尤其是输入控件)、css 伪类激活、offsetWidth 等属性计算。

如何减少重绘回流?

  • 避免逐项更改样式。一次性更改**style属性,或者直接定义class**属性
  • 避免直接插入DOM**documentFragment上操作,然后再插入document**
  • 避免循环读取offsetWidth等属性。循环外存取
  • 避免复杂动画。利用绝对定位将其脱离文档流
  • 避免CSS选择符层级太多。尽量平级类名,参考 scss 中的**&**的用法
  • 为频繁重绘或者回流的节点设置图层:
    • iframevideo 等节点自动变为图层
    • 通过 3d 动画出发:transform: translate3d(0, 0, 0)
  • 提前通知浏览器:will-change 属性

页面生命周期

onloadDOMContentLoaded触发的先后顺序是什么?

页面声明周期的变化,会触发document上的readystatechange事件,用户可以通过document.readyState拿到当前的状态。

1
2
3
4
5
6
// 初始时候的readyState
console.log(document.readyState);
// 每次改变都打印readyState
document.addEventListener("readystatechange", () =>
console.log(document.readyState);
);

上面的代码在 Chrome 中的输出是:

  1. loading:加载 document
  2. interactive:document 加载成功,DOM 树构建完成
  3. complete:图像,样式表和框架之类的子资源完成加载

所以,**DOMContentLoaded是在onload**前进行的

  • DOMContentLoaded事件在 DOM 树构建完毕后被触发,我们可以在这个阶段使用 js 去访问元素。
    • asyncdefer的脚本可能还没有执行。
    • 图片及其他资源文件可能还在下载中。
  • load事件在页面所有资源被加载完毕后触发,通常我们不会用到这个事件,因为我们不需要等那么久。
  • beforeunload在用户即将离开页面时触发,它返回一个字符串,浏览器会向用户展示并询问这个字符串以确定是否离开。
  • unload~~~~在用户已经离开时触发,我们在这个阶段仅可以做一些没有延迟的操作,由于种种限制,很少被使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
document.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded");
});
window.addEventListener("load", () => {
console.log("load");
});
window.addEventListener("beforeunload", () => {
console.log("will unload");
});
/*
window.addEventListener("unload", () => {
console.log("unload");
});
*/

property 和 attribute 区别

①property

指的是属性:DOM 节点本质是 JS 对象,因此 property 可以理解成 JS 对象上的属性。而 property 改变,就是直接改变 JS 对象的属性。

比如<p>上有 style、className、nodeName 和 nodeType 等属性。

1
2
3
let p = $("p");
console.log(p.style.width, p.className);
console.log(p.nodeName, p.nodeType);

②attribute

attribute 是指 HTML 的属性,改变 attribute 就是针对 HTML 属性的 set 和 get,和 JS 对象无关。

常用的 API 就是:getAttributesetAttribute。常见的用法是setAttribute()来设置元素的style

1
2
3
let p = $("p");
p.setAttribute("data-name", "yuanxin");
console.log(p.getAttribute("data-name"));

cookie、localStorage 以及 sessionStorage

cookie

  • 大小限制为 4kb,主要用来保存登陆信息,一般会存储一段表示用户信息的数据。
  • 生命周期上,一般是服务器设置失效时间;如果是浏览器生成,默认是关闭浏览器后失效。
  • 每次会被携带在 http 头中,所以数据量过大的时候有性能问题。

localStorage:大小限制为 5MB,用于永久存储信息,也可以用于缓存 ajax 信息用于离线应用。它保存在浏览器,不参与与服务器的通信。

sessionStorage:与 localStorage 类似,不同的是信息不是永久存储,仅在当前会话下有效。关闭标签或者浏览器,都会清除。

AJAX

XMLHttpRequest

题目:不借助任何库实现XMLHttpRequest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let xhr = new XMLHttpRequest();
// readyState 为 4 和 status 为 200 的时候,是正常情况
// Step1: 监听状态
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
xhr.status === 200 && console.log(xhr.responseText);
}
};
// xhr.open(method: [get, post], url: string, async: [true, false])
// async: 默认是 true; 代表异步请求
// 如果async = false, 那么 xhr.send() 会阻塞
// Step2: 打开请求
xhr.open(
"GET",
"<http://localhost:5050/search/song?key=周杰伦&page=1&limit=10&vendor=qq>"
);
// Step3: 发送请求
xhr.send();

Fetch API

题目:介绍和使用fetch()

淘汰了写法不舒服的XMLHttpRequest,本身支持Promise回调,是 ES6 下的最佳 AJAX 实践。但是浏览器兼容不是太好,但几年后,估计就只剩它了!

1
2
3
4
5
6
7
8
9
10
11
12
const api = "<http://localhost:5050/search/song>";
const formData = new FormData();
formData.append("key", "周杰伦");
formData.append("page", 1);
formData.append("limit", 10);
formData.append("vendor", "qq");
fetch(api, {
method: "POST",
body: formData
})
.then(res => res.json())
.then(json => console.log(json));

注意koabodyparser不支持FormData解析(换用koa-better-body)。那么请用如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const api = "<http://localhost:5050/search/song>";
fetch(api, {
method: "POST",
body: JSON.stringify({
key: "周杰伦",
page: 1,
limit: 10,
vendor: "qq"
}),
headers: new Headers({
"Content-Type": "application/json"
})
})
.then(res => res.json())
.then(json => console.log(json));

实现跨域

题目:如何实现跨域?

目前我已知的方法有三个:

  • JSONP:通过script标签实现,但是只能实现GET请求
  • 代理转发:Webpack 的 dev 模式,配合proxy选项,启动一个前端服务器,实现代理转发
  • CORS:后端允许跨域资源共享,这是我最推荐的一种方法

代理转发请见《webpack4 系列教程》,CORS 请见 Koa 部分。这里实现一下 JSONP。

注意srcparamscallback属性,指定的是回调函数。实例的回调函数是:handleResponse()

1
2
3
4
5
6
7
8
9
10
// 定义回调函数
const handleResponse = data => {
console.log(data);
};
// 构造 script 标签
let script = document.createElement("script");
script.src =
"<https://api.douban.com/v2/book/search?q=javascript&count=1&callback=handleResponse>";
// 向document中添加 script 标签,并且发送GET请求
document.body.appendChild(script);