两栏布局的实现
一般两栏布局指的是左边一栏宽度固定,右边一栏宽度自适应,两栏布局的具体实现:
- 利用浮动,将左边元素宽度设置为200px,并且设置向左浮动。将右边元素的margin-left设置为200px,宽度设置为auto(默认为auto,撑满整个父元素)。
.outer {
height: 100px;
}
.left {
float: left;
width: 200px;
background: tomato;
}
.right {
margin-left: 200px;
width: auto;
background: gold;
}
- 利用浮动,左侧元素设置固定大小,并左浮动,右侧元素设置overflow: hidden; 这样右边就触发了BFC,BFC的区域不会与浮动元素发生重叠,所以两侧就不会发生重叠。
.left{
width: 100px;
height: 200px;
background: red;
float: left;
}
.right{
height: 300px;
background: blue;
overflow: hidden;
}
- 利用flex布局,将左边元素设置为固定宽度200px,将右边的元素设置为flex:1。
.outer {
display: flex;
height: 100px;
}
.left {
width: 200px;
background: tomato;
}
.right {
flex: 1;
background: gold;
}
- 利用绝对定位,将父级元素设置为相对定位。左边元素设置为absolute定位,并且宽度设置为200px。将右边元素的margin-left的值设置为200px。
.outer {
position: relative;
height: 100px;
}
.left {
position: absolute;
width: 200px;
height: 100px;
background: tomato;
}
.right {
margin-left: 200px;
background: gold;
}
- 利用绝对定位,将父级元素设置为相对定位。左边元素宽度设置为200px,右边元素设置为绝对定位,左边定位为200px,其余方向定位为0。
.outer {
position: relative;
height: 100px;
}
.left {
width: 200px;
background: tomato;
}
.right {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 200px;
background: gold;
}
React 17 带来了哪些改变
最重要的是以下三点:
- 新的
JSX
转换逻辑 - 事件系统重构
Lane 模型
的引入
1. 重构 JSX 转换逻辑
在过去,如果我们在 React 项目中写入下面这样的代码:
代码语言:javascript复制function MyComponent() {
return <p>这是我的组件</p>
}
React 是会报错的,原因是 React 中对 JSX 代码的转换依赖的是 React.createElement
这个函数。因此但凡我们在代码中包含了 JSX,那么就必须在文件中引入 React,像下面这样:
import React from 'react';
function MyComponent() {
return <p>这是我的组件</p>
}
而 React 17 则允许我们在不引入 React 的情况下直接使用 JSX
。这是因为在 React 17 中,编译器会自动帮我们引入 JSX 的解析器,也就是说像下面这样一段逻辑:
function MyComponent() {
return <p>这是我的组件</p>
}
会被编译器转换成这个样子:
代码语言:javascript复制import {jsx as _jsx} from 'react/jsx-runtime';
function MyComponent() {
return _jsx('p', { children: '这是我的组件' });
}
react/jsx-runtime
中的 JSX 解析器将取代 React.createElement
完成 JSX
的编译工作,这个过程对开发者而言是自动化、无感知的。因此,新的 JSX 转换逻辑带来的最显著的改变就是降低了开发者的学习成本。
react/jsx-runtime
中的 JSX 解析器看上去似乎在调用姿势上和 React.createElement
区别不大,那么它是否只是 React.createElement
换了个马甲呢?当然不是,它在内部实现了 React.createElement
无法做到的性能优化和简化。在一定情况下,它可能会略微改善编译输出内容的大小
2. 事件系统重构
事件系统在 React 17 中的重构要从以下两个方面来看:
- 卸掉历史包袱
- 拥抱新的潮流
2.1 卸掉历史包袱:放弃利用 document 来做事件的中心化管控
React 16.13.x 版本中的事件系统会通过将所有事件冒泡到 document 来实现对事件的中心化管控
这样的做法虽然看上去已经足够巧妙,但仍然有它不聪明的地方——document 是整个文档树的根节点,操作 document 带来的影响范围实在是太大了,这将会使事情变得更加不可控
代码语言:javascript复制在 React 17 中,React 团队终于正面解决了这个问题:事件的中心化管控不会再全部依赖
document
,管控相关的逻辑被转移到了每个 React 组件自己的容器 DOM 节点中。比如说我们在 ID 为 root 的 DOM 节点下挂载了一个 React 组件,像下面代码这样:
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
那么事件管控相关的逻辑就会被安装到 root 节点
上去。这样一来, React 组件就能够自己玩自己的,再也无法对全局的事件流构成威胁了
2.2 拥抱新的潮流:放弃事件池
在 React 17 之前,合成事件对象会被放进一个叫作“事件池”的地方统一管理。这样做的目的是能够实现事件对象的复用,进而提高性能:每当事件处理函数执行完毕后,其对应的合成事件对象内部的所有属性都会被置空,意在为下一次被复用做准备。这也就意味着事件逻辑一旦执行完毕,我们就拿不到事件对象了,React 官方给出的这个例子就很能说明问题,请看下面这个代码
代码语言:javascript复制function handleChange(e) {
// This won't work because the event object gets reused.
setTimeout(() => {
console.log(e.target.value); // Too late!
}, 100);
}
异步执行的
setTimeout
回调会在handleChange
这个事件处理函数执行完毕后执行,因此它拿不到想要的那个事件对象e
。
要想拿到目标事件对象,必须显式地告诉 React——我永远需要它,也就是调用 e.persist()
函数,像下面这样:
function handleChange(e) {
// Prevents React from resetting its properties:
e.persist();
setTimeout(() => {
console.log(e.target.value); // Works
}, 100);
}
在 React 17 中,我们不需要 e.persist()
,也可以随时随地访问我们想要的事件对象。
3. Lane 模型的引入
初学 React 源码的同学由此可能会很自然地认为:优先级就应该是用 Lane 来处理的
。但事实上,React 16 中处理优先级采用的是 expirationTime 模型
。
expirationTime
模型使用expirationTime
(一个时间长度) 来描述任务的优先级;而Lane 模型
则使用二进制数来表示任务的优先级
:
lane 模型
通过将不同优先级赋值给一个位,通过 31 位的位运算
来操作优先级。
Lane 模型
提供了一个新的优先级排序的思路,相对于 expirationTime
来说,它对优先级的处理会更细腻,能够覆盖更多的边界条件。
变量提升
代码语言:javascript复制当执行
JS
代码时,会生成执行环境,只要代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境,只此两种执行环境。
b() // call b
console.log(a) // undefined
var a = 'Hello world'
function b() {
console.log('call b')
}
想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行环境时,会有两个阶段。第一个阶段是创建的阶段,
JS
解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为undefined
,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用
- 在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升
b() // call b second
function b() {
console.log('call b fist')
}
function b() {
console.log('call b second')
}
var b = 'Hello world'
var
会产生很多错误,所以在 ES6中引入了let
。let
不能在声明前使用,但是这并不是常说的let
不会提升,let
提升了,在第一阶段内存也已经为他开辟好了空间,但是因为这个声明的特性导致了并不能在声明前使用
OSI七层模型
ISO
为了更好的使网络应用更为普及,推出了OSI
参考模型。
(1)应用层
OSI
参考模型中最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:HTTP
,HTTPS
,FTP
,POP3
、SMTP
等。
- 在客户端与服务器中经常会有数据的请求,这个时候就是会用到
http(hyper text transfer protocol)(超文本传输协议)
或者https
.在后端设计数据接口时,我们常常使用到这个协议。 FTP
是文件传输协议,在开发过程中,个人并没有涉及到,但是我想,在一些资源网站,比如百度网盘``迅雷
应该是基于此协议的。SMTP
是simple mail transfer protocol(简单邮件传输协议)
。在一个项目中,在用户邮箱验证码登录的功能时,使用到了这个协议。
(2)表示层
表示层提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式。数据压缩和加密也是表示层可提供的转换功能之一。
在项目开发中,为了方便数据传输,可以使用base64
对数据进行编解码。如果按功能来划分,base64
应该是工作在表示层。
(3)会话层
会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。
(4)传输层
传输层建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。我们通常说的,TCP
UDP
就是在这一层。端口号既是这里的“端”。
(5)网络层
本层通过IP
寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的IP
层。这一层就是我们经常说的IP
协议层。IP
协议是Internet
的基础。我们可以这样理解,网络层规定了数据包的传输路线,而传输层则规定了数据包的传输方式。
(6)数据链路层
将比特组合成字节,再将字节组合成帧,使用链路层地址 (以太网使用MAC地址)来访问介质,并进行差错检测。
网络层与数据链路层的对比,通过上面的描述,我们或许可以这样理解,网络层是规划了数据包的传输路线,而数据链路层就是传输路线。不过,在数据链路层上还增加了差错控制的功能。
(7)物理层
实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。常用设备有(各种物理设备)集线器、中继器、调制解调器、网线、双绞线、同轴电缆。这些都是物理层的传输介质。
OSI七层模型通信特点:对等通信 对等通信,为了使数据分组从源传送到目的地,源端OSI模型的每一层都必须与目的端的对等层进行通信,这种通信方式称为对等层通信。在每一层通信过程中,使用本层自己协议进行通信。
参考:前端进阶面试题详细解答
== 操作符的强制类型转换规则?
对于 ==
来说,如果对比双方的类型不一样,就会进行类型转换。假如对比 x
和 y
是否相同,就会进行如下判断流程:
- 首先会判断两者类型是否相同,相同的话就比较两者的大小;
- 类型不相同的话,就会进行类型转换;
- 会先判断是否在对比
null
和undefined
,是的话就会返回true
- 判断两者类型是否为
string
和number
,是的话就会将字符串转换为number
1 == '1'
↓
1 == 1
- 判断其中一方是否为
boolean
,是的话就会把boolean
转为number
再进行判断
'1' == true
↓
'1' == 1
↓
1 == 1
- 判断其中一方是否为
object
且另一方为string
、number
或者symbol
,是的话就会把object
转为原始类型再进行判断
'1' == { name: 'js' } ↓'1' == '[object Object]'
Ajax
它是一种异步通信的方法,通过直接由 js 脚本向服务器发起 http 通信,然后根据服务器返回的数据,更新网页的相应部分,而不用刷新整个页面的一种方法。
面试手写(原生):
代码语言:javascript复制//1:创建Ajax对象
var xhr = window.XMLHttpRequest?new XMLHttpRequest():new ActiveXObject('Microsoft.XMLHTTP');// 兼容IE6及以下版本
//2:配置 Ajax请求地址
xhr.open('get','index.xml',true);
//3:发送请求
xhr.send(null); // 严谨写法
//4:监听请求,接受响应
xhr.onreadysatechange=function(){
if(xhr.readySate==4&&xhr.status==200 || xhr.status==304 )
console.log(xhr.responsetXML)
}
jQuery写法
代码语言:javascript复制$.ajax({
type:'post',
url:'',
async:ture,//async 异步 sync 同步
data:data,//针对post请求
dataType:'jsonp',
success:function (msg) {
},
error:function (error) {
}
})
promise 封装实现:
代码语言:javascript复制// promise 封装实现:
function getJSON(url) {
// 创建一个 promise 对象
let promise = new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();
// 新建一个 http 请求
xhr.open("GET", url, true);
// 设置状态的监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功或失败时,改变 promise 的状态
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
// 设置错误监听函数
xhr.onerror = function() {
reject(new Error(this.statusText));
};
// 设置响应的数据类型
xhr.responseType = "json";
// 设置请求头信息
xhr.setRequestHeader("Accept", "application/json");
// 发送 http 请求
xhr.send(null);
});
return promise;
}
数字证书是什么?
现在的方法也不一定是安全的,因为没有办法确定得到的公钥就一定是安全的公钥。可能存在一个中间人,截取了对方发给我们的公钥,然后将他自己的公钥发送给我们,当我们使用他的公钥加密后发送的信息,就可以被他用自己的私钥解密。然后他伪装成我们以同样的方法向对方发送信息,这样我们的信息就被窃取了,然而自己还不知道。为了解决这样的问题,可以使用数字证书。
首先使用一种 Hash 算法来对公钥和其他信息进行加密,生成一个信息摘要,然后让有公信力的认证中心(简称 CA )用它的私钥对消息摘要加密,形成签名。最后将原始的信息和签名合在一起,称为数字证书。当接收方收到数字证书的时候,先根据原始信息使用同样的 Hash 算法生成一个摘要,然后使用公证处的公钥来对数字证书中的摘要进行解密,最后将解密的摘要和生成的摘要进行对比,就能发现得到的信息是否被更改了。
这个方法最要的是认证中心的可靠性,一般浏览器里会内置一些顶层的认证中心的证书,相当于我们自动信任了他们,只有这样才能保证数据的安全。
介绍一下 Tree Shaking
对tree-shaking的了解
作用:
它表示在打包的时候会去除一些无用的代码
原理 :
ES6
的模块引入是静态分析的,所以在编译时能正确判断到底加载了哪些模块- 分析程序流,判断哪些变量未被使用、引用,进而删除此代码
特点:
- 在生产模式下它是默认开启的,但是由于经过
babel
编译全部模块被封装成IIFE
,它存在副作用无法被tree-shaking
掉 - 可以在
package.json
中配置sideEffects
来指定哪些文件是有副作用的。它有两种值,一个是布尔类型,如果是false
则表示所有文件都没有副作用;如果是一个数组的话,数组里的文件路径表示改文件有副作用 rollup
和webpack
中对tree-shaking
的层度不同,例如对babel
转译后的class
,如果babel
的转译是宽松模式下的话(也就是loose
为true
),webpack
依旧会认为它有副作用不会tree-shaking
掉,而rollup
会。这是因为rollup
有程序流分析的功能,可以更好的判断代码是否真正会产生副作用。
原理
ES6 Module
引入进行静态分析,故而编译的时候正确判断到底加载了那些模块- 静态分析程序流,判断那些模块和变量未被使用或者引用,进而删除对应代码
依赖于
import/export
通过导入所有的包后再进行条件获取。如下:
代码语言:javascript复制import foo from "foo";
import bar from "bar";
if(condition) {
// foo.xxxx
} else {
// bar.xxx
}
ES6的import语法完美可以使用tree shaking,因为可以在代码不运行的情况下就能分析出不需要的代码
CommonJS的动态特性模块意味着tree shaking不适用 。因为它是不可能确定哪些模块实际运行之前是需要的或者是不需要的。在ES6中,进入了完全静态的导入语法:import。这也意味着下面的导入是不可行的:
代码语言:javascript复制// 不可行,ES6 的import是完全静态的
if(condition) {
myDynamicModule = require("foo");
} else {
myDynamicModule = require("bar");
}
setState原理分析
1. setState异步更新
- 我们都知道,
React
通过this.state
来访问state
,通过this.setState()
方法来更新state
。当this.setState()
方法被调用的时候,React
会重新调用render
方法来重新渲染UI
- 首先如果直接在
setState
后面获取state
的值是获取不到的。在React
内部机制能检测到的地方,setState
就是异步的;在React
检测不到的地方,例如setInterval
,setTimeout
,setState
就是同步更新的
因为
setState
是可以接受两个参数的,一个state
,一个回调函数。因此我们可以在回调函数里面获取值
setState
方法通过一个队列机制实现state
更新,当执行setState
的时候,会将需要更新的state
合并之后放入状态队列,而不会立即更新this.state
- 如果我们不使用
setState
而是使用this.state.key
来修改,将不会触发组件的re-render
。 - 如果将
this.state
赋值给一个新的对象引用,那么其他不在对象上的state
将不会被放入状态队列中,当下次调用setState
并对状态队列进行合并时,直接造成了state
丢失
1.1 setState批量更新的过程
在
react
生命周期和合成事件执行前后都有相应的钩子,分别是pre
钩子和post
钩子,pre
钩子会调用batchedUpdate
方法将isBatchingUpdates
变量置为true
,开启批量更新,而post
钩子会将isBatchingUpdates
置为false
isBatchingUpdates
变量置为true
,则会走批量更新分支,setState
的更新会被存入队列中,待同步代码执行完后,再执行队列中的state
更新。isBatchingUpdates
为true
,则把当前组件(即调用了setState
的组件)放入dirtyComponents
数组中;否则batchUpdate
所有队列中的更新- 而在原生事件和异步操作中,不会执行
pre
钩子,或者生命周期的中的异步操作之前执行了pre
钩子,但是pos
钩子也在异步操作之前执行完了,isBatchingUpdates
必定为false
,也就不会进行批量更新
enqueueUpdate
包含了React
避免重复render
的逻辑。mountComponent
和updateComponent
方法在执行的最开始,会调用到batchedUpdates
进行批处理更新,此时会将isBatchingUpdates
设置为true
,也就是将状态标记为现在正处于更新阶段了。isBatchingUpdates
为true
,则把当前组件(即调用了setState
的组件)放入dirtyComponents
数组中;否则batchUpdate
所有队列中的更新
1.2 为什么直接修改this.state无效
- 要知道
setState
本质是通过一个队列机制实现state
更新的。 执行setState
时,会将需要更新的state合并后放入状态队列,而不会立刻更新state
,队列机制可以批量更新state
。 - 如果不通过
setState
而直接修改this.state
,那么这个state
不会放入状态队列中,下次调用setState
时对状态队列进行合并时,会忽略之前直接被修改的state
,这样我们就无法合并了,而且实际也没有把你想要的state
更新上去
1.3 什么是批量更新 Batch Update
在一些
mv*
框架中,,就是将一段时间内对model
的修改批量更新到view
的机制。比如那前端比较火的React
、vue
(nextTick
机制,视图的更新以及实现)
1.4 setState之后发生的事情
setState
操作并不保证是同步的,也可以认为是异步的React
在setState
之后,会经对state
进行diff
,判断是否有改变,然后去diff dom
决定是否要更新UI
。如果这一系列过程立刻发生在每一个setState
之后,就可能会有性能问题- 在短时间内频繁
setState
。React
会将state
的改变压入栈中,在合适的时机,批量更新state
和视图,达到提高性能的效果
1.5 如何知道state已经被更新
代码语言:javascript复制传入回调函数
setState({
index: 1
}}, function(){
console.log(this.state.index);
})
代码语言:javascript复制在钩子函数中体现
componentDidUpdate(){
console.log(this.state.index);
}
2. setState循环调用风险
- 当调用
setState
时,实际上会执行enqueueSetState
方法,并对partialState
以及_pending-StateQueue
更新队列进行合并操作,最终通过enqueueUpdate
执行state
更新 - 而
performUpdateIfNecessary
方法会获取_pendingElement
,_pendingStateQueue
,_pending-ForceUpdate
,并调用receiveComponent
和updateComponent
方法进行组件更新 - 如果在
shouldComponentUpdate
或者componentWillUpdate
方法中调用setState
,此时this._pending-StateQueue != null
,就会造成循环调用,使得浏览器内存占满后崩溃
3 事务
- 事务就是将需要执行的方法使用
wrapper
封装起来,再通过事务提供的perform
方法执行,先执行wrapper
中的initialize
方法,执行完perform
之后,在执行所有的close
方法,一组initialize
及close
方法称为一个wrapper
。 - 那么事务和
setState
方法的不同表现有什么关系,首先我们把4
次setStat
e简单归类,前两次属于一类,因为它们在同一调用栈中执行,setTimeout
中的两次setState
属于另一类 - 在
setState
调用之前,已经处在batchedUpdates
执行的事务中了。那么这次batchedUpdates
方法是谁调用的呢,原来是ReactMount.js
中的_renderNewRootComponent
方法。也就是说,整个将React
组件渲染到DOM
中的过程就是处于一个大的事务中。而在componentDidMount
中调用setState
时,batchingStrategy
的isBatchingUpdates
已经被设为了true
,所以两次setState
的结果没有立即生效 - 再反观
setTimeout
中的两次setState
,因为没有前置的batchedUpdates
调用,所以导致了新的state
马上生效
4. 总结
- 通过
setState
去更新this.state
,不要直接操作this.state
,请把它当成不可变的 - 调用
setState
更新this.state
不是马上生效的,它是异步的,所以不要天真以为执行完setState
后this.state
就是最新的值了 - 多个顺序执行的
setState
不是同步地一个一个执行滴,会一个一个加入队列,然后最后一起执行,即批处理
absolute与fixed共同点与不同点
共同点:
- 改变行内元素的呈现方式,将display置为inline-block
- 使元素脱离普通文档流,不再占据文档物理空间
- 覆盖非定位文档元素
不同点:
- abuselute与fixed的根元素不同,abuselute的根元素可以设置,fixed根元素是浏览器。
- 在有滚动条的页面中,absolute会跟着父元素进行移动,fixed固定在页面的具体位置。
async/await对比Promise的优势
- 代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担
- Promise传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅
- 错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获⾮常冗余
- 调试友好,Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。
性能优化
DNS 预解析
DNS
解析也是需要时间的,可以通过预解析的方式来预先获得域名所对应的IP
<link rel="dns-prefetch" href="//blog.poetries.top">
缓存
- 缓存对于前端性能优化来说是个很重要的点,良好的缓存策略可以降低资源的重复加载提高网页的整体加载速度
- 通常浏览器缓存策略分为两种:强缓存和协商缓存
强缓存
代码语言:javascript复制实现强缓存可以通过两种响应头实现:
Expires
和Cache-Control
。强缓存表示在缓存期间不需要请求,state code
为200
Expires: Wed, 22 Oct 2018 08:41:00 GMT
代码语言:javascript复制
Expires
是HTTP / 1.0
的产物,表示资源会在Wed, 22 Oct 2018 08:41:00 GMT
后过期,需要再次请求。并且Expires
受限于本地时间,如果修改了本地时间,可能会造成缓存失效
Cache-control: max-age=30
Cache-Control
出现于HTTP / 1.1
,优先级高于Expires
。该属性表示资源会在30
秒后过期,需要再次请求
协商缓存
- 如果缓存过期了,我们就可以使用协商缓存来解决问题。协商缓存需要请求,如果缓存有效会返回 304
- 协商缓存需要客户端和服务端共同实现,和强缓存一样,也有两种实现方式
Last-Modified
和 If-Modified-Since
Last-Modified
表示本地文件最后修改日期,If-Modified-Since
会将Last-Modified
的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来- 但是如果在本地打开缓存文件,就会造成
Last-Modified
被修改,所以在HTTP / 1.1
出现了ETag
ETag
和 If-None-Match
ETag
类似于文件指纹,If-None-Match
会将当前ETag
发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且ETag
优先级比Last-Modified
高
选择合适的缓存策略
对于大部分的场景都可以使用强缓存配合协商缓存解决,但是在一些特殊的地方可能需要选择特殊的缓存策略
- 对于某些不需要缓存的资源,可以使用
Cache-control: no-store
,表示该资源不需要缓存 - 对于频繁变动的资源,可以使用
Cache-Control: no-cache
并配合ETag
使用,表示该资源已被缓存,但是每次都会发送请求询问资源是否更新。 - 对于代码文件来说,通常使用
Cache-Control: max-age=31536000
并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件
使用 HTTP / 2.0
- 因为浏览器会有并发请求限制,在
HTTP / 1.1
时代,每个请求都需要建立和断开,消耗了好几个RTT
时间,并且由于TCP
慢启动的原因,加载体积大的文件会需要更多的时间 - 在
HTTP / 2.0
中引入了多路复用,能够让多个请求使用同一个TCP
链接,极大的加快了网页的加载速度。并且还支持Header
压缩,进一步的减少了请求的数据大小
预加载
- 在开发中,可能会遇到这样的情况。有些资源不需要马上用到,但是希望尽早获取,这时候就可以使用预加载
- 预加载其实是声明式的
fetch
,强制浏览器请求资源,并且不会阻塞onload
事件,可以使用以下代码开启预加载
<link rel="preload" href="http://example.com">
预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好
预渲染
代码语言:html复制可以通过预渲染将下载的文件预先在后台渲染,可以使用以下代码开启预渲染
<link rel="prerender" href="http://poetries.com">
- 预渲染虽然可以提高页面的加载速度,但是要确保该页面百分百会被用户在之后打开,否则就白白浪费资源去渲染
总结
defer
和async
在网络读取的过程中都是异步解析defer
是有顺序依赖的,async
只要脚本加载完后就会执行preload
可以对当前页面所需的脚本、样式等资源进行预加载prefetch
加载的资源一般不是用于当前页面的,是未来很可能用到的这样一些资源
懒执行与懒加载
懒执行
- 懒执行就是将某些逻辑延迟到使用时再计算。该技术可以用于首屏优化,对于某些耗时逻辑并不需要在首屏就使用的,就可以使用懒执行。懒执行需要唤醒,一般可以通过定时器或者事件的调用来唤醒
懒加载
- 懒加载就是将不关键的资源延后加载
懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。对于图片来说,先设置图片标签的
src
属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为src
属性,这样图片就会去下载资源,实现了图片懒加载
- 懒加载不仅可以用于图片,也可以使用在别的资源上。比如进入可视区域才开始播放视频等
文件优化
图片优化
对于如何优化图片,有 2 个思路
- 减少像素点
- 减少每个像素点能够显示的颜色
图片加载优化
- 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用
CSS
去代替。 - 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片
- 小图使用
base64
格式 - 将多个图标文件整合到一张图片中(雪碧图)
- 选择正确的图片格式:
- 对于能够显示
WebP
格式的浏览器尽量使用WebP
格式。因为WebP
格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好 - 小图使用
PNG
,其实对于大部分图标这类图片,完全可以使用SVG
代替 - 照片使用
JPEG
- 对于能够显示
其他文件优化
CSS
文件放在head
中- 服务端开启文件压缩功能
- 将
script
标签放在body
底部,因为JS
文件执行会阻塞渲染。当然也可以把script
标签放在任意位置然后加上defer
,表示该文件会并行下载,但是会放到HTML
解析完成后顺序执行。对于没有任何依赖的JS
文件可以加上async
,表示加载和渲染后续文档元素的过程将和JS
文件的加载与执行并行无序进行。 执行JS
代码过长会卡住渲染,对于需要很多时间计算的代码 - 可以考虑使用
Webworker
。Webworker
可以让我们另开一个线程执行脚本而不影响渲染。
CDN
静态资源尽量使用
CDN
加载,由于浏览器对于单个域名有并发请求上限,可以考虑使用多个CDN
域名。对于CDN
加载静态资源需要注意CDN
域名要与主站不同,否则每次请求都会带上主站的Cookie
其他
使用 Webpack 优化项目
- 对于
Webpack4
,打包项目使用production
模式,这样会自动开启代码压缩 - 使用
ES6
模块来开启tree shaking
,这个技术可以移除没有使用的代码 - 优化图片,对于小图可以使用
base64
的方式写入文件中 - 按照路由拆分代码,实现按需加载
- 给打包出来的文件名添加哈希,实现浏览器缓存文件
监控
对于代码运行错误,通常的办法是使用
window.onerror
拦截报错。该方法能拦截到大部分的详细报错信息,但是也有例外
- 对于跨域的代码运行错误会显示
Script error
. 对于这种情况我们需要给script
标签添加crossorigin
属性 - 对于某些浏览器可能不会显示调用栈信息,这种情况可以通过
arguments.callee.caller
来做栈递归 - 对于异步代码来说,可以使用
catch
的方式捕获错误。比如Promise
可以直接使用 catch 函数,async await
可以使用try catch
- 但是要注意线上运行的代码都是压缩过的,需要在打包时生成
sourceMap
文件便于debug
。 - 对于捕获的错误需要上传给服务器,通常可以通过
img
标签的src
发起一个请求
如何根据chrome的timing优化
性能优化API
Performance
。performance.now()
与new Date()
区别,它是高精度的,且是相对时间,相对于页面加载的那一刻。但是不一定适合单页面场景window.addEventListener("load", "");
window.addEventListener("domContentLoaded", "");
Img
的onload
事件,监听首屏内的图片是否加载完成,判断首屏事件RequestFrameAnmation
和RequestIdleCallback
IntersectionObserver
、MutationObserver
,PostMessage
Web Worker
,耗时任务放在里面执行
检测工具
Chrome Dev Tools
Page Speed
Jspref
前端指标
代码语言:javascript复制window.onload = function(){
setTimeout(function(){
let t = performance.timing
console.log('DNS查询耗时 :' (t.domainLookupEnd - t.domainLookupStart).toFixed(0))
console.log('TCP链接耗时 :' (t.connectEnd - t.connectStart).toFixed(0))
console.log('request请求耗时 :' (t.responseEnd - t.responseStart).toFixed(0))
console.log('解析dom树耗时 :' (t.domComplete - t.domInteractive).toFixed(0))
console.log('白屏时间 :' (t.responseStart - t.navigationStart).toFixed(0))
console.log('domready时间 :' (t.domContentLoadedEventEnd - t.navigationStart).toFixed(0))
console.log('onload时间 :' (t.loadEventEnd - t.navigationStart).toFixed(0))
if(t = performance.memory){
console.log('js内存使用占比 :' (t.usedJSHeapSize / t.totalJSHeapSize * 100).toFixed(2) '%')
}
})
}
DNS预解析优化
dns解析是很耗时的,因此如果解析域名过多,会让首屏加载变得过慢,可以考虑dns-prefetch优化
DNS Prefetch
应该尽量的放在网页的前面,推荐放在 后面。具体使用方法如下:
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//www.zhix.net">
<link rel="dns-prefetch" href="//api.share.zhix.net">
<link rel="dns-prefetch" href="//bdimg.share.zhix.net">
request请求耗时
- 不请求,用cache(最好的方式就是尽量引用公共资源,同时设置缓存,不去重新请求资源,也可以运用PWA的离线缓存技术,可以帮助wep实现离线使用)
- 前端打包时压缩
- 服务器上的zip压缩
- 图片压缩(比如tiny),使用webp等高压缩比格式
- 把过大的包,拆分成多个较少的包,防止单个资源耗时过大
- 同一时间针对同一域名下的请求有一定数量限制,超过限制数目的请求会被阻塞。如果资源来自于多个域下,可以增大并行请求和下载速度
- 延迟、异步、预加载、懒加载
- 对于非首屏的资源,可以使用 defer 或 async 的方式引入
- 也可以按需加载,在逻辑中,只有执行到时才做请求
- 对于多屏页面,滚动时才动态载入图片
移动端优化
1. 概述
PC
优化手段在Mobile
侧同样适用- 在
Mobile
侧我们提出三秒种渲染完成首屏指标 - 基于第二点,首屏加载
3
秒完成或使用Loading
- 基于联通3G网络平均
338KB/s(2.71Mb/s
),所以首屏资源不应超过1014KB
Mobile
侧因手机配置原因,除加载外渲染速度也是优化重点- 基于第五点,要合理处理代码减少渲染损耗
- 基于第二、第五点,所有影响首屏加载和渲染的代码应在处理逻辑中后置
- 加载完成后用户交互使用时也需注意性能
2. 加载优化
加载过程是最为耗时的过程,可能会占到总耗时的
80%
时间,因此是优化的重点
2.1 缓存
使用缓存可以减少向服务器的请求数,节省加载时间,所以所有静态资源都要在服务器端设置缓存,并且尽量使用长
Cache
(长Cache
资源的更新可使用时间戳)
2.2 压缩HTML、CSS、JavaScript
减少资源大小可以加快网页显示速度,所以要对
HTML
、CSS
、JavaScript
等进行代码压缩,并在服务器端设置GZip
- a) 压缩(例如,多余的空格、换行符和缩进)
- b) 启用
GZip
2.3 无阻塞
写在
HTML
头部的JavaScript
(无异步),和写在HTML
标签中的Style
会阻塞页面的渲染,因此CSS
放在页面头部并使用Link
方式引入,避免在HTML
标签中写Style
,JavaScript
放在页面尾部或使用异步方式加载
2.4 使用首屏加载
首屏的快速显示,可以大大提升用户对页面速度的感知,因此应尽量针对首屏的快速显示做优化。
2.5 按需加载
将不影响首屏的资源和当前屏幕资源不用的资源放到用户需要时才加载,可以大大提升重要资源的显示速度和降低总体流量。
PS:按需加载会导致大量重绘,影响渲染性能
- a)
LazyLoad
- b) 滚屏加载
- c) 通过
Media Query
加载
2.6 预加载
大型重资源页面(如游戏)可使用增加
Loading
的方法,资源加载完成后再显示页面。但Loading
时间过长,会造成用户流失。
对用户行为分析,可以在当前页加载下一页资源,提升速度。
- a)可感知
Loading
- b)不可感知的
Loading
(如提前加载下一页)
2.7 压缩图片
图片是最占流量的资源,因此尽量避免使用他,使用时选择最合适的格式(实现需求的前提下,以大小判断),合适的大小,然后使用智图压缩,同时在代码中用
Srcset
来按需显示
PS:过度压缩图片大小影响图片显示效果
- a)使用智图( http://zhitu.tencent.com/ )
- b)使用其它方式代替图片(1. 使用
CSS3
2. 使用SVG
3. 使用IconFont
) - c)使用
Srcset
- d)选择合适的图片(1.
webP
优于JPG
2.PNG8
优于GIF
) - e)选择合适的大小(1. 首次加载不大于
1014KB
2. 不宽于640
(基于手机屏幕一般宽度))
2.8 减少Cookie
Cookie
会影响加载速度,所以静态资源域名不使用Cookie
。
2.9 避免重定向
重定向会影响加载速度,所以在服务器正确设置避免重定向。
2.10 异步加载第三方资源
第三方资源不可控会影响页面的加载和显示,因此要异步加载第三方资源
2.11 减少HTTP请求
因为手机浏览器同时响应请求为4个请求(
Android
支持4个,iOS
5后可支持6个),所以要尽量减少页面的请求数,首次加载同时请求数不能超过4个
- a)合并
CSS
、JavaScript
- b)合并小图片,使用雪碧图
3. 三、脚本执行优化
脚本处理不当会阻塞页面加载、渲染,因此在使用时需当注意
CSS
写在头部,JavaScript
写在尾部或异步- 避免图片和
iFrame
等的空Src
,空Src
会重新加载当前页面,影响速度和效率。 - 尽量避免重设图片大小
- 重设图片大小是指在页面、
CSS
、JavaScript
等中多次重置图片大小,多次重设图片大小会引发图片的多次重绘,影响性能 - 图片尽量避免使用
DataURL
,DataURL
图片没有使用图片的压缩算法文件会变大,并且要解码后再渲染,加载慢耗时长
4. CSS优化
尽量避免写在HTML标签中写
Style
属性
4.1 css3过渡动画开启硬件加速
代码语言:javascript复制.translate3d{
-webkit-transform: translate3d(0, 0, 0);
-moz-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
4.2 避免CSS表达式
CSS表达式的执行需跳出CSS树的渲染,因此请避免CSS表达式。
4.3 不滥用Float
Float在渲染时计算量比较大,尽量减少使用
4.4 值为0时不需要任何单位
为了浏览器的兼容性和性能,值为0
时不要带单位
5. JavaScript执行优化
5.1 减少重绘和回流
- 避免不必要的Dom操作
- 尽量改变
Class
而不是Style
,使用classList
代替className
- 避免使用
document.write
- 减少
drawImage
5.2 TOUCH事件优化
使用
touchstart
、touchend
代替click
,因快影响速度快。但应注意Touch
响应过快,易引发误操作
6. 渲染优化
6.1 HTML使用Viewport
Viewport可以加速页面的渲染,请使用以下代码
代码语言:javascript复制<meta name=”viewport” content=”width=device-width, initial-scale=1″>
6.2 动画优化
- 尽量使用
CSS3
动画 - 合理使用
requestAnimationFrame
动画代替setTimeout
- 适当使用
Canvas
动画5
个元素以内使用css
动画,5
个以上使用Canvas
动画(iOS8
可使用webGL
)
6.3 高频事件优化
Touchmove
、Scroll
事件可导致多次渲染
- 使用
requestAnimationFrame
监听帧变化,使得在正确的时间进行渲染 - 增加响应变化的时间间隔,减少重绘次数
6.4 GPU加速
CSS
中以下属性(CSS3 transitions
、CSS3 3D transforms
、Opacity
、Canvas
、WebGL
、Video
)来触发GPU
渲染,请合理使用
HTTPS通信(握手)过程
HTTPS的通信过程如下:
- 客户端向服务器发起请求,请求中包含使用的协议版本号、生成的一个随机数、以及客户端支持的加密方法。
- 服务器端接收到请求后,确认双方使用的加密方法、并给出服务器的证书、以及一个服务器生成的随机数。
- 客户端确认服务器证书有效后,生成一个新的随机数,并使用数字证书中的公钥,加密这个随机数,然后发给服 务器。并且还会提供一个前面所有内容的 hash 的值,用来供服务器检验。
- 服务器使用自己的私钥,来解密客户端发送过来的随机数。并提供前面所有内容的 hash 值来供客户端检验。
- 客户端和服务器端根据约定的加密方法使用前面的三个随机数,生成对话秘钥,以后的对话过程都使用这个秘钥来加密信息。
JavaScript 中如何进行隐式类型转换?
首先要介绍ToPrimitive
方法,这是 JavaScript 中每个值隐含的自带的方法,用来将值 (无论是基本类型值还是对象)转换为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象,其看起来大概是这样:
/*** @obj 需要转换的对象* @type 期望的结果类型*/
ToPrimitive(obj,type)
type
的值为number
或者string
。
(1)当type
为number
时规则如下:
- 调用
obj
的valueOf
方法,如果为原始值,则返回,否则下一步; - 调用
obj
的toString
方法,后续同上; - 抛出
TypeError
异常。
(2)当type
为string
时规则如下:
- 调用
obj
的toString
方法,如果为原始值,则返回,否则下一步; - 调用
obj
的valueOf
方法,后续同上; - 抛出
TypeError
异常。
可以看出两者的主要区别在于调用toString
和valueOf
的先后顺序。默认情况下:
- 如果对象为 Date 对象,则
type
默认为string
; - 其他情况下,
type
默认为number
。
总结上面的规则,对于 Date 以外的对象,转换为基本类型的大概规则可以概括为一个函数:
代码语言:javascript复制var objToNumber = value => Number(value.valueOf().toString())
objToNumber([]) === 0
objToNumber({}) === NaN
而 JavaScript 中的隐式类型转换主要发生在 、-、*、/
以及==、>、<
这些运算符之间。而这些运算符只能操作基本类型值,所以在进行这些运算前的第一步就是将两边的值用ToPrimitive
转换成基本类型,再进行操作。
以下是基本类型的值在不同操作符的情况下隐式转换的规则 (对于对象,其会被ToPrimitive
转换成基本类型,所以最终还是要应用基本类型转换规则):
string
类型变量时,两边的变量都会被隐式转换为字符串;其他情况下两边的变量都会被转换为数字。
1 '23' // '123'
1 false // 1
1 Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
'1' false // '1false'
false true // 1
-
、*
、操作符
NaN
也是一个数字
1 * '23' // 23
1 * false // 0
1 / 'aa' // NaN
- 对于
==
操作符
操作符两边的值都尽量转成number
:
3 == true // false, 3 转为number为3,true转为number为1
'0' == false //true, '0'转为number为0,false转为number为0
'0' == 0 // '0'转为number为0
- 对于
<
和>
比较符
如果两边都是字符串,则比较字母表顺序:
代码语言:javascript复制'ca' < 'bd' // false
'a' < 'b' // true
其他情况下,转换为数字再比较:
代码语言:javascript复制'12' < 13 // true
false > -1 // true
以上说的是基本类型的隐式转换,而对象会被ToPrimitive
转换为基本类型再进行转换:
var a = {}
a > 2 // false
其对比过程如下:
代码语言:javascript复制a.valueOf() // {}, 上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]",现在是一个字符串了
Number(a.toString()) // NaN,根据上面 < 和 > 操作符的规则,要转换成数字
NaN > 2 //false,得出比较结果
又比如:
代码语言:javascript复制var a = {name:'Jack'}
var b = {age: 18}
a b // "[object Object][object Object]"
运算过程如下:
代码语言:javascript复制a.valueOf() // {},上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]"
b.valueOf() // 同理
b.toString() // "[object Object]"
a b // "[object Object][object Object]"
HTTPS的特点
HTTPS的优点如下:
- 使用HTTPS协议可以认证用户和服务器,确保数据发送到正确的客户端和服务器;
- 使用HTTPS协议可以进行加密传输、身份认证,通信更加安全,防止数据在传输过程中被窃取、修改,确保数据安全性;
- HTTPS是现行架构下最安全的解决方案,虽然不是绝对的安全,但是大幅增加了中间人攻击的成本;
HTTPS的缺点如下:
- HTTPS需要做服务器和客户端双方的加密个解密处理,耗费更多服务器资源,过程复杂;
- HTTPS协议握手阶段比较费时,增加页面的加载时间;
- SSL证书是收费的,功能越强大的证书费用越高;
- HTTPS连接服务器端资源占用高很多,支持访客稍多的网站需要投入更大的成本;
- SSL证书需要绑定IP,不能再同一个IP上绑定多个域名。
HTTP前生今世
HTTP
协议始于三十年前蒂姆·伯纳斯 - 李的一篇论文HTTP/0.9
是个简单的文本协议,只能获取文本资源;HTTP/1.0
确立了大部分现在使用的技术,但它不是正式标准;HTTP/1.1
是目前互联网上使用最广泛的协议,功能也非常完善;HTTP/2
基于 Google 的SPDY
协议,注重性能改善,但还未普及;HTTP/3
基于 Google 的QUIC
协议,是将来的发展方向
::before 和 :after 的双冒号和单冒号有什么区别?
(1)冒号(:
)用于CSS3
伪类,双冒号(::
)用于CSS3
伪元素。
(2)::before
就是以一个子元素的存在,定义在元素主体内容之前的一个伪元素。并不存在于dom
之中,只存在在页面之中。
注意: :before
和 :after
这两个伪元素,是在CSS2.1
里新出现的。起初,伪元素的前缀使用的是单冒号语法,但随着Web
的进化,在CSS3
的规范里,伪元素的语法被修改成使用双冒号,成为::before
、::after
。
即时通讯的实现:短轮询、长轮询、SSE 和 WebSocket 间的区别?
短轮询和长轮询的目的都是用于实现客户端和服务器端的一个即时通讯。
短轮询的基本思路: 浏览器每隔一段时间向浏览器发送 http 请求,服务器端在收到请求后,不论是否有数据更新,都直接进行响应。这种方式实现的即时通信,本质上还是浏览器发送请求,服务器接受请求的一个过程,通过让客户端不断的进行请求,使得客户端能够模拟实时地收到服务器端的数据的变化。这种方式的优点是比较简单,易于理解。缺点是这种方式由于需要不断的建立 http 连接,严重浪费了服务器端和客户端的资源。当用户增加时,服务器端的压力就会变大,这是很不合理的。
长轮询的基本思路: 首先由客户端向服务器发起请求,当服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则到达一定的时间限制才返回。客户端 JavaScript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。长轮询和短轮询比起来,它的优点是明显减少了很多不必要的 http 请求次数,相比之下节约了资源。长轮询的缺点在于,连接挂起也会导致资源的浪费。
SSE 的基本思想: 服务器使用流信息向服务器推送信息。严格地说,http 协议无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息。也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 http 协议,目前除了 IE/Edge,其他浏览器都支持。它相对于前面两种方式来说,不需要建立过多的 http 请求,相比之下节约了资源。
WebSocket 是 HTML5 定义的一个新协议议,与传统的 http 协议不同,该协议允许由服务器主动的向客户端推送信息。使用 WebSocket 协议的缺点是在服务器端的配置比较复杂。WebSocket 是一个全双工的协议,也就是通信双方是平等的,可以相互发送消息,而 SSE 的方式是单向通信的,只能由服务器端向客户端推送信息,如果客户端需要发送信息就是属于下一个 http 请求了。
上面的四个通信协议,前三个都是基于HTTP协议的。
对于这四种即使通信协议,从性能的角度来看: WebSocket > 长连接(SEE) > 长轮询 > 短轮询 但是,我们如果考虑浏览器的兼容性问题,顺序就恰恰相反了: 短轮询 > 长轮询 > 长连接(SEE) > WebSocket 所以,还是要根据具体的使用场景来判断使用哪种方式。
同样是重定向,307,303,302的区别?
302是http1.0的协议状态码,在http1.1版本的时候为了细化302状态码⼜出来了两个303和307。 303明确表示客户端应当采⽤get⽅法获取资源,他会把POST请求变为GET请求进⾏重定向。 307会遵照浏览器标准,不会从post变为get。
vue3带来的新特性/亮点
1. 压缩包体积更小
当前最小化并被压缩的 Vue 运行时大小约为 20kB(2.6.10 版为 22.8kB)。Vue 3.0捆绑包的大小大约会减少一半,即只有10kB!
2. Object.defineProperty -> Proxy
Object.defineProperty
是一个相对比较昂贵的操作,因为它直接操作对象的属性,颗粒度比较小。将它替换为es6的Proxy
,在目标对象之上架了一层拦截,代理的是对象而不是对象的属性。这样可以将原本对对象属性的操作变为对整个对象的操作,颗粒度变大。- javascript引擎在解析的时候希望对象的结构越稳定越好,如果对象一直在变,可优化性降低,proxy不需要对原始对象做太多操作。
3. Virtual DOM 重构
vdom的本质是一个抽象层,用javascript描述界面渲染成什么样子。react用jsx,没办法检测出可以优化的动态代码,所以做时间分片,vue中足够快的话可以不用时间分片
- 传统vdom的性能瓶颈:
- 虽然 Vue 能够保证触发更新的组件最小化,但在单个组件内部依然需要遍历该组件的整个 vdom 树。
- 传统 vdom 的性能跟模版大小正相关,跟动态节点的数量无关。在一些组件整个模版内只有少量动态节点的情况下,这些遍历都是性能的浪费。
- JSX 和手写的 render function 是完全动态的,过度的灵活性导致运行时可以用于优化的信息不足
- 那为什么不直接抛弃vdom呢?
- 高级场景下手写
render function
获得更强的表达力 - 生成的代码更简洁
- 兼容2.x
- 高级场景下手写
vue的特点是底层为Virtual DOM,上层包含有大量静态信息的模版。为了兼容手写 render function,最大化利用模版静态信息,
vue3.0采用了动静结合的解决方案
,将vdom的操作颗粒度变小,每次触发更新不再以组件为单位进行遍历,主要更改如下
- 将模版基于动态节点指令切割为嵌套的区块
- 每个区块内部的节点结构是固定的
- 每个区块只需要以一个
Array
追踪自身包含的动态节点
vue3.0将 vdom 更新性能由与模版整体大小相关提升为与动态内容的数量相关
Vue 3.0 动静结合的 Dom diff
Vue3.0 提出动静结合的 DOM diff 思想,动静结合的 DOM diff其实是在预编译阶段进行了优化。之所以能够做到预编译优化,是因为 Vue core 可以静态分析 template,在解析模版时,整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签和文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。 借助预编译过程,Vue 可以做到的预编译优化就很强大了。比如在预编译时标记出模版中可能变化的组件节点,再次进行渲染前 diff 时就可以跳过“永远不会变化的节点”,而只需要对比“可能会变化的动态节点”。这也就是动静结合的 DOM diff 将 diff 成本与模版大小正相关优化到与动态节点正相关的理论依据。
4. Performance
vue3在性能方面比vue2快了2倍。
- 重写了虚拟DOM的实现
- 运行时编译
- update性能提高
- SSR速度提高
5. Tree-shaking support
vue3中的核心api都支持了tree-shaking,这些api都是通过包引入的方式而不是直接在实例化时就注入,只会对使用到的功能或特性进行打包(按需打包),这意味着更多的功能和更小的体积。
6. Composition API
vue2中,我们一般会采用mixin来复用逻辑代码,用倒是挺好用的,不过也存在一些问题:例如代码来源不清晰、方法属性等冲突。基于此在vue3中引入了Composition API(组合API),使用纯函数分隔复用代码。和React中的
hooks
的概念很相似
- 更好的逻辑复用和代码组织
- 更好的类型推导
<template>
<div>X: {{ x }}</div>
<div>Y: {{ y }}</div>
</template>
<script>
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
const useMouseMove = () => {
const x = ref(0);
const y = ref(0);
function move(e) {
x.value = e.clientX;
y.value = e.clientY;
}
onMounted(() => {
window.addEventListener("mousemove", move);
});
onUnmounted(() => {
window.removeEventListener("mousemove", move);
});
return { x, y };
};
export default defineComponent({
setup() {
const { x, y } = useMouseMove();
return { x, y };
}
});
</script>
7. 新增的三个组件Fragment、Teleport、Suspense
Fragment
在书写vue2时,由于组件必须只有一个根节点,很多时候会添加一些没有意义的节点用于包裹。Fragment组件就是用于解决这个问题的(这和React中的Fragment组件是一样的)。
这意味着现在可以这样写组件了。
代码语言:html复制/* App.vue */
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
<script>
export default {};
</script>
或者这样
代码语言:javascript复制// app.js
import { defineComponent, h, Fragment } from 'vue';
export default defineComponent({
render() {
return h(Fragment, {}, [
h('header', {}, ['...']),
h('main', {}, ['...']),
h('footer', {}, ['...']),
]);
}
});
Teleport
Teleport其实就是React中的Portal。Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
一个 portal 的典型用例是当父组件有 overflow: hidden 或 z-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框。
代码语言:html复制/* App.vue */
<template>
<div>123</div>
<Teleport to="#container">
Teleport
</Teleport>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
setup() {}
});
</script>
/* index.html */
<div id="app"></div>
<div id="container"></div>
Suspense
同样的,这和React中的Supense是一样的。
代码语言:html复制
Suspense
让你的组件在渲染之前进行“等待”,并在等待时显示 fallback 的内容
// App.vue
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
Loading...
</template>
</Suspense>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import AsyncComponent from './AsyncComponent.vue';
export default defineComponent({
name: "App",
components: {
AsyncComponent
}
});
</script>
// AsyncComponent.vue
<template>
<div>Async Component</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
const sleep = () => {
return new Promise(resolve => setTimeout(resolve, 1000));
};
export default defineComponent({
async setup() {
await sleep();
}
});
</script>
8. Better TypeScript support
在vue2中使用过TypesScript的童鞋应该有过体会,写起来实在是有点难受。vue3则是使用ts进行了重写,开发者使用vue3时拥有更好的类型支持和更好的编写体验。