1.何时发生回流重绘?
- 添加或者删除可见的dom元素
- 元素的位置发生变化
- 元素的尺寸发生变化(包括外边距,内边距,边框,高度和宽度)
- 内容发生变化,比说说文字发生改变或图片被另一个尺寸不相同的图片替换
- 页面一开始的渲染(这个是避免不了的)
- 浏览器的窗口尺寸的变化(因为回流是根据窗口的大小来计算元素的位置和大小)
如何优化?
-
浏览器的优化机制-浏览器会将重排的操作放到队列中,直到一段时间或者到达一个阀值后才会一次性清空队列,但是当你获取布局信息操作的时候,会强制队列刷新(offsetTop,scrollTop,clientTop,getBoundingClientRect)
-
最小化重绘和重排
- 批量操作样式
- 批量修改dom
- 避免触发同步布局事件(循环设置width的时候访问了offsetWidth,那么最好提出来定义一个变量)
- 对于复杂的动画效果,使用绝对定位让其脱离文档流
- css3硬件加速(GPU) transform, opacity, filters 这些动画不会引起回流,不过对于动画的其他属性, 比如background-color还是会引起回流和重绘的,不过它还是可以提升这个动画的性能.
概念:
- 回流:通过构造渲染树,我们将可见的dom节点以及它对应的样式结合起来, 可是我们还需要计算它们在设备视口(viewPort)中的确切位置和大小,这个计算的阶段就叫做回流.
- 重绘:我们通过构造渲染树和回流阶段,知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置和大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘节点.
- 回流比重绘的代价要高,因为回流的话,尺寸变化,可能影响父元素以及其相邻元素,整个都需要回流;如果说只是样式变化的,只需要重绘当前样式改变的元素即可;回流一定会造成重绘。
- 模仿vue2的响应式原理
const Observer = function(data) {
// 循环修改为每个属性添加get set
for (let key in data) {
defineReactive(data, key);
}
}
const defineReactive = function(obj, key) {
// 局部变量dep,用于get set内部调用
const dep = new Dep();
// 获取当前值
let val = obj[key];
Object.defineProperty(obj, key, {
// 设置当前描述属性为可被循环
enumerable: true,
// 设置当前描述属性可被修改
configurable: true,
get() {
console.log('in get');
// 调用依赖收集器中的addSub,用于收集当前属性与Watcher中的依赖关系
dep.depend();
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
// 这里每个需要更新通过什么断定?dep.subs
dep.notify();
}
});
}
const observe = function(data) {
return new Observer(data);
}
const Vue = function(options) {
const self = this;
// 将data赋值给this._data,源码这部分用的Proxy所以我们用最简单的方式临时实现
if (options && typeof options.data === 'function') {
this._data = options.data.apply(this);
}
// 挂载函数
this.mount = function() {
new Watcher(self, self.render);
}
// 渲染函数
this.render = function() {
with(self) {
_data.text;
}
}
// 监听this._data
observe(this._data);
}
const Watcher = function(vm, fn) {
const self = this;
this.vm = vm;
// 将当前Dep.target指向自己
Dep.target = this;
// 向Dep方法添加当前Wathcer
this.addDep = function(dep) {
dep.addSub(self);
}
// 更新方法,用于触发vm._render
this.update = function() {
console.log('in watcher update');
fn();
}
// 这里会首次调用vm._render,从而触发text的get
// 从而将当前的Wathcer与Dep关联起来
this.value = fn();
// 这里清空了Dep.target,为了防止notify触发时,不停的绑定Watcher与Dep,
// 造成代码死循环
Dep.target = null;
}
const Dep = function() {
const self = this;
// 收集目标
this.target = null;
// 存储收集器中需要通知的Watcher
this.subs = [];
// 当有目标时,绑定Dep与Wathcer的关系
this.depend = function() {
if (Dep.target) {
// 这里其实可以直接写self.addSub(Dep.target),
// 没有这么写因为想还原源码的过程。
Dep.target.addDep(self);
}
}
// 为当前收集器添加Watcher
this.addSub = function(watcher) {
self.subs.push(watcher);
}
// 通知收集器中所的所有Wathcer,调用其update方法
this.notify = function() {
for (let i = 0; i < self.subs.length; i += 1) {
self.subs[i].update();
}
}
}
const vue = new Vue({
data() {
return {
text: 'hello world'
};
}
})
vue.mount(); // in get
vue._data.text = '123'; // in watcher update /n in get
- 实现三列布局的方式
- 1.三个元素全部左浮动,然后设置第一个div、设置第三个div为100px,然后设置第二个div为calc(100% - 200px)
- 2.父元素设置position: relative, 三个子组件都设置绝对定位,position: absolute; 左右两边都设置width为100px,然后中间元素设置左右各100px(left: 100px; right:100px),最右边元素设置right:0
- 3.父元素设置display:flex; 两边元素设置宽度为100px,中间元素设置flex:1;
- vue3相对于vue2的优势
-
Composition API的引入,解决痛点:
-
- vue2中相关的代码逻辑分散到data、methods、computed,当我们中有很多块逻辑的话,就会掺杂到各个逻辑代码中间,当我们要修改或者添加一些功能时,就需要在这些模块中跳来跳去,这样会让我们开发者感觉到很痛苦
- vue3改进后,一些相关功能模块统一放到一起,如果其他地方也会使用到此逻辑时,可以使用hooks的方式,把公共的提出去,这个要比vue2的mixin要好用的多。
-
- 响应式比较:vue2主要是使用Object.defineProperty进行对象属性的劫持,如果对象的属性是对象就需要递归调用,而vue3通过proxy直接代理对象,不需要遍历操作
- Object.defineProperty对新增属性需要手动observe
-
- Teleport(传送门)可以把组件渲染到你想渲染的地方,不会受父组件UI的影响,同时又可以使用组件内部的状态
-
- v-model的绑定名称和事件的修改(:modelValue="" @update:modelValue="" 可以绑定名称v-model:visible="isVisible")vue2(:value="" @input="")
-
- TreeShaking 所有api都需要使用es module的引用方式进行具名引用,这样更利于webpack打包时候进行treeShaking,vue2中由于导出的是一个vue对象,打包器无法分辨这个对像中哪些属性未使用到。
- Fragment(片段) vue2中template只允许有一个根节点,但是vue3中就没有此限制。
-
- ES6、ES7、ES8、ES9、ES10新增的特性 ES6新增的特性
-
类
-
模块化
-
箭头函数
-
函数参数默认值
-
模板字符串
-
解构赋值
-
延展操作符
-
对象属性简写
-
Promise
-
Let与Const ES7新增特性
-
Array.prototype.includes()
-
指数操作符 Math.pow(..) ES8新增特性
-
async/await
-
Object.values() -
Object.entries() -
String padding:
padStart()和padEnd(),填充字符串达到当前长度 -
函数参数列表结尾允许逗号
-
Object.getOwnPropertyDescriptors() -
ShareArrayBuffer和Atomics对象,用于从共享内存位置读取和写入 ES9新增特性 -
异步迭代
-
Promise.finally()
-
Rest/Spread 属性
-
正则表达式命名捕获组(Regular Expression Named Capture Groups)
-
正则表达式反向断言(lookbehind)
-
正则表达式dotAll模式
-
非转义序列的模板字符串 ES10的新特性
-
行分隔符(U + 2028)和段分隔符(U + 2029)符号现在允许在字符串文字中,与JSON匹配
-
更加友好的 JSON.stringify
-
新增了Array的
flat()方法和flatMap()方法 -
新增了String的
trimStart()方法和trimEnd()方法 -
Object.fromEntries() -
Symbol.prototype.description -
String.prototype.matchAll -
Function.prototype.toString()现在返回精确字符,包括空格和注释 -
简化
try {} catch {},修改catch绑定 -
新的基本数据类型
BigInt -
globalThis
-
import()
-
Legacy RegEx
-
私有的实例方法和访问器
5.v-for中为啥要使用key 原因是:因为v-for更新已渲染列表时,默认用就地复用策略,它会根据key值去判断某个值是否修改,如果修改则重新渲染这一项,反之,则直接复用之前的元素。(在开发中我们经常使用index作为key,其实是不合理的,如果在中间插入数据的话,就会造成之前选中的数据还是第一项,其实它已经变成第二项,这一点需要注意)
diff算法的核心基于两种假设
- 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构。
- 同一层级的一组节点,他们可以通过唯一的id进行区分。基于以上这两点假设,使得虚拟DOM的Diff算法的复杂度从O(n^3)降到了O(n)。
当某一层是列表节点数据时,就会遵从上面的两种假设进行虚拟dom更新,所以我们需要使用key来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点,总结一句话:key的作用主要是为了高效的更新虚拟DOM。
6.深拷贝 简单的深拷贝: JSON.parse(JSON.stringify)可以实现简单的深拷贝,但是有以下缺陷: 1.会忽略 undefined和symbol 2.不能序列话函数 3.无法拷贝不可枚举的属性 4.无法拷贝对象的原型链 5.拷贝RegExp引用类型会变成空对象 6.拷贝Date类型会变成字符串 7.对象NAN、Infinity、-Infinity会变成null 8.无法解决循环引用问题
手写一个深拷贝
const isComplexDataType = (obj) => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null);
const deepClone = function (obj, hash = new WeakMap()) {
if (obj.constructor === Date) {
return new Date(obj) // 日期对象直接返回一个新的日期对象
}
if (obj.constructor === RegExp){
return new RegExp(obj) //正则对象直接返回一个新的正则对象
}
//如果循环引用了就用 weakMap 来解决
if (hash.has(obj)) {
return hash.get(obj)
}
let allDesc = Object.getOwnPropertyDescriptors(obj)
//遍历传入参数所有键的特性
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
//继承原型链
hash.set(obj, cloneObj)
// 针对能够遍历对象的不可枚举属性以及 Symbol 类型,可以使用Reflect.ownKeys()
for (let key of Reflect.ownKeys(obj)) {
cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
}
return cloneObj
}
// 下面是验证代码
let obj = {
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: { name: '我是一个对象', id: 1 },
arr: [0, 1, 2],
func: function () { console.log('我是一个函数') },
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
[Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
enumerable: false, value: '不可枚举属性' }
);
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj // 设置loop成循环引用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)
7.浅拷贝
1.Object.assign()
- 它不会拷贝对象的继承属性;
- 它不会拷贝对象的不可枚举的属性;
- 可以拷贝
Symbol类型的属性
2.扩展运算符(let cloneObj = { ...obj })
- 问题和Object.assign()一样的问题
3.数组的拷贝 contact和slice
- arr.concat()
- arr.slice()
8.浏览器缓存总结
浏览器缓存分为强缓存和协商缓存。当客户端请求某个资源时,获取缓存的流程如下
- 先根据这个资源的一些 http header 判断它是否命中强缓存,先检查
Cache-Control,如果命中,则直接从本地获取缓存资源,不会发请求到服务器; - 当强缓存没有命中时,客户端会发送请求到服务器,服务器通过另一些request header验证这个资源是否命中协商缓存,称为http再验证,如果命中,服务器将请求返回,但不返回资源,而是返回304告诉客户端直接从缓存中获取,客户端收到返回后就会从缓存中获取资源;(服务器通过请求头中的
If-Modified-Since或者If-None-Match字段检查资源是否更新) - 强缓存和协商缓存共同之处在于,如果命中缓存,服务器都不会返回资源; 区别是,强缓存不对发送请求到服务器,但协商缓存会。
- 当协商缓存也没命中时,服务器就会将资源发送回客户端。
- 当 ctrl+f5 强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存;
- 当 f5刷新网页时,跳过强缓存,但是会检查协商缓存;
强缓存
- Expires(该字段是 http1.0 时的规范,值为一个绝对时间的 GMT 格式的时间字符串,代表缓存资源的过期时间)
- Cache-Control:max-age(该字段是 http1.1的规范,强缓存利用其 max-age 值来判断缓存资源的最大生命周期,它的值单位为秒)
协商缓
- Last-Modified(值为资源最后更新时间,随服务器response返回,即使文件改回去,日期也会变化)
- If-Modified-Since(通过比较两个时间来判断资源在两次请求期间是否有过修改,如果没有修改,则命中协商缓存)
- ETag(表示资源内容的唯一标识,随服务器response返回,仅根据文件内容是否变化判断)
- If-None-Match(服务器通过比较请求头部的If-None-Match与当前资源的ETag是否一致来判断资源是否在两次请求之间有过修改,如果没有修改,则命中协商缓存)
9.XSS攻击和CSRF
XSS:跨站脚本攻击,是一种网站应用程序的安全漏洞攻击,是代码注入的一种。常见方式是将恶意代码注入合法代码里隐藏起来,再诱发恶意代码,从而进行各种各样的非法活动
防范:记住一点 “所有用户输入都是不可信的”,所以得做输入过滤和转义,可以使用 CSP的方式处理(
CSP本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行) 通常可以通过两种方式来开启 CSP:
-
设置
HTTP Header中的Content-Security-Policy -
设置
meta标签的方式<meta http-equiv="Content-Security-Policy"> -
CSRF:跨站请求伪造,也称XSRF,是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。与XSS相比,XSS利用的是用户对指定网站的信任,CSRF利用的是网站对用户网页浏览器的信任。
防范:用户操作验证(验证码),额外验证机制(
token使用)等