Giter Club home page Giter Club logo

blog's People

Contributors

edwardzerb avatar

Stargazers

 avatar

Watchers

 avatar

blog's Issues

性能优化

性能领域的术语概念

F:first 的缩写 ,意思第一次
P: paint 的缩写,意思为绘制
C:contentful,意思为内容

FP(First Paint):首次绘制,页面在屏幕上首次发生视觉上变化的事件

FCP(First Contentful Paint):首次内容绘制,浏览器第一次在屏幕上渲染内容

FMP(First Meaniful Paint):首次有效绘制,表示页面的 “主要内容” 开始出现在屏幕上的时间点,该指标是测量用户加载体验的主要指标

LCP(Large Contentful Paint):表示可视区域中 内容 最大的可见元素开始出现在屏幕上的时间点

TTI(Time To Interactive):可交互时间,网页中第一次 完全达到可交互状态 的时间点。主线程的任务均不超过 50 毫秒(time slicing)。

TTFB(Time To First Byte):浏览器接受第一个字节时间

FCI(First CPU Idle):CPU 第一次空闲的时间,CPU空闲,说明主线程已经空闲下来了,此时就可以接受用户的响应了

FID(First Input Delay):首次输入延迟,可在TTI前开始与网页产生交互,也可能在TTI之后才与网页产生交互
可在 head 标签里注册一个事件(click、mousedown、keydown、touchstart、pointerdown),事件响应函数中使用当前时间减去时间对象被创建的时间

FP与FCP 的区别

  • FP重点是在于视觉上的变化,无论什么内容
  • FCP 指的是 浏览器首次绘制来自 DOM 的内容。例如:文本、图片、SVG、canvas
  • 两个时间可能是一致的,也有可能是 先 FP 后 FCP

关键资源:阻塞网页首次渲染的资源(FP)

DOM的生命周期

  • domLoading:表示开始解析第一批收到的 HTML 文档的字节
  • domInteractive:表示完成全部 HTML 的解析并且 DOM 构建完毕,defer 延迟的脚本就在这个阶段后,domContentLoaded前执行
  • domContentLoaded:HTML 已经加载、解析完毕
    如果没有解析器阻塞 JavaScript,DOMContentLoaded 事件会在 domInteractive 之后立即执行,defer 延迟的脚本就会阻塞
    大多 JavaScript 框架 会在执行自己的逻辑前等待这个事件的触发
  • domComplete:所有的处理都已经完成并且所有的附属资源都已经下载完毕
  • loadEvent:作为网页加载的最后一步以便触发附加的应用逻辑

前端流程

下面的流程就是前端的基本流程,根据这个流程,我们可以看看每一个阶段都可以做哪些性能优化

  • 项目打包编译
  • 输入 url 到 FP
    • DNS
    • 浏览器缓存
    • HTTP
    • CDN
    • 渲染路径(HTML、CSS、JavaScript)
  • FP/FCP
  • FMP
  • LCP
  • TTI
  • FCI

导致性能低的原因

  • 长任务
    • 由于js是单线程的,浏览器同一时间内只能执行一个任务
    • 所以,需要避免任务执行时间超过50ms
  • 像素管道
    • 保证每16.7 秒必须有一帧传输到屏幕上
  • 布局抖动
    • 原本像素管道中的每一步都是异步执行的,如果变成了设置了宽高,然后马上获取,就会变成同步
    • 其实导致这个发生原理就是因为把渲染路径从原本的异步变成了同步
  • 丢帧
    • 每个16.7ms就要输出一帧,要保证每帧都有输出就使用 requestAnimationFrame

解决方案

1.打包构建

1.1 预编译

类似vue,最终都是将tempalte编译成render 函数,预编译就可以省去在运行时才将template 编译成render函数

tree-shaking,在构建过程中,清楚无用代码,可以减少构建后文件的体积

/**
 * tree shaking: 去除无用代码(js、css)
 *   前提:1.必须使用ES6模块化
 *        2.开启production环境
 *   作用:减少代码体积
 *
 *  package.json中配置
 *    "sideEffects": false 所有代码都没有副作用(都可以进行 tree shaking)
 *      问题:可能会把 css/@babel/polyfill(副作用)文件干掉
 *    "sideEffects": ["*.css", "*.less"]
 */

1.2 code Spliting

两种方式:

  • 分离业务代码
  • 第三方库、按需加载
    // 在webpack 配置文件里面配置
  /**
   * 单入口:
   *  可以将 node_modules 中代码单独打包一个chunk 最终输出
   * 多入口:
   *  自动分析多入口chunk中,有没有公共的文件。如果有会打包成单独一个chunk,并且都引用这个chunk
   */
optimization: {
    splitChunks: {
      chunks: 'all',
      // 分割的 chunk 最小为30kb
      miniSize: 30 * 1024,
      // 没有最大限制
      maxSize: 0,
      // 要提取的chunk最少被引用1次
      minChunks: 1,
      // 按需加载时并行加载的文件的最大数量
      maxAsyncRequests: 5,
      // 入口js文件最大并行请求数量
      maxInitialRequests: 3,
      // 名称连接符
      automaticNameDelimiter: '~',
      // 可以使用命名规则
      name: true,
      cacheGroups: {
        // 分割chunk的组
        // node_moudules文件会被打包到 vendors 组的chunk中 --> vendors~xxx.js
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          // 优先级
          priority: -10
        },
        default: {
          // 要提取的chunk最少被引用2次
          minChunks: 2,
          // 优先级
          priority: -20,
          // 如果当前要打包的模块,和之前已经被提取的模块是同一个,就会复用,而不是重新打包
          reuseExistingChunk: true
        }
      }
    }
  }
  

1.3 import 按需加载

// 按需加载 import
/**
 * 在不配置多入口的情况下,如何将引用的js文件打包成独立的文件
 * 通过js代码,让某个文件被单独打包成一个chunk
 * import 动态导入语法:能将某个文件单独打包
 */

 import(/* webpackChunkName: 'test' */ './print)
  .then(({ mul, count }) => {
    // 文件加载成功
    console.log(mul(2, 5));
  })
  .catch(() => {
    console.log('文件加载失败')
  });

1.4 ssr 渲染 ? dll?

2.浏览器缓存

浏览器缓存:通过 HTTP 请求获取到的资源缓存在浏览器,分为强缓存、协商缓存;这是一个递进关系,先强缓存,缓存不命中再到协商缓存。

HTTP 1.0

  • 强缓存:Expires
  • 协商缓存:If-Modify-Since / Last-Modify

HTTP 1.1

  • 强缓存:Cache-Control
  • 协商缓存:If-none-match/ ETag

详情可看我另一篇文章:浏览器缓存

3.DNS

DNS(Domain Name System):域名系统,域名和 IP 地址相互映射的一个分布式数据库。

DNS Prefetching

浏览器会根据自定义的规则,先提前去解析后面可能会使用到的域名,提前解析,不需要等到要去加载资源的时候再去解析。默认的情况下,网页里的 a 标签里的href属性带的域名会自动去启用DNS Prefetching(不需要在 link 里手动设置),HTTPS 下该默认规则无效。HTTPS 下可以在 meta 标签上操作

<link rel="dns-prefetch" href="//test.com">

// 让 a 标签在 HTTPS 下也能使用 dns-prefetch
<meta http-equiv="x-dns-prefetch-control" content="on">

注意

  • 如果做了 js、服务端 重定向,没有在 link 手动设置,是不起作用的
  • DNS 预解析适合使用在网站里引用了大量其它域名的资源;如果所有资源都在单个域名下,Chrome 就已经做了DNS的缓存

4.HTTP

当强缓存没有命中的时候,就要去服务端获取数据。一个TCP连接下,(chrome下)同个域名的HTTP请求最大并发连接数是6个,多处的请求需要排队等候;其它的浏览器也都有限制。

有一个衡量网络性能的指标,RTT(Round Trip Time),客户端到服务端的往返时延,从发送端发送数据开始,到发送端收到来自接收端的确认,总共的耗时;TCP的传输大小也是有限制,一个RRT只能传输14KB的资源,对此我们要对这个RTT进行优化。

这个阶段从三个点来优化:

  • 减少关键资源个数
  • 降低关键资源大小
  • 降低关键资源的RTT次数

如何减少关键资源的个数

  • 首先,TCP连接下同个域名的HTTP请求个数最大为6,那么我们可以把一些静态资源通过CDN的方式来获取,不在同一个域名下
  • JavaScript 没有操作 DOM 或者 CSSOM 的可以使用defer、async
  • 不是在构建页面之前就要加载的 CSS ,添加媒体取下阻止显现标志
  • 以上两个操作就可以把资源变成非关键资源

如何减少关键资源的大小

  • 打包的时候,压缩 CSS 、Javascript 资源
  • 打包时,去掉js、css、html的注释

如何减少RTT的次数

影响 RTT 的因素,就是资源大小,资源数量,所以方式就是使用上面的两种方式结合。

3.CDN

CDN(Content Delivery Network):内容分发网络,由分布在不同地理位置的 Web 服务器组成。当服务器的地理位置距离我们越远,那传播的延迟就越高;而 CDN 就是让服务器距离客户端更近。

GLSB(全局负载均衡系统):

SLB(本地负载均衡系统):

使用了 CDN 作为缓存的流程:

  • 浏览器在缓存里面查找有没有DNS缓存,没有就继续在操作系统中找,再没,就向本地DNS发出请求
  • 本地DNS逐级向根服务器、顶级域名服务器、权限服务器发出请求,直到得到GLSB的IP地址
  • 本地DNS向GLSB发出请求,GLSB主要是根据本地DNS的IP地址判断用户的位置,筛选出距离用户较近的SLB,并把SLB的地址返回给本地DNS
  • 本地DNS将SLB的地址返回给浏览器,浏览器向其发起请求
  • SLB 根据浏览器请求的资源和地址,选出最优的缓存服务器地址发回给浏览器
  • 浏览器根据返回的地址重定向
  • 如果缓存服务器有资源,使用;无,就向源服务器请求资源,把资源发回给浏览器

4. 加载解析主要资源角度

当我们去把服务端里的数据请求回来以后,就需要进行 HTML 解析,加载关键资源(JS、CSS),我们来看看 JS 是如何影响DOM生成的。正常来说JavaScript文件的下载是同步的,会阻塞DOM的解析;chrome 在这里做了很多优化,在这里会开启预解析操作,当渲染引擎接受到字节流以后,就会开启一个预解析线程去分析HTML中包含的JS、CSS 文件,当解析到相关文件以后,就会提前下载这些文件。

由于JavaScript线程会阻塞DOM,可采纳以下策略进行相关优化:

  • 使用 CDN 加速 JavaScript 文件的加载
  • 压缩 JavaScript 的文件体积
  • 若是 JavaScript 中不操作 DOM、CSS,可开启async、defer

上面的优化是从文件加载的角度来看的,下面是从JS文件执行的角度来优化。

5.主动交互角度

长任务:主动交互的角度是从 TTI 开始,就是主要是保证用户在做交互的时候要保证流畅,不要长时间的占用主线程(尽量保证任务的执行时间小于50ms)。

两种技术方案:

  • web-worker
  • Time Slicing(时间切片)

5.1 Web Worker

相当于是开启了一个新的线程,把需要循环的任务放在当中执行,这样就不会占用主线层,缺点是无法操作 DOM

const testWorker = new Worker('./worker.js')
setTimeout(_ => {
  testWorker.postMessage({})
  testWorker.onmessage = function (ev) {
    console.log(ev.data)
  }
}, 5000)

// worker.js
self.onmessage = function () {
  const start = performance.now()
  while (performance.now() - start < 1000) {}
  postMessage('done!')
}

5.2 Time Slicing

Time Slicing 就是把一个长任务切割成多个执行时间短的任务,
核心的实现是调用 setTimeout 会将任务添加到宏任务中、yield 的可以暂停执行

function block() {
    test(function *() {
        const start = performance.now();
        while(performance.now() - start < 1000) {
            console.log(222);
            yield;
        }
        console.log('done!');
    })
}

setTimeout(block, 5000);

function test(gen) {
    if(typeof gen === 'function') gen = gen();
    if(!gen || typeof gen.next !== 'function') return
    
    // 立即执行函数
    (function next() {
        // 调用next,拿到返回来的 {value: , done: }
        const rest = gen.next();
        // 结束
        if(res.done) return;
        setTimeout(next);
    })()
}

6. 数据读取

  • 变量的查询,从局部作用域到全局作用域的搜索过程越长速度越慢
  • 对象嵌套越深,读取速度越慢
  • 对象在原型链上藏得越深,读取也越慢
  • 数组元素和对象成员比较慢
  • 局部变量和 对象字面量的读取速度较快
  • 可对对象成员进行缓存到局部变量

6. DOM

  • 防止同时对 DOM 就行写 读操作
  • 操作DOM 先提升文档流,修改完毕以后,再复原。这样只需要重排重绘两次(脱离与回归)
  • 使用事件委托

8. 流程控制

  • 减少迭代的次数,防止时间复杂度到 O(n2)
  • 普通的循环迭代速度优于基于函数(for..of,forEach...)迭代(5~8倍)
  • for...in会遍历原型链上的属性,少用
  • Map代替 if...else 、switch

响应式原理

响应式原理的流程图

响应式流程图

响应式原理

原理使用了数据劫持和发布订阅,通过Object.defineProperty() 来劫持各个属性的 getter 和 setter
数据变动时,发布消息给依赖收集器(Dep),去通知观察者(Watcher)
作出对应的回调,去更新视图或数据

代码实现

MVVM 作为绑定的入口,整合 Observer、Compile、Watcher 三者
通过Observer来监听 model 数据变化表,通过Compile来解析编译模版指令
最终利用Watcher搭起Observer、Compiler之间的通信桥梁,达到数据变化 =》视图更新
视图交互变化 =》数据model变更的双向绑定效果

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <h2>{{person.name}} --- {{person.age}}</h2>
      <h3>{{person.fav}}</h3>
      <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
      </ul>
      <h3>{{msg}}</h3>
      <div v-text="msg"></div>
      <div v-text="person.text"></div>
      <div v-html="htmlStr"></div>
      <div v-bind:class="className">sss</div>
      <input type="text" v-model="msg" />
      <button v-on:click="handleClick">点击 v-on</button>
      <button @click="handleClick">点击@</button>
    </div>
  </body>
  <script src="./Obeserver.js"></script>
  <script src="./MyVue.js"></script>
  <script>
    
    let vm = new MVue({
      data: {
        person: {
          name: "xfy",
          age: "18",
          fav: "study"
        },
        msg: "success",
        htmlStr: "亲爱的来了",
        className: "test"
      },
      el: "#app",
      methods: {
        handleClick() {
          console.log(this);
          this.$data.person.name = "tom";
        }
      }
    });
  </script>
</html>

MyVue

// compileUtil
const compileUtil = {
  getVal(expr, vm) {
    // [person, name]
    return expr.split('.').reduce((data, currentVal) => {
      return data[currentVal];
    }, vm.$data);
  },
  setVal(expr, vm, inputVal) {
    return expr.split('.').reduce((data, currentVal) => {
      data[currentVal] = inputVal;
    }, vm.$data);
  },
  getContentVal(expr, vm) {
    return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
      return this.getVal(args[1], vm);
    })
  },
  // expr: msg <div v-text="person.text"></div>
  // {{}}
  text(node, expr, vm) {
    let value;
    if (expr.indexOf('{{') !== -1) {
      // {person.name}} --- {{person.age}}
      value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
        // 绑定观察者,将来数据发生变化,触发这里的回调 进行更新
        new Watcher(vm, args[1], (newVal) => {
          this.updater.textUpdater(node, this.getContentVal(expr, vm));
        })
        return this.getVal(args[1], vm);
      })
    } else {
      value = this.getVal(expr, vm);
    }
    this.updater.textUpdater(node, value);
  },
  html(node, expr, vm) {
    const value = this.getVal(expr, vm);
    new Watcher(vm, expr, (newVal) => {
      this.updater.htmlUpdater(node, newVal);
    })
    this.updater.htmlUpdater(node, value);
  },
  model(node, expr, vm) {
    const value = this.getVal(expr, vm);

    // 绑定更新函数 数据 =》 视图
    new Watcher(vm, expr, (newVal) => {
      this.updater.modelUpdater(node, newVal);
    })

    // 视图 =》 数据 =》 视图
    node.addEventListener('input', (e) => {
      // 设置值
      this.setVal(expr, vm, e.target.value);

    })
    this.updater.modelUpdater(node, value);
  },
  on(node, expr, vm, eventName) {
    let fn = vm.$options.methods && vm.$options.methods[expr];
    node.addEventListener(eventName, fn.bind(vm));
  },
  bind(node, expr, vm, attrName) {
    const value = this.getVal(expr, vm);
    this.updater.bindUpdater(node, attrName, value);
  },
  // 更新的函数
  updater: {
    textUpdater(node, value) {
      node.textContent = value;
    },
    htmlUpdater(node, value) {
      node.innerHTML = value;
    },
    modelUpdater(node, value) {
      node.value = value;
    },
    bindUpdater(node, attrName, value) {
      // node.attributes[attrName] = value;
      node.setAttribute(attrName, value);
    }
  }
}

Compile

class Compile {
  constructor(el, vm) {
    this.el = this.isElementNode(el) ? el : document.querySelector(el);
    this.vm = vm;

    // 1.获取文档碎片对象 放入内存中会减少页面的回流和重绘
    const fragment = this.node2Fragment(this.el);

    // 2.编译模版
    this.compile(fragment);

    // 3.追加子元素到根元素
    this.el.appendChild(fragment);
  }

  compile(fragment) {
    // 1.获取子节点
    const childNodes = fragment.childNodes;

    [...childNodes].forEach(child => {
      // console.log(child);
      if (this.isElementNode(child)) {
        // 是元素节点
        // 编译元素节点
        this.compileElement(child);
      } else {
        this.compileText(child);
        // 文本
      }

      if (child.childNodes && child.childNodes.length) {
        this.compile(child);
      }
    });
  }

  // 编译元素节点
  compileElement(node) {
    // <div v-text='msg'></div>
    const attributes = node.attributes;
    [...attributes].forEach(attr => {
      const { name, value } = attr;
      if (this.isDirective(name)) { // 是一个指令
        const [, directive] = name.split('-'); // text html model on:click 
        const [dirName, eventName] = directive.split(':'); // text html model on
        // 更新数据 数据驱动视图
        compileUtil[dirName](node, value, this.vm, eventName);

        // 删除有指令的标签上的属性
        node.removeAttribute('v-' + directive);
      } else if (this.isEventName(name)) {  // @click='handleclick'
        let [, eventName] = name.split('@');
        compileUtil['on'](node, value, this.vm, eventName);
        // 删除有指令的标签上的属性
        node.removeAttribute('@' + eventName);
      }
    });

  }
  // 编译文本节点
  compileText(node) {
    // {{}} v-text
    const content = node.textContent;
    if (/\{\{(.+?)\}\}/.test(content)) {
      compileUtil['text'](node, content, this.vm);
    }
  }

  // @click
  isEventName(attrName) {
    return attrName.startsWith('@');
  }

  // 指令
  isDirective(attrName) {
    return attrName.startsWith('v-');
  }

  node2Fragment(el) {
    const f = document.createDocumentFragment();
    let firstChild;
    while (firstChild = el.firstChild) {
      f.appendChild(firstChild);
    }
    return f;
  }

  isElementNode(node) {
    return node.nodeType === 1;
  }

}

MyVue

class MyVue {
  constructor(options) {
    this.$data = options.data;
    this.$el = options.el;
    this.$options = options;

    if (this.$el) {
      // 1.实现一个数据观察者
      new Observer(this.$data);
      // 2.实现一个指令解析器
      new Compile(this.$el, this);
    }
  }
}


Observer
// Watcher
class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;
    // 先把旧值保存起来
    this.oldVal = this.getOldVal();
  }

  getOldVal() {
    // 初始化的时候
    Dep.target = this;
    const oldVal = compileUtil.getVal(this.expr, this.vm);
    // 如果这里不置为 null 的话
    // 那么后续每一次调用 expr(例如 vm.$data.hrmlStr) 这个属性
    // 都会往监听数组里面添加 watcher
    // 重点要记住,只有在 模版编译的时候才会创建 watcher(暂时)
    Dep.target = null;
    return oldVal;
  }

  update() {
    const newVal = compileUtil.getVal(this.expr, this.vm);
    if (newVal !== this.oldVal) {
      this.cb(newVal);
    }
  }
}

// Dep
class Dep {
  constructor() {
    this.subs = [];
  }

  // 收集观察者
  addSub(watcher) {
    this.subs.push(watcher);
  }

  // 通知观察者更新
  notify() {
    console.log('通知观察者', this.subs);
    this.subs.forEach(w => w.update());
  }
}

// Observer
class Observer {
  constructor(data) {
    this.observe(data);
  }

  observe(data) {
    if (data && typeof data === 'object') {
      Object.keys(data).forEach(key => {
        this.defineReactive(data, key, data[key]);
      });

    }
  }

  defineReactive(obj, key, value) {

    // 递归遍历
    this.observe(value);
    let dep = new Dep();
    // 劫持并监听所有的属性
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: false,
      get() {
        // 初始化(data初始化,wathcer初始化)
        // 订阅数据变化时,往 Dep 中添加观察者
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      set: (newVal) => {
        // 特殊情况,当遇到data.person 被重新赋值的时候,需要重新劫持
        this.observe(newVal);
        if (value !== newVal) {
          value = newVal;
        }
        // 告诉 Dep 通知变化
        dep.notify();
      }
    })
  }
}

数组监听

数组监听的步骤:
获取Array.prtotype 上的七个方法

  • push
  • pop
  • shift
  • unshift
  • splice
  • sort
  • reverse

然后监听根据数组原型创建出来的对象,把上面的方法传到key里面去
然后就把经过加工过的 ArrayMethods 添加到要监听的 arr.proto
加工的方法里面就可以把通过函数算出来的值进行响应式
(后期继续完善Array)

Virtual DOM

什么是虚拟DOM

虚拟DOM(Virtual DOM):

React、Vue都使用到了虚拟 DOM ,也为其带来了跨平台的能力(React-Native、Weex)。
虚拟 DOM 其实就是一个普通的JavaScript对象,包含了 tag、props、children 属性
本质上来说,就是使用JavaScript来描述DOM

<div id="app">
    <p class="test">hello vue</p>
</div>

// 把上面的 HTML 转换成虚拟DOM
{
    // 节点名称 p、ul、li
    tag: 'div',
    // 节点上的数据 attrs、class、style等等
    data: {
        id: 'app'
    },
    // 当前组件 vue 的实例
    context: {},
    // 当前节点的子节点列表
    children: [
        {
            tag: 'p',
            props: {
                class: 'test'
            },
            children: [
                'hello vue'
            ]
        }
    ]
}

为什么使用虚拟 DOM

当我们数据变化的时候,我们需要进行更新页面,三种方式:

  • 重建整个 DOM,重新渲染,很暴力很简单,但是效率很低,相当于重新渲染
  • 遍历整个 DOM ,匹配到数据已经发生变化的节点。操作困难,但是效率高
  • 数据发生变化,生成新的虚拟DOM,和旧的虚拟DOM比较,找到差异的地方,更新到 DOM 上,编程容易,效率较高

在 vue1.0 版本的时候,通过更细的粒度,一定程度上知道哪些节点使用了这个状态,当这些节点需要进行更新操作的时候,不需要去比对,可直接更新视图;因为粒度太细了,一个绑定对应一个watcher,当需要追踪的数据多时,那这个内存消耗就会非常大。

所以在 vue2.0 以后的版本,选择了一个中等粒度的方案,引入虚拟 DOM,组件级别就是 watcher 实例,此时,当一个组件内有多个节点使用到了某个状态,也就只有一个 watcher 在观察这个状态的变化。

Vue之所以能随意的调整绑定的粒度,主要还是因为使用了变化侦测。

vue中的虚拟DOM

vue会通过编译将模版转换成渲染函数(render),执行这个渲染函数以后就会得到一个虚拟节点树,这个虚拟节点树就可以用来渲染页面。

因为操作DOM的速度慢,避免在操作DOM上面损耗太多性能,虚拟DOM就可以在节点映射到视图中的时候,可以跟旧的虚拟节点(oldVnode)进行对比,找到需要更新的节点,然后在DOM上进行操作。

所以vue中的虚拟DOM主要是做了两件事:

  • 作为对应真实DOM的虚拟节点树
  • 更新视图的时候,新虚拟节点(new Vnode)和旧虚拟节点(old Vnode)进行比对

VNode 的类型

  • 注释节点
  • 文本节点
  • 元素节点
  • 组件节点
  • 函数式组件
  • 克隆节点(主要优化静态节点和插槽节点)

浏览器缓存

缓存过程

什么是浏览器缓存

浏览器缓存其实就是浏览器保存通过 HTTP 获取的所有资源,是浏览器将网络资源存储在本地的一种行为

缓存的资源存储在哪里

  1. memory cache

将资源缓存在 memory cache 中,下次再访问时,并不需要重新下载资源,可直接从缓存中获取
Webkit 已经支持 memoryCache

Webkit的资源分为两类:

  • 主资源,类似 HTML 页面、下载想
  • 派生资源,HTML中内嵌的图片或脚本链接
  1. disk cache

DiskCache顾名思义,就是将资源缓存到磁盘中,等待下次访问时不需要重新下载资源,而直接从磁盘中获取,它的直接操作对象为CurlCacheManager。

*| memory cache | disk cache
---|---|---|---|
相同点 | 只存储一些派生资源文件 | 只存储一些派生资源文件
不同点 | 退出进程时,数据会被清除 | 退出进程时,数据不清除
存储资源 | 一般脚本、字体、图片会存在内存中 | 一般非脚本会存在内存中,如css

因为CSS文件加载一次就可渲染出来,我们不会频繁读取它,所以它不适合缓存到内存中,但是js之类的脚本却随时可能会执行,如果脚本在磁盘当中,我们在执行脚本的时候需要从磁盘取到内存中来,这样IO开销就很大了,有可能导致浏览器失去响应。

三级缓存原理(缓存访问的优先级)

  • 内存中查找,直接加载
  • 如果内存中不存在,则在硬盘中查找,如果有直接加载
  • 如果硬盘中也没有,那么就发起请求

请求获取到的资源缓存在内存和硬盘

浏览器缓存

  • 强缓存,如果本地资源过期
  • 协商缓存,进行协商缓存请求,如果过期,携带新的资源回来,命中缓存,返回304

优点

  • 减轻服务器压力
  • 客户端加载网页速度加快
  • 减少了冗余数据的传输

浏览器缓存的过程:
浏览器在加载资源时,会根据本地缓存资源的header 中的信息判断是否命中 强缓存,如果命中,则加载缓存中的资源,不会再发起请求

强缓存

Expires

  • http1.0 协议
  • 值为一个 绝对时间的 GMT 格式的时间字符串
  • Expires:Jan,22 Oct 2088 21:22:22 GMT
  • 在上面的时间之前都是命中缓存
  • 缺点:
    • 因为该资源是存储在本地,当本地时间和服务器时间偏差较大时,会导致缓存混乱

Cache-Control

  • http 1.1协议
  • 是一个相对时间
  • Cache-Control:max-age=3600,代表资源有效期是 3600
  • 常用的值
    • no-cache:需要进行协商缓存,发送请求到服务器是否使用缓存
    • no-store:禁止使用缓存,每一次都要重新请求数据
    • public:可以被所有终端用户缓存,包括代理服务器、CDN、客户端
    • private:只有终端用户能缓存
    • immutable:Cache-Control: max-age=3600, immutable
      • 表示资源在一小时内,即使用户刷新也不会发送请求
      • 这个字段只会返回200,不会有304返回
      • chrome不支持,因为chrome实现了防止重新加载时重新验证子资源
      • Cache-Control、Expires可同时配置,但是前者优先级更高

协商缓存

当协商缓存没有命中时,浏览器会发送一个请求给服务器,服务器会判断 header 中 部分信息来判断是否命中缓存,若命中,返回304

Last-Modify / If-Modify-Since

  • http 1.0协议
  • 浏览器第一次请求时,服务器会返回 一个添加了 Last-Modify 的header,标志最后的修改时间
  • 浏览器下次请求时 便携带 If-modify-Since,值为之前返回的 Last-Modify 的值
  • 缺点
    • 资源在短时间内发生改变,Last-Modify 不会发生变化
    • 周期性变化,资源变化以后,在一个周期以后,又修改回原本的样子,可以使用缓存,但是last-modify无法判断

ETag / If-None-Match

  • http 1.1协议
  • ETag 是一个哈希值,保证资源的唯一性,资源变化都会导致 ETag 变化
  • 和 Last-Modify 不同,当服务器返回 304 Not Modify的响应时,由于 ETag 重新生成过,Etag会重新赋值,即使没变化
  • 缺点
    • ETag的计算会比较消耗性能

Last-Modified 与 ETag 是可以一起使用的,服务器会优先验证 ETag,一致的情况下,才会继续比对 Last-Modified,最后才决定是否返回 304。

总结

当浏览器再次访问一个已经访问过的资源时,它会这样做:

  1. 看看是否命中强缓存,如果命中,就直接使用缓存了。
  2. 如果没有命中强缓存,就发请求到服务器检查是否命中协商缓存。
  3. 如果命中协商缓存,服务器会返回 304 告诉浏览器使用本地缓存。
  4. 否则,返回最新的资源。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.