总结

183 阅读7分钟

1.何时发生回流重绘?

  • 添加或者删除可见的dom元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距,内边距,边框,高度和宽度)
  • 内容发生变化,比说说文字发生改变或图片被另一个尺寸不相同的图片替换
  • 页面一开始的渲染(这个是避免不了的)
  • 浏览器的窗口尺寸的变化(因为回流是根据窗口的大小来计算元素的位置和大小)

如何优化?

  • 浏览器的优化机制-浏览器会将重排的操作放到队列中,直到一段时间或者到达一个阀值后才会一次性清空队列,但是当你获取布局信息操作的时候,会强制队列刷新(offsetTop,scrollTop,clientTop,getBoundingClientRect)

  • 最小化重绘和重排

    • 批量操作样式
    • 批量修改dom
    • 避免触发同步布局事件(循环设置width的时候访问了offsetWidth,那么最好提出来定义一个变量)
    • 对于复杂的动画效果,使用绝对定位让其脱离文档流
    • css3硬件加速(GPU) transform, opacity, filters 这些动画不会引起回流,不过对于动画的其他属性, 比如background-color还是会引起回流和重绘的,不过它还是可以提升这个动画的性能.

概念:

  • 回流:通过构造渲染树,我们将可见的dom节点以及它对应的样式结合起来, 可是我们还需要计算它们在设备视口(viewPort)中的确切位置和大小,这个计算的阶段就叫做回流.
  • 重绘:我们通过构造渲染树和回流阶段,知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置和大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘节点.
  • 回流比重绘的代价要高,因为回流的话,尺寸变化,可能影响父元素以及其相邻元素,整个都需要回流;如果说只是样式变化的,只需要重绘当前样式改变的元素即可;回流一定会造成重绘。
  1. 模仿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. 实现三列布局的方式
  • 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;
  1. vue3相对于vue2的优势
  • Composition API的引入,解决痛点:

      1. vue2中相关的代码逻辑分散到data、methods、computed,当我们中有很多块逻辑的话,就会掺杂到各个逻辑代码中间,当我们要修改或者添加一些功能时,就需要在这些模块中跳来跳去,这样会让我们开发者感觉到很痛苦
      • vue3改进后,一些相关功能模块统一放到一起,如果其他地方也会使用到此逻辑时,可以使用hooks的方式,把公共的提出去,这个要比vue2的mixin要好用的多。
      1. 响应式比较:vue2主要是使用Object.defineProperty进行对象属性的劫持,如果对象的属性是对象就需要递归调用,而vue3通过proxy直接代理对象,不需要遍历操作
      • Object.defineProperty对新增属性需要手动observe
      1. Teleport(传送门)可以把组件渲染到你想渲染的地方,不会受父组件UI的影响,同时又可以使用组件内部的状态
      1. v-model的绑定名称和事件的修改(:modelValue="" @update:modelValue="" 可以绑定名称v-model:visible="isVisible")vue2(:value="" @input="")
      1. TreeShaking 所有api都需要使用es module的引用方式进行具名引用,这样更利于webpack打包时候进行treeShaking,vue2中由于导出的是一个vue对象,打包器无法分辨这个对像中哪些属性未使用到。
    • Fragment(片段) vue2中template只允许有一个根节点,但是vue3中就没有此限制。
  1. 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()

  • ShareArrayBufferAtomics对象,用于从共享内存位置读取和写入 ES9新增特性

  • 异步迭代

  • Promise.finally()

  • Rest/Spread 属性

  • 正则表达式命名捕获组(Regular Expression Named Capture Groups)

  • 正则表达式反向断言(lookbehind)

  • 正则表达式dotAll模式

  • 正则表达式 Unicode 转义

  • 非转义序列的模板字符串 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算法的核心基于两种假设

  1. 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构。
  2. 同一层级的一组节点,他们可以通过唯一的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使用)等