1. 加载页面和渲染过程
题目:浏览器从加载页面到渲染页面的过程。
① 加载过程
要点如下:
DNS
服务器解析域名的IP
地址- 建立
TCP
握手连接 - 向
IP
指向的服务器发送HTTP
请求 - 服务器收到、处理并返回
HTTP
请求 - 浏览器获取返回内容
② 渲染过程
要点如下:
- 根据
HTML
代码生成DOM
树 - 根据
CSS
生成CSSDOM
- 将 DOM 树和 CSSOM 整合成 RenderTree
- 根据 RenderTree 开始渲染和展示
- 遇到
<script>
,会阻塞渲染
这个过程要注意<link>
标签位置,以及<script>
标签位置和HTML
提供的async
defer
属性
2. 渲染线程和 JS 引擎线程
浏览器中常见的线程有:渲染线程、JS 引擎线程、HTTP 线程等等。
例如,当我们打开一个 Ajax 请求的时候,就启动了一个 HTTP 线程。
同样地,我们可以用线程的只是解释:为什么直接操作 DOM 会变慢,性能损耗更大?因为 JS 引擎线程和渲染线程是互斥的。而直接操作 DOM 就会涉及到两个线程互斥之间的通信,所以开销更大。
除此之外,这还能解释为什么<script>
标签为什么会阻塞 DOM 树渲染,毕竟 JS 是可以修改 DOM 的,如果 JS 执行的时候 UI 也工作,就有可能导致不安全的渲染。
3. 重绘和回流
重绘(repaint)和回流(reflow)会在样式节点变动时候出现,回流所需要的成本更高,回流一定会引重绘。
重绘是只一些元素更新属性,这些属性只影响外观,不影响布局。比如背景颜色、字体颜色等等。
回流是元素的尺寸、布局、可见等属性发生改变。会导致渲染树重新构造。比如窗口字体大小变化、样式表改动、元素内容(尤其是输入控件)、css 伪类激活、offsetWidth 等属性计算。
如何减少重绘回流?
- 避免逐项更改样式。一次性更改
style
属性,或者直接定义class
属性 - 避免直接插入
DOM
。在documentFragment
上操作,然后再插入document
中 - 避免循环读取
offsetWidth
等属性。循环外存取 - 避免复杂动画。利用绝对定位将其脱离文档流
- 避免
CSS
选择符层级太多。尽量平级类名,参考 scss 中的&
的用法 为频繁重绘或者回流的节点设置图层:
iframe
、video
等节点自动变为图层- 通过 3d 动画出发:
transform: translate3d(0, 0, 0)
- 提前通知浏览器:
will-change
属性
4. 页面生命周期
onload
和DOMContentLoaded
触发的先后顺序是什么?
页面声明周期的变化,会触发document
上的readystatechange
事件,用户可以通过document.readyState
拿到当前的状态。
// 初始时候的readyState
console.log(document.readyState);
// 每次改变都打印readyState
document.addEventListener("readystatechange", () =>
console.log(document.readyState);
);
上面的代码在 Chrome 中的输出是:
- loading:加载 document
- interactive:document 加载成功,DOM 树构建完成
- complete:图像,样式表和框架之类的子资源完成加载
所以,DOMContentLoaded
是在onload
前进行的。
DOMContentLoaded
事件在 DOM 树构建完毕后被触发,我们可以在这个阶段使用 js 去访问元素。async
和defer
的脚本可能还没有执行。- 图片及其他资源文件可能还在下载中。
load
事件在页面所有资源被加载完毕后触发,通常我们不会用到这个事件,因为我们不需要等那么久。beforeunload
在用户即将离开页面时触发,它返回一个字符串,浏览器会向用户展示并询问这个字符串以确定是否离开。unload
在用户已经离开时触发,我们在这个阶段仅可以做一些没有延迟的操作,由于种种限制,很少被使用。
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");
});
*/
5. property 和 attribute 区别
①property
指的是属性:DOM 节点本质是 JS 对象,因此 property 可以理解成 JS 对象上的属性。而 property 改变,就是直接改变 JS 对象的属性。
比如<p>
上有 style、className、nodeName 和 nodeType 等属性。
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 就是:getAttribute
和setAttribute
。常见的用法是setAttribute()
来设置元素的style
。
let p = $("p");
p.setAttribute("data-name", "yuanxin");
console.log(p.getAttribute("data-name"));
6. cookie、localStorage 以及 sessionStorage
cookie:
- 大小限制为 4kb,主要用来保存登陆信息,一般会存储一段表示用户信息的数据。
- 生命周期上,一般是服务器设置失效时间;如果是浏览器生成,默认是关闭浏览器后失效。
- 每次会被携带在 http 头中,所以数据量过大的时候有性能问题。
localStorage:大小限制为 5MB,用于永久存储信息,也可以用于缓存 ajax 信息用于离线应用。它保存在浏览器,不参与与服务器的通信。
sessionStorage:与 localStorage 类似,不同的是信息不是永久存储,仅在当前会话下有效。关闭标签或者浏览器,都会清除。
7. AJAX
7.1 XMLHttpRequest
题目:不借助任何库实现
XMLHttpRequest
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();
7.2 Fetch API
题目:介绍和使用
fetch()
淘汰了写法不舒服的XMLHttpRequest
,本身支持Promise
回调,是 ES6 下的最佳 AJAX 实践。但是浏览器兼容不是太好,但几年后,估计就只剩它了!
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
)。那么请用如下代码。
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));
7.3 实现跨域
题目:如何实现跨域?
目前我已知的方法有三个:
- JSONP:通过
<script>
标签实现,但是只能实现GET
请求 - 代理转发:Webpack 的 dev 模式,配合
proxy
选项,启动一个前端服务器,实现代理转发 - CORS:后端允许跨域资源共享,这是我最推荐的一种方法
代理转发请见《webpack4 系列教程》,CORS 请见 Koa 部分。这里实现一下 JSONP。
注意:src
的params
中callback
属性,指定的是回调函数。实例的回调函数是:handleResponse()
// 定义回调函数
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);