Skip to content

Latest commit

 

History

History
2233 lines (1498 loc) · 93.2 KB

「历时8个月」10万字前端知识体系总结(前端框架+浏览器原理篇).md

File metadata and controls

2233 lines (1498 loc) · 93.2 KB

前言

本文是 10 万字前端知识体系总结 的第四篇

前三篇为基础知识篇算法篇工程化篇,如果还没有阅读,建议了解下

对于前端工程师来说,必须要有一个拿手的框架

Vue 和 React 没有好坏之分,熟悉其中一个,另一个也要学习,这样才有对比性,能帮助我们更好的理解他们

曾经做过一段时间的 Node 接口开发,主要是做抽奖类的活动。结合自己的实践,聊一聊对 Node、数据库、高并发的理解,说不定以后也可以和后端一起吹牛皮了

最后梳理下计算机网络与安全和浏览器原理中常见的面试题目

文章导图

第4篇.png

前端框架

Vue

手写 mini 版的 MVVM 框架

实现效果:2s 后修改姓名和年龄这两个值,页面响应式更新渲染

mvvm.gif

实现流程

1)定义observe函数,利用Object.defineProperty把 data 中的属性变成响应式的,同时给每一个属性添加一个dep对象(用来存储对应的watcher观察者

2)定义compile 函数,模板编译,遍历 DOM,遇到 mustache(双大括号{{}})形式的文本,则替换成 data.key 对应的值,同时将该 dom 节点添加到对应 key 值的 dep 对象中

3)当 data 的数据变化时,调用 dep 对象的update方法,更新所有观察者中的 dom 节点

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>vue的MVVM简单实现</title></head>
<body>
<div id="app">
  <p>姓名: <span>{{name}}</span></p>
  <p>年龄: <span>{{age}}</span></p>
</div>
<script>
  window.onload = function () {
    // new一个vue实例
    let vue = new Vue(
       {
         el: '#app',
         data: {
             name: '加载中', age: '18'
           }
         }
      )
    // 2s后更新页面的信息
    setTimeout(() => {
      // 修改vue中$data的name和age属性
      vue.$data.name = '小明';
      vue.$data.age = 20;
    }, 2000)
  }
  class Vue {
    constructor(options) {
      this.options = options
      this.$data = options.data
      this.observe(options.data)
      this.compile(document.querySelector(options.el))
    }
    // 监听data中属性的变化
    observe(data) {
      Object.keys(data).forEach(key => {
        // 给data中的每一个属性添加一个dep对象(该对象用来存储对应的watcher观察者)
        let observer = new Dep()
        // 利用闭包 获取和设置属性的时候,操作的都是value
        let value = data[key]
        Object.defineProperty(data, key, {
          get() {
            // 观察者对象添加对应的dom节点
            Dep.target && observer.add(Dep.target)
            return value
          },
          set(newValue) {
            value = newValue
            // 属性值变化时,更新观察者中所有节点
            observer.update(newValue)
          }
        })
      })
    }
    compile(dom) {
      dom.childNodes.forEach(child => {
        // nodeType 为3时为文本节点,并且该节点的内容包含`mustache`(双大括号{{}})
        if(child.nodeType === 3 && /\{\{(.*)\}\}/.test(child.textContent)) {
          // RegExp.$1是正则表达式匹配的第一个字符串,这里对应的就是data中的key值
          let key = RegExp.$1.trim()
          // 将该节点添加到对应的观察者对象中,在下面的的this.options.data[key]中触发对应的get方法
          Dep.target = child
          // 将{{key}} 替换成data中对应的值
          child.textContent = child.textContent.replace(`{{${key}}}`, this.options.data[key])
          Dep.target = null
        }
        // 递归遍历子节点
        if(child.childNodes.length) {
          this.compile(child)
        }
      })
    }
  }

  // dep对象存储所有的观察者
  class Dep {
    constructor() {
      this.watcherList = []
    }
    // 添加watcher
    add(node) {
      this.watcherList.push(node)
    }
    // 更新watcher
    update(value) {
      this.watcherList.forEach(node => {
        node.textContent= value
      })
    }
  }
</script>
</body>
</html>

50 行代码的 MVVM,感受闭包的艺术

手写 v-model 数据双向绑定

mvvm1.gif

和前文 mini 版 MVVM 框架的区别

1)实现v-model指令,input 值改变后,页面对应的数据也会变化,实现了数据的双向绑定

2)给 input 元素绑定input事件,当输入值变化会触发对应属性的dep.update方法,通知对应的观察者发生变化

3)增加了数据代理,通过this.info.person.name就可以直接修 $data对应的值,实现了this对this.$data的代理

4)数据劫持,对 data 增加了递归和设置新值的劫持,让 data 中每一层数据都是响应式的,如info.person.name

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>vue双向绑定的简单实现</title>
</head>
<body>
<div id="app">
  <div>年龄: <span>{{info.person.name}}</span></div>
  <p>{{job}}</p>
  <input v-model="job" placeholder="请输入工作" type="text">
</div>

<script>
  window.onload = function () {
    // new一个vue对象
    let vm = new Vue({
      // el为需要挂载的dom节点
      el: '#app',
      data: {
        info: {
          person: {
            name: '加载中'
          }
        },
        job: '程序猿'
      }
    })
    setTimeout(() => {
      vm.info.person.name = '小明'
    }, 2000)
  }

  class Vue {
    constructor(options) {
      this.$data = options.data
      this.$el = document.querySelector(options.el)
      observe(options.data)
      this.proxy(this.$data, this)
      this.compile(this.$el, this)
    }
    // 模板编译
    compile (dom, vm) {
      Array.from(dom.childNodes).forEach(child => {
        // 元素节点,匹配v-model,如input textArea元素等
        if (child.nodeType == 1) {
          Array.from(child.attributes).forEach(attr => {
            // 判断元素是否设置 v-model 属性
            if (attr.name.includes('v-model')) {
              Dep.target = child
              child.value = vm.$data[attr.value]
              Dep.target = null
              // 重点:给input原定绑定原生的input事件
              child.addEventListener('input', (e) => {
                // 当input输入内容发生变化时,动态设置vm.$data[attr.value]的值
                vm.$data[attr.value] = e.target.value
              })
            }
          })
        }
        if (child.nodeType === 3 && /\{\{(.*)\}\}/.test(child.textContent)) {
          let key = RegExp.$1.trim()
          let keyList = key.split('.')
          let value = vm.$data
          Dep.target = child

          // 循环遍历,找到info.person.name对应的name值
          keyList.forEach(item => {
            value = value[item]
          })
          Dep.target = null
          child.textContent = child.textContent.replace(`{{${key}}}`, value)
        }
        if (child.childNodes.length > 0) {
          // 递归模板编译
          this.compile(child, vm)
        }
      })
    }
    // this代理 this.$data
    // vm.info.person.name 相当于 vm.$data.info.person.name
    proxy ($data, vm) {
      Object.keys($data).forEach(key => {
        Object.defineProperty(vm, key, {
          set (newValue) {
            $data[key] = newValue
          },
          get () {
            return $data[key]
          }
        })
      })
    }
  }
  function observe (data) {
    if (data && typeof data == 'object') {
      return new Observe(data)
    }
  }
  // 递归进行数据劫持,使data中的每一层都是响应式的
  function Observe(data) {
    Object.keys(data).forEach(key => {
      let value = data[key]
      let dep = new Dep()
      // 递归
      observe(value)
      Object.defineProperty(data, key, {
        get () {
          Dep.target && dep.add(Dep.target)
          return value
        },
        set (newValue) {
          value = newValue
          // 如果新设置的值是一个对象,该对象也要变成响应式的
          observe(newValue)
          dep.update(newValue)
        }
      })
    })
  }

  class Dep {
    constructor() {
      this.subs = []
    }
    add (target) {
      this.subs.push(target)
    }
    update (newValue) {
      this.subs.forEach(node => {
        if (node.tagName == 'INPUT' || node.tagName == 'TEXTATEA') {
          node.value = newValue
        } else {
          node.textContent = newValue
        }
      })
    }
  }

</script>
</body>
</html>

手写 v-model 的 github 源码地址

使用 proxy 实现数据监听

vue3 底层通过Proxy实现了数据监听,替代了 vue2 中的Object.defineProperty

function observe(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      let result = Reflect.get(target, key);
      // 递归获取对象多层嵌套的情况,如pro.info.type(递归监听,保证每一层返回都是proxy对象)
      return isObject(result);
    },
    set(target, key, value, receiver) {
      if (key !== 'length') {
        // 解决对数组修改,重复更新视图的问题
        console.log('更新视图');
      }
      return Reflect.set(target, key, value, receiver);
    }
  });
}
function isObject(target) {
  if (typeof target === 'object' && target !== null) {
    return observe(target);
  } else {
    return target;
  }
}
let target = { name: '测试', info: { type: '1' } };
let pro = observe(target);
pro.info.type = 2; // 更新视图

vue 异步更新原理

Vue 的数据频繁变化,但为什么 dom 只会更新一次?

1)Vue 数据发生变化之后,不会立即更新 dom,而是异步更新的

2)侦听到数据变化,Vue 将开启一个队列,并缓存在同一事件循环中发生的所有数据变更

3)如果同一个 watcher 被多次触发,只会被推入到队列中一次,可以避免重复修改相同的 dom,这种去除重复数据,对于避免不必要的计算和 DOM 操作是非常重要的

4)同步任务执行完毕,开始执行异步 watcher 队列的任务,一次性更新 DOM

异步更新的源码实现

// 定义watcher类
class Watcher {
  update() {
    // 放到watcher队列中,异步更新
    queueWatcher(this);
  }
  // 触发更新
  run() {
    this.get();
  }
}

// 队列中添加watcher
function queueWatcher(watcher) {
  const id = watcher.id;
  // 先判断watcher是否存在 去掉重复的watcher
  if (!has[id]) {
    queue.push(watcher);
    has[id] = true;
    if (!pending) {
      pending = true;
      // 使用异步更新watcher
      nextTick(flushSchedulerQueue);
    }
  }
}

let queue = []; // 定义watcher队列
let has = {}; // 使用对象来保存id,进行去重操作
let pending = false; // 如果异步队列正在执行,将不会再次执行

// 执行watcher队列的任务
function flushSchedulerQueue() {
  queue.forEach((watcher) => {
    watcher.run();
    if (watcher.options.render) {
      // 在更新之后执行对应的回调: 这里是updated钩子函数
      watcher.cb();
    }
  });
  // 执行完成后清空队列 重置pending状态
  queue = [];
  has = {};
  pending = false;
}

nextTick 为什么要优先使用微任务实现?

1)vue nextTick 的源码实现,异步优先级判断,总结就是Promise > MutationObserver > setImmediate > setTimeout

2)优先使用 Promise,因为根据 event loop 与浏览器更新渲染时机,宏任务 → 微任务 → 渲染更新,使用微任务,本次 event loop 轮询就可以获取到更新的 dom

3)如果使用宏任务,要到下一次event loop中,才能获取到更新的 dom

nextTick 的源码实现

// 定义nextTick的回调队列
let callbacks = [];

// 批量执行nextTick的回调队列
function flushCallbacks() {
  callbacks.forEach((cb) => cb());
  callbacks = [];
  pending = false;
}

//定义异步方法,优先使用微任务实现
let timerFunc;

// 优先使用promise 微任务
if (Promise) {
  timerFunc = function () {
    return Promise.resolve().then(flushCallbacks);
  };
  // 如不支持promise,再使用MutationObserver 微任务
} else if (MutationObserver) {
  timerFunc = function () {
    const textNode = document.createTextNode('1');
    const observer = new MutationObserver(() => {
      flushCallbacks();
      observer.disconnect();
    });
    const observe = observer.observe(textNode, { characterData: true });
    textNode.textContent = '2';
  };
  // 微任务不支持,再使用宏任务实现
} else if (setImmediate) {
  timerFunc = function () {
    setImmediate(flushCallbacks);
  };
} else {
  timerFunc = function () {
    setTimeout(flushCallbacks);
  };
}

// 定义nextTick方法
export function nextTick(cb) {
  callbacks.push(cb);
  if (!pending) {
    pending = true;
    timerFunc();
  }
}

你真的理解$nextTick 么
Vue 源码详解之 nextTick:microtask 才是核心!

computed 和 watch 的区别

1)计算属性本质上是 computed watcher,而 watch 本质上是 user watcher(用户自己定义的 watcher)

2)computed 有缓存的功能,通过dirty控制

3)wacher 设置deep:true,实现深度监听的功能

4)computed 可以监听多个值的变化

computed 原理

1)初始化计算属性时,遍历 computed 对象,给其中每一个计算属性分别生成唯一computed watcher,并将该 watcher 中的dirty设置为 true

初始化时,计算属性并不会立即计算(vue 做的优化之一),只有当获取的计算属性值才会进行对应计算

2)初始化计算属性时,将Dep.target设置成当前的 computed watcher,将 computed watcher 添加到所依赖 data 值对应的dep中(依赖收集的过程),然后计算 computed 对应的值,后将 dirty 改成 false

3)当所依赖 data 中的值发生变化时,调用 set 方法触发 dep 的 notify 方法,将 computed watcher 中的 dirty 设置为 true

4)下次获取计算属性值时,若 dirty 为 true, 重新计算属性的值

5)dirty 是控制缓存的关键,当所依赖的 data 发生变化,dirty 设置为 true,再次被获取时,就会重新计算

computed 源码实现

// 空函数
const noop = () => {};
// computed初始化的Watcher传入lazy: true,就会触发Watcher中的dirty值为true
const computedWatcherOptions = { lazy: true };
//Object.defineProperty 默认value参数
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
};
// 初始化computed
class initComputed {
  constructor(vm, computed) {
    //新建存储watcher对象,挂载在vm对象执行
    const watchers = (vm._computedWatchers = Object.create(null));
    // 遍历computed
    for (const key in computed) {
      const userDef = computed[key];
      //getter值为computed中key的监听函数或对象的get值
      let getter = typeof userDef === 'function' ? userDef : userDef.get;
      // 新建computed watcher
      watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions);
      if (!(key in vm)) {
        // 定义计算属性
        this.defineComputed(vm, key, userDef);
      }
    }
  }

  // 重新定义计算属性  对get和set劫持
  // 利用Object.defineProperty来对计算属性的get和set进行劫持
  defineComputed(target, key, userDef) {
    // 如果是一个函数,需要手动赋值到get上
    if (typeof userDef === 'function') {
      sharedPropertyDefinition.get = this.createComputedGetter(key);
      sharedPropertyDefinition.set = noop;
    } else {
      sharedPropertyDefinition.get = userDef.get
        ? userDef.cache !== false
          ? this.createComputedGetter(key)
          : userDef.get
        : noop;
      // 如果有设置set方法则直接使用,否则赋值空函数
      sharedPropertyDefinition.set = userDef.set ? userDef.set : noop;
    }
    Object.defineProperty(target, key, sharedPropertyDefinition);
  }

  // 计算属性的getter 获取计算属性的值时会调用
  createComputedGetter(key) {
    return function computedGetter() {
      // 获取对应的计算属性watcher
      const watcher = this._computedWatchers && this._computedWatchers[key];
      if (watcher) {
        // dirty为true,计算属性需要重新计算
        if (watcher.dirty) {
          watcher.evaluate();
        }
        // 获取依赖
        if (Dep.target) {
          watcher.depend();
        }
        //返回计算属性的值
        return watcher.value;
      }
    };
  }
}

watch 原理

1)遍历watch对象, 给其中每一个 watch 属性,生成对应的user watcher

2)调用 watcher 中的 get 方法,将Dep.target设置成当前的 user watcher,并将 user watcher 添加到监听 data 值对应的 dep 中(依赖收集的过程)

3)当所监听 data 中的值发生变化时,会调用 set 方法触发 dep 的 notify 方法,执行 watcher 中定义的方法

4)设置成deep:true的情况,递归遍历所监听的对象,将user watcher添加到对象中每一层 key 值的 dep 对象中,这样无论当对象的中哪一层发生变化,wacher 都能监听到。通过对象的递归遍历,实现了深度监听功能

Vue.js 的 computed 和 watch 是如何工作的?
手写 Vue2.0 源码(十)-计算属性原理
珠峰:史上最全最专业的 Vue.js 面试题训练营

vue css scoped

css 属性选择器示例

 // 页面上 “属性选择器”这几个字显示红色
 <div data-v-hash class="test-attr">属性选择器</div>
  <style>
    /* 该标签有个data-v-hash的属性,只不过该属性为空,依然可以使用属性选择器 */
   .test-attr[data-v-hash] {
    color: red;
  }
  </style>
  <script>
     // 通过js判断是否存在 data-v-hash 属性
     console.log(document.querySelector('.test-attr').getAttribute('data-v-hash') === ''); // true
  </script>

vue css scoped 原理

1)编译时,会给每个 vue 文件生成一个唯一的 id,会将此 id 添加到当前文件中所有 html 的标签上

<div class="demo"></div>会被编译成<div class="demo" data-v-27e4e96e></div>

2)编译 style 标签时,会将 css 选择器改造成属性选择器,如.demo{color: red;}会被编译成.demo[data-v-27e4e96e]{color: red;}

虚拟 dom

什么是虚拟 dom?

Virtual DOM是 JS 模拟真实 DOM 节点,这个对象就是更加轻量级的对 DOM 的描述

为什么现在主流的框架都使用虚拟 dom?

1)前端性能优化的一个秘诀就是尽可能少地操作 DOM,频繁变动 DOM 会造成浏览器的回流或者重绘

2)使用虚拟 dom,当数据变化,页面需要更新时,通过 diff 算法,对新旧虚拟 dom 节点进行对比,比较两棵树的差异,生成差异对象,一次性对 DOM 进行批量更新操作,进而有效提高了性能

3)虚拟 DOM 本质上是 js 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便的跨平台操作,例如服务器渲染、weex 开发等等

虚拟 dom 与真实 dom 的相互转化
// 将真实dom转化为虚拟dom
function dom2Json(dom) {
  if (!dom.tagName) return;
  let obj = {};
  obj.tag = dom.tagName;
  obj.children = [];
  dom.childNodes.forEach((item) => {
    // 去掉空的节点
    dom2Json(item) && obj.children.push(dom2Json(item));
  });
  return obj;
}

// 虚拟dom包含三个参数  type, props, children
class Element {
  constructor(type, props, children) {
    this.type = type;
    this.props = props;
    this.children = children;
  }
}

// 将虚拟dom渲染成真实的dom
function render(domObj) {
  let el = document.createElement(domObj.type);
  Object.keys(domObj.props).forEach((key) => {
    // 设置属性
    let value = domObj.props[key];
    switch (key) {
      case 'value':
        if (el.tagName == 'INPUT' || el.tagName == 'TEXTAREA') {
          el.value = value;
        } else {
          el.setAttribute(key, value);
        }
        break;
      case 'style':
        el.style.cssText = value;
        break;
      default:
        el.setAttribute(key, value);
    }
  });
  domObj.children.forEach((child) => {
    child = child instanceof Element ? render(child) : document.createTextNode(child);
  });
  return el;
}

让虚拟 DOM 和 DOM-diff 不再成为你的绊脚石
虚拟 DOM 到底是什么?
详解 vue 的 diff 算法

vuex 原理

1)vuex 中的 store 本质就是一个没有template模板的隐藏式的 vue 组件

2)vuex 是利用 vue 的mixin混入机制,在beforeCreate钩子前混入 vuexInit 方法

3)vuexInit 方法实现将 vuex store 注册到当前组件的$store 属性上

4)vuex 的 state 作为一个隐藏的 vue 组件的 data,定义在 state 上面的变量,相当于这个 vue 实例的 data 属性,凡是定义在 data 上的数据都是响应式的

5)当页面中使用了 vuex state 中的数据,就是依赖收集的过程,当 vuex 中 state 中的数据发生变化,就通过调用对应属性的 dep 对象的 notify 方法,去修改视图变化

vuex 工作原理详解
Vuex 数据流动过程

vue-router 原理

1)创建的页面路由会与该页面形成一个路由表(key value 形式,key 为该路由,value 为对应的页面)

2)vue-router 原理是监听 URL 的变化,然后匹配路由规则,会用新路由的页面替换到老的页面 ,无需刷新

3)目前单页面使用的路由有两种实现方式: hash 模式history 模式

5)hash 模式(路由中带#号),通过hashchange事件来监听路由的变化
window.addEventListener('hashchange', ()=>{})

6)history 模式,利用了pushState()replaceState() 方法,实现往 history 中添加新的浏览记录、或替换对应的浏览记录

通过popstate事件来监听路由的变化,window.addEventListener('popstate', ()=>{})

前端路由简介以及 vue-router 实现原理
Vue Router 原理

vue3 与 vue2 的区别

1)vue3 性能比 Vue2.x 快 1.2~2 倍

2)使用 proxy 取代 Object.defineproperty,解决了 vue2 中新增属性监听不到的问题,同时 proxy 也支持数组,不需要像 vue2 那样对数组的方法做拦截处理

3)diff 方法优化
vue3 新增了静态标记(patchflag),虚拟节点对比时,就只会对比这些带有静态标记的节点

4)静态提升
vue3 对于不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用即可。vue2 无论元素是否参与更新,每次都会重新创建然后再渲染

5)事件侦听器缓存
默认情况下 onClick 会被视为动态绑定,所以每次都会追踪它的变化,但是因为是同一个函数,所以不用追踪变化,直接缓存起来复用即可

6)按需引入,通过 treeSharking 体积比 vue2.x 更小

7)组合 API(类似 react hooks),可以将 data 与对应的逻辑写到一起,更容易理解

8)提供了很灵活的 api 比如 toRef、shallowRef 等等,可以灵活控制数据变化是否需要更新 ui 渲染

9)更好的 Ts 支持

VUE3 对比 VUE2 的优势及新特性原理

React

vue 和 react 的区别

1)设计理念不同

react 整体上是函数式编程思想,组件使用jsx语法,all in js,将 html 与 css 全都融入 javaScript 中,jsx 语法相对来说更加灵活

vue 的整体思想,是拥抱经典的html(结构)+css(表现)+js(行为)的形式,使用 template 模板,并提供指令供开发者使用,如 v-if、v-show、v-for 等,开发时有结构、表现、行为分离的感觉

2)数据是否可变

vue 的思想是响应式的,通过 Object.defineproperty 或 proxy 代理实现数据监听,每一个属性添加一个 dep 对象(用来存储对应的 watcher),当属性变化的时候,通知对应的 watcher 发生改变

react 推崇的是数据不可变,react 使用的是浅比较,如果对象和数据的引用地址没有变,react 认为该对象没有变化,所以 react 变化时一般都是新创建一个对象

3)更新渲染方式不同

当组件的状态发生变化时,vue 是响应式,通过对应的 watcher 自动找到对应的组件重新渲染

react 需要更新组件时,会重新走渲染的流程,通过从根节点开始遍历,dom diff 找到需要变更的节点,更新任务还是很大,需要使用到 Fiber,将大任务分割为多个小任务,可以中断和恢复,不阻塞主进程执行高优先级的任务

4)各自的优势不同

vue 的优势包括:框架内部封装的多,更容易上手,简单的语法及项目创建, 更快的渲染速度和更小的体积

react 的优势包括: react 更灵活,更接近原生的 js、可操控性强,对于能力强的人,更容易造出更个性化的项目

React 与 Vue 的对比
关于 Vue 和 React 区别的一些笔记

react Hooks

可以在函数式组件中,获取 state、refs、生命周期钩子等其他特性

Hook 使用规则

1)只在最顶层使用 Hook,Hooks 底层使用链表存储数据,按照定义的 useState 顺序存储对应的数据,不要在循环、条件或嵌套函数中调用 Hook,否则 Hooks 的顺序会错乱

2)自定义 Hook 必须以 “use” 开头,如 useFriendStatus

3)在两个组件中使用相同的 Hook 不会共享 state,每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的

React Hooks 原理

为什么 vue 和 react 都选择了 Hooks

1)更好的状态复用

对于 vue2 来说,使用的是 mixin 进行混入,会造成方法与属性的难以追溯。 随着项目的复杂,文件的增多,经常会出现不知道某个变量在哪里引入的,几个文件间来回翻找, 同时还会出现同名变量,相互覆盖的情况……😥

2)更好的代码组织

vue2 的属性是放到 data 中,方法定义在 methods 中,修改某一块的业务逻辑时, 经常会出现代码间来回跳转的情况,增加开发人员的心智负担

使用Hooks后,可以将相同的业务逻辑放到一起,高效而清晰地组织代码

componentApi.jpg

3)告别 this

this 有多种绑定方式,存在显示绑定、隐式绑定、默认绑定等多种玩法,里边的坑不是一般的多

vue3 的 setup 函数中不能使用 this,不能用挺好,直接避免使用 this 可能会造成错误的

浅谈:为啥 vue 和 react 都选择了 Hooks🏂?

react Fiber

解决 react 旧版本,更新页面时会出现丢帧卡顿的问题

React 旧版本问题

当我们调用 setState 更新页面的时候,React 会遍历应用的所有节点,计算出差异,然后再更新 UI

整个过程是一气呵成,不能被打断的。如果页面元素很多,整个过程执行的时间可能超过 50 毫秒,就容易出现掉帧的现象

新版本解决方案

React Fiber 是把一个大任务拆分为了很多个小块任务,一个小块任务的执行必须是一次完成的,不能出现暂停,但是一个小块任务执行完后可以移交控制权给浏览器去响应用户操作

核心是通过 requestIdleCallback ,会在利用浏览器空闲时间会找出所有需要变更的节点

阶段一,生成 Fiber 树,得出需要更新的节点信息,这一步是一个渐进的过程,可以被打断

阶段二,将需要更新的节点一次性批量更新,这个过程不能被打断

走进 React Fiber 的世界

react 中使用了 Fiber,为什么 vue 没有用 Fiber?

原因是二者的更新机制不一样

Vue 是基于 template 和 watcher 的组件级更新,把每个更新任务分割得足够小,不需要使用到 Fiber 架构,将任务进行更细粒度的拆分

React 是不管在哪里调用 setState,都是从根节点开始更新的,更新任务还是很大,需要使用到 Fiber 将大任务分割为多个小任务,可以中断和恢复,不阻塞主进程执行高优先级的任务,如果不用 Fiber,会出现老版本卡顿的问题

走进 React Fiber 的世界

为什么 react 推行函数式组件

1)函数组件不需要声明类,可以避免大量的譬如 extends 或者 constructor 这样的代码

2)函数组件不需要处理 this 指向的问题

3)函数组件更贴近于函数式编程,更加贴近 react 的原则。使用函数式编程,灵活度更高,更好的代码复用

4)随着 Hooks 功能的强大,更推动了函数式组件 + Hooks 这对组合的发展

为什么 React 现在要推行函数式组件,用 class 不好吗?

函数式组件 && React Hook

useMemo 和 useCallback 的作用与区别

useCallback

useCallback 返回一个函数,只有在依赖项发生变化的时候才会更新(返回一个新的函数),多用于生成一个防抖函数

注意:组件每次更新时,所有方法都会重新创建,这样之前写的防抖函数就会失效,需要使用 useCallback 包裹

import {debounce} from 'debounce'
// 第二个参数为要监听的变量,当为空数组时[],submit只会被创建一次
// 当监听有值时,会随着值的变化重新创建生成新的submit
const submit = useCallback(debounce(fn, 2000), [])
<button onClick={() => submit()}>提交</button>

useMemo

useMemo 只有在依赖项发生改变的时候,才会重新调用此函数,返回一个新的值, 类似于 vue 中的 computed 计算属性

const info = useMemo(() => {
  //  定义info变量, 该变量会随着 inputPerson, inputAge的变化而变化, info可以在页面中显示
  return {
    name: inputPerson,
    age: inputAge
  };
}, [inputPerson, inputAge]);

详解 React useCallback & useMemo

setState 是同步还是异步?

首先,同步和异步主要取决于它被调用的环境
这里的同步还是异步,指的调用 setState 方法后,是否能立刻拿到更新后的值

1)如果 setState 在 React 能够控制的范围被调用,它就是异步的。比如合成事件处理函数、生命周期函数

在合成事件和钩子函数中,多次调用 setState 修改同一个值,只会取最后一次的执行,前面的会被覆盖

2)如果 setState 在原生 JavaScript 控制的范围被调用,它就是同步的。比如原生事件处理函数、setTimeout、promise 的回调函数等

在原生事件和异步中,可以多次调用 setState 修改同一个值,每次修改都会生效

react 中的合成事件和原生事件

react 为了解决跨平台,兼容性问题,自己封装了一套事件机制,代理了原生的事件,像在 jsx 中常见的 onClick、onChange 这些都是合成事件

原生事件是指非 react 合成事件,原生自带的事件监听 addEventListener,或者也可以用原生 js、jq 直接绑定事件的形式都属于原生事件

你真的理解 setState 吗?

jsx 语法

1)jsx 是React.createElement(component, props, ...children) 函数的语法糖

2)底层是使用babel-plugin-transform-react-jsx插件 将 jsx 的语法转化为 js 对象,判断是否是 jsx 对象或是否是一个组件,转化为对应的 js 对象(虚拟 dom)

jsx 代码示例

// 示例一:
// 如下 JSX 代码
<MyButton color="blue" shadowSize={2}>
  Click Me
</MyButton>;
// 会编译为:
React.createElement(MyButton, { color: 'blue', shadowSize: 2 }, 'Click Me');

// 示例二:
// 以下两种示例代码完全等效
const element = <h1 className="greet">Hello</h1>;
// 等价于
const element = React.createElement('h1', { className: 'greet' }, 'Hello');

服务端渲染

工作中,曾使用过NestjsNextjs这两个服务端渲染的框架,开发过一些需要支持 SEO 的项目,借此总结一些服务端渲染的知识

服务器端渲染的多种模式

传统的 spa 应用,都属于 CSR (Client Side Rendering)客户端渲染

主要问题

1)白屏时间过长:在 JS bundle 返回之前,假如 bundle 体积过大或者网络条件不好的情况下,页面一直是空白的,用户体验不友好

2)SEO 不友好:搜索引擎访问页面时,只会看 HTML 中的内容,默认是不会执行 JS,所以抓取不到页面的具体内容

服务器端渲染的多种模式

1)SSR (Server Side Rendering), 即服务端渲染

服务端直接实时同构渲染当前用户访问的页面,返回的 HTML 包含页面具体内容,提高用户的体验

适用于:页面动态内容,SEO 的诉求、要求首屏时间快的场景

缺点:SSR 需要较高的服务器运维成本、切换页面时较慢,每次切换页面都需要服务端新生成页面

2)SSG (Static Site Generation),是指在应用编译构建时预先渲染页面,并生成静态的 HTML

把生成的 HTML 静态资源部署到服务器后,浏览器不仅首次能请求到带页面内容的 HTML ,而且不需要服务器实时渲染和响应,大大节约了服务器运维成本和资源

适用于:页面内容由后端获取,但变化不频繁,满足 SEO 的诉求、要求首屏时间快的场景,SSG 打包好的是静态页面,和普通页面部署一样

3)ISR (Incremental Static Regeneration),创建需要增量静态再生的页面

创建具有动态路由的页面(增量静态再生),允许在应用运行时再重新生成每个页面 HTML,而不需要重新构建整个应用,这样即使有海量页面(比如博客、商品介绍页等场景),也能使用上 SSG 的特性

在 Nextjs 中,使用 ISR 需要 getStaticPaths 和 getStaticProps 同时配合使用

vue SSR 服务端渲染

vue 项目,可以使用 Nestjs 框架,实现 ssr 渲染,开发有 SEO 需求的页面

SSR 原理

通过asyncData获取数据,数据获取成功后,通过vue-server-renderer将数据渲染到页面中,生成完整的 html 内容,服务端将这段 html 发送给客户端,实现服务端渲染

SSR 基本交互流程

1)在浏览器访问首页时,Web 服务器根据路由拿到对应数据渲染并输出 html,输出的 html 包含两部分

① 路由页对应的页面及已渲染好的数据(后端渲染)

② 完整的 SPA 程序代码

2)在客户端首屏渲染完成之后,其实已经是一个和之前的 SPA 相差无几的应用程序了,接下来我们进行的任何操作都只是客户端的应用进行交互

vue SSR 整体流程

1)配置两个入口文件,一个是客户端使用,一个是服务端使用,一套代码两套执行环境

2)服务端渲染需要 Vue 实例,每一次客户端请求页面,服务端渲染都是用一个新的 Vue 实例,服务端利用工厂函数每次都返回一个新的 Vue 实例

3)获取请求页面的路由,生成对应的 vue 实例

4)如果页面中需要调接口获取数据,通过asyncData获取数据,数据获取成功后,通过异步的方式再继续进行初始化,通过vue-server-renderer将数据渲染到页面中,生成 html 内容

如何避免客户端重复请求数据

在服务端已经请求的数据,在客户端应该避免重复请求,怎样同步数据到客户端?

通过(window 对象作为中间媒介进行传递数据)

1)服务端获取数据,保存到服务端的 store 状态,以便渲染时候使用,最终会将 store 保存到 window 中

2)在 renderer 中会在 html 代码中添加 <script>window.__INITIAL_STATE__ = context.state</script>,
在解析页面的时候会进行设置全局变量

3)在浏览器进行初始化 Store 的时候,通过 window 对象进行获取数据在服务端的状态,并且将其注入到 store.state 状态中,这样能够实现状态统一

为什么服务端渲染不能调用 mounted 钩子

服务端渲染不能调用beforeMountmounted,Node 环境没有 document 对象,初始化的时候,vue 底层会判断当前环境中是否有 el 这个 dom 对象,如果没有,就不会执行到 beforeMount 和 mounted 这两个钩子函数

Vue 服务端渲染(SSR)
理解 Vue SSR 原理,搭建项目框架

react Next 预渲染模式

Next.js 支持 SSR、SSG、ISR 三种模式,默认是 SSG

1)SSR 模式

需要将 Next.js 应用程序部署到服务器,开启服务端渲染

整个流程

用户访问页面 → 如果该页面配置了 getServerSideProps 函数 → 调用 getServerSideProps 函数 → 用接口的数据渲染出完整的页面返回给用户

// 定义页面
function Page({ data }) {
  // Render data...
}

// 如果该页面配置了 getServerSideProps函数,调用该函数
export async function getServerSideProps() {
  // 请求接口拿到对应的数据
  const res = await fetch(`https://.../data`);
  const data = await res.json();

  // 将数据渲染到页面中
  return { props: { data } };
}

// 导出整个页面
export default Page;

2)SSG 模式

SSG 模式:项目在打包时,从接口中请求数据,然后用数据构建出完整的 html 页面,最后把打包好的静态页面,直接放到服务器上即可

Next.js 允许你从同一文件  export(导出)  一个名为  getStaticProps  的  async(异步)  函数。该函数在构建时被调用,并允许你在预渲染时将获取的数据作为  props  参数传递给页面

// 定义Blog页面
function Blog({ posts }) {
  // Render posts...
}

// getStaticProps函数,在构建时被调用
export async function getStaticProps() {
  // 调用外部 API 获取博文列表
  const res = await fetch('https://.../posts');
  const posts = await res.json();

  // 通过返回 { props: { posts } } 对象,Blog 组件
  // 在构建时将接收到 `posts` 参数
  return {
    props: {
      posts
    }
  };
}

// 导出Blog页面
export default Blog;

3)ISR 模式

创建具有 动态路由 的页面,用于海量生成

Next.js 允许在应用运行时再重新生成每个页面 HTML,而不需要重新构建整个应用。这样即使有海量页面,也能使用上 SSG 的特性。一般来说,使用 ISR 需要 getStaticPaths 和 getStaticProps 同时配合使用

// 定义博文页面
function Blog({ post }) {
  // Render post...
}

// 此函数在构建时被调用
export async function getStaticPaths() {
  // 调用外部 API 获取博文列表
  const res = await fetch('https://.../posts');
  const posts = await res.json();

  // 据博文列表生成所有需要预渲染的路径
  const paths = posts.map((post) => ({
    params: { id: post.id }
  }));

  return { paths, fallback: false };
}

// 在构建时也会被调用
export async function getStaticProps({ params }) {
  // params 包含此片博文的 `id` 信息。
  // 如果路由是 /posts/1,那么 params.id 就是 1
  const res = await fetch(`https://.../posts/${params.id}`);
  const post = await res.json();

  // 通过 props 参数向页面传递博文的数据
  return { props: { post } };
}

export default Blog;

next 预渲染
使用 Next.js 进行增量静态再生(ISR)的完整指南

Node

Node 经常用于前端构建、微服务、中台等场景

我曾用 Node 做过一些抽奖类的活动,项目架构是 express + mongoDb + redis

开发后台项目的总体感受

1)和后端的同事沟通起来更顺畅了,之前他们老是说这张表、那张表、redis 什么的,现在也能理解了,消除了一些彼此的隔阂

2)更全面的去理解业务,了解整套流程,比如前后端是如何配合的、数据如何传递、后台是如何处理,甚至在需求评审时,可以提出自己的方案或建议

下面,我浅谈一下对 Node 理解

Node 高并发的原理

Node 的特点:事件驱动、非阻塞 I/O、高并发

Node 高并发的原理

Nodejs 之所以单线程可以处理高并发的原因,得益于内部的事件循环机制和底层线程池实现

遇到异步任务,node 将所有的阻塞操作都交给了内部的线程池去实现。本质上的异步操作还是由线程池完成的,主线程本身只负责不断的往返调度,从而实现异步非阻塞 I/O,这便是 node 单线程和事件驱动的精髓之处

整体流程

1)每个 Node 进程只有一个主线程在执行程序代码

2)当用户的网络请求、数据库操作、读取文件等其它的异步操作时,node 都会把它放到 Event Queue("事件队列")之中,此时并不会立即执行它,代码也不会被阻塞,继续往下走,直到主线程代码执行完毕

3)主线程代码执行完毕完成后,然后通过事件循环机制,依次取出对应的事件,从线程池中分配一个对应的线程去执行,当有事件执行完毕后,会通知主线程,主线程执行回调拿到对应的结果

Node 事件循环机制与浏览器的区别

主要区别:浏览器中的微任务是在每个相应的宏任务中执行的,而 nodejs 中的微任务是在不同阶段之间执行的。

node 事件循环机制分为 6 个阶段,它们会按照顺序反复运行

每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段

nodeLoop.jpg

主要介绍timers、poll、check这 3 个阶段,因为日常开发中的绝大部分异步任务都是在这 3 个阶段处理的

1)timer
timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的

2)poll
poll 是一个至关重要的阶段,这一阶段中,系统会做两件事情:回到 timer 阶段执行回调:执行 I/O 回调

3) check 阶段
setImmediate()的回调会被加入 check 队列中,从 event loop 的阶段图可以知道,check 阶段的执行顺序在 poll 阶段之后

其中的细节推荐看下这两篇文章

面试题:说说事件循环机制(满分答案来了)
浏览器与 Node 的事件循环(Event Loop)有何区别?

中间件原理

比较流行的 Node.js 框架有ExpressKOAEgg.js,都是基于中间件来实现的。中间件主要用于请求拦截和修改请求或响应结果

node 中间件本质上就是在进入具体的业务处理之前,先让特定过滤器进行处理

一次 Http 请求通常包含很多工作,如请求体解析、Cookie处理、权限验证、参数验证、记录日志、ip过滤、异常处理等,这些环节通过中间件处理,让开发人员把核心放在对应的业务开发上

这种模式也被称为"洋葱圈模型"

onion.png

模拟一个中间件流程

const m1 = async next => {
  console.log("m1 run");
  await next();
  console.log("result1");
};

const m2 = async next => {
  console.log("m2 run");
  await next();
  console.log("result2");
};
const m3 = async next => {
  console.log("m3 run");
  await next();
  console.log("result3");
};

const middlewares = [m1, m2, m3];

function useApp() {
  const next = () => {
    const middleware = middlewares.shift();
    if (middleware) {
      return Promise.resolve(middleware(next));
    } else {
      return Promise.resolve("end");
    }
  };
  next();
}
// 启动中间件
useApp();

// 依次打印:
m1 run
m2 run
m3 run
result3
result2
result1

express 中间件的执行过程

const express = require("express");
const app = express();

app.listen("3000", () => {
  "启动服务";
});

app.use((req, res, next) => {
  console.log("first");
  next();
  console.log("end1");
});

app.use((req, res, next) => {
  console.log("second");
  next();
  console.log("end2");
});

app.use((req, res, next) => {
  console.log("third");
  next();
  console.log("end3");
});

app.get("/", (req, res) => res.send("express"));

// 请求http://localhost:3000/#/
依次打印:
first
second
third
end3
end2
end1

express 常用的中间件

中间件名称 作用
express.static() 用来返回静态文件
body-parser 用于解析 post 数据
multer 处理文件上传
cookie-parser 用来操作 cookie
cookie-session 处理 session

深入浅出 node 中间件原理
nodejs 中间件理解

实现一个大文件上传和断点续传

推荐一个使用 node 的经典案例

该案例会使用 node 对文件进行操作,这也是 node 最常用的场景之一

其中一个关键的知识点:pipe管道流

管道: 一个程序的输出直接成为下一个程序的输入,就像水流过管道一样方便 readStream.pipe(writeStream) 就是在可读流与可写流中间加入一个管道,实现一边读取,一边写入,读一点写一点。

管道流的好处:节约内存

读出的数据,不做保存,直接流出。写入写出会极大的占用内存,stream 可以边读边写,不占用太多内存,并且完成所需任务

字节跳动面试官:请你实现一个大文件上传和断点续传

如何做到接口防刷

因为之前做的是抽奖系统,接口防刷是非常必要的,也是高并发下的经典场景

其中的一些知识点,已经超过了前端的范畴,不过技不压身,多了解一些总是没错的

1)第一步:负载均衡层的限流,防止用户重复抽奖

在负载均衡设备中做一些配置,判断如果同一个用户在 1 分钟之内多次发送请求来进行抽奖,就认为是恶意重复抽奖,或者是他们自己写的脚本在刷

这种流量一律认为是无效流量,在负载均衡设备那个层次就给直接屏蔽掉。 所以这里就可以把无效流量给拦截掉

2)第二步:暴力拦截流量

其实秒杀、抢红包、抽奖,这类系统有一个共同的特点,那就是假设有 50 万请求涌入进来,可能前 5 万请求就直接把事儿干完了,甚至是前 500 请求就把事儿干完了

后续的几十万流量是无效的,不需要让他们进入后台系统执行业务逻辑了

这样的话,其实在负载均衡这一层(可以考虑用 Nginx 之类的来实现)就可以拦截掉 99%的无效流量

3)第三步:ip 或用户抽奖次数校验

建立一个抽奖表,该表记录所有参与抽奖的 ip 和用户信息,比如判断 5s 内,该用户连续抽奖了 2 次以上,就拒绝该请求,认为是在刷接口,就把该用户和 ip 加入黑名单

如何设计一个百万级用户的抽奖系统?

mongoDb 和 mySQL 的区别

1)mongoDb 是非关系型数据库,mySQL 是关系型数据库

mongoDb 里存储的是 json 格式的数据,键值对形式,该数据结构非常符合前端的需求

关系型数据天然就是表格式的,就是后端常说的“表”,数据存储在数据表的行和列中。数据表可以彼此关联协作存储,也很容易提取数据

2)对事务性的支持不同

mongoDb 不支持事务,mySQL 支持事务

事务的好处便于回滚,如第一个账户划出款项必须保证正确的存入第二个账户,如果第二个环节没有完成,整个的过程都应该取消,否则就会发生丢失款项的问题。这时就需要回滚,恢复到初始的状态

mongodb 与 mysql 区别(超详细)

高并发时的如何正确修改库存

场景:
抽奖或秒杀活动,同时一千个请求过来,但奖品库存只有一个,期望的结果是只有一个人中奖,剩余 999 个人没有中奖

但压测时,遇到的情况却是 1000 个都中奖了,并且库存还是一个

吓得当时脸都绿了,这是什么情况啊……

原因就是高并发时,一千个请求同时读到的库存都是一个,都中奖后,库存同时减一,最后导致库存没有减对

解决此类问题,就是要给数据库加锁的概念,保证库存一个一个减、串行的减,解决方式是使用 mongoDb 中update方法减库存

mongoDb 中,有三种方法可以实现更新数据:

1)save方法,如db.collection.save(obj),save 是在客户端代码中生成的对象,需要从客户端回写到服务器端

2)findOneAndUpdate方法,如db.findOneAndUpdate(<filter>,{obj}), 和 save 类似也需要从客户端回写到服务器端

3)update方法,如db.update(<filter>,{obj}),update 是服务器端处理的,速度最快;实测当并发数超过 1000 次每秒时,update 的速度是其他的 2 倍

Redis

Redis 的特点

1)Redis 也是一种数据库,Redis 中的数据是放到内存中的,Redis 查询速度极快。一些常用的数据,可以存到 Redis 中,缩短从数据库查询数据的时间

2)Redis 可以设置过期时间,可以将一些需要定期过期的信息放到 Redis 中,有点类似 cookie

运用场景

1)将经常查询的信息存储到 redis 中,如抽奖活动的配置信息,这些信息查询的频率最高,放到 Redis 中可以提高查询速度,还可以存储用户的个人信息(权限、基础信息等)

2)需要设置过期时长的信息,比如微信授权,每 2 小时去过期一次,将对应的授权 code 存进去,到时删除

Redis 的优缺点

node 创建子进程

当的项目中需要有大量计算的操作时候,就要考虑开启多进程来完成了,类似于web worker,否则会阻塞主线程的执行

Node 提供了 child_process 模块来创建子进程

进程间通信:使用fork方法创建的子进程,可通过send、on(message)方法来发送和接收进程间的数据

// 具体代码
// parent.js
const cp = require('child_process');
// 通过child_process中的fork方法来生成子进程
let child = cp.fork('child.js');
child.send({ message: 'from_parent' }); // send方法发送数据
child.on('message', (res) => console.log(res)); // on方法接收数据
// child.js
process.on('message', (res) => console.log(res));
process.send({ message: 'from_child' });

Nodejs 进阶:如何玩转子进程(child_process)

PM2

PM2 可以根据 cpu 核数,开启多个进程,充分利用 cpu 的多核性能

pm2 start app.js -i 8 该命令可以开启 8 个进程

主要作用:

1)内建负载均衡(使用Node cluster集群模块)
2)线程守护,keep alive
3)0 秒停机重载,维护升级的时候不需要停机
4)停止不稳定的进程(避免无限循环)

负载均衡 cluster 的原理

1)Node.js 给我们提供了cluster模块,它可以生成多个工作线程来共享同一个 TCP 连接

2)首先,Cluster 会创建一个 master,然后根据你指定的数量复制出多个 server app(也被称之为工作线程)

3)它通过 IPC 通道与工作线程之间进行通信,并使用内置的负载均衡来更好地处理线程之间的压力,该负载均衡使用了 Round-robin 算法(也被称之为循环算法)

使用 PM2 将 Node.js 的集群变得更加容易  
PM2 入门指南

计算机网络与安全

计算机网络与安全.png

从输入 URL 到页面加载发生了什么?

1)浏览器查找当前 URL 是否存在缓存,并比较缓存是否过期。(先判断 HTTP 请求浏览器是否已缓存)

有缓存

如为强制缓存,通过Expires或Cache-Control:max-age判断该缓存是否过期,未过期,直接使用该资源;Expires 和 max-age,如果两者同时存在,则被 Cache-Control 的 max-age 覆盖。

如为协商缓存,请求头部带上相关信息如if-none-match(Etag)if-modified-since(last-modified),验证缓存是否有效,若有效则返回状态码为304,若无效则重新返回资源,状态码为200

2)DNS 解析 URL 对应的 IP(DNS解析流程见下文

3)根据 IP 建立 TCP 连接(三次握手)(握手过程见下文

4)HTTP 发起请求

5)服务器处理请求,浏览器接收 HTTP 响应

6)渲染页面,构建 DOM 树

①HTML 解析,生成 DOM 树
② 根据 CSS 解析生成 CSS 树
③ 结合 DOM 树和 CSS 规则树,生成渲染树
④ 根据渲染树计算每一个节点的信息(layout 布局)
⑤ 根据计算好的信息绘制页面

如果遇到 script 标签,则判断是否含有 defer 或者 async 属性,如果有,异步去下载该资源;如果没有设置,暂停 dom 的解析,去加载 script 的资源,然后执行该 js 代码(script 标签加载和执行会阻塞页面的渲染

7)关闭 TCP 连接(四次挥手)(挥手过程见下文

从输入 url 到页面加载完成发生了什么详解
在浏览器输入 URL 回车之后发生了什么(超详细版)

彻底弄懂 cors 跨域请求

cors 是解决跨域问题的常见解决方法,关键是服务器要设置Access-Control-Allow-Origin,控制哪些域名可以共享资源

origin是 cors 的重要标识,只要是非同源或者 POST 请求都会带上 Origin 字段,接口返回后服务器也可以将Access-Control-Allow-Origin设置为请求的 Origin,解决 cors 如何指定多个域名的问题

cors 将请求分为简单请求和非简单请求

简单请求

1)只支持 HEAD,get、post 请求方式;

2)没有自定义的请求头;

3)Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是在头信息之中,增加一个Origin字段。如果浏览器发现这个接口回应的头信息没有包含Access-Control-Allow-Origin字段的话就会报跨域错误

非简单请求的跨域处理

非简单请求,会在正式通信之前,增加一次 HTTP 查询请求,称为"预检"请求(options),用来判断当前网页所在的域名是否在服务器的许可名单之中

如果在许可名单中,就会发正式请求;如果不在,就会报跨越错误

注:新版 chrome 浏览器看不到 OPTIONS 预检请求,可以网上查找对应的查看方法

跨域资源共享 CORS 详解

TCP 的三次握手和四次挥手

三次握手的过程:

1)第一次握手:客户端向服务端发送连接请求报文,请求发送后,客户端便进入 SYN-SENT 状态

2)第二次握手:服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,发送完成后便进入 SYN-RECEIVED 状态

3)第三次握手:当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED(已建立的) 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功

为什么需要三次握手?

三次握手之所以是三次,是保证 client 和 server 均让对方知道自己的接收和发送能力没问题而保证的最小次数。两次不安全,四次浪费资源

四次挥手的过程

当服务端收到客户端关闭报文时,并不会立即关闭,先回复一个报文,告诉客户端,"你发的 FIN 报文我收到了"。只有等到我 Server 端所有的报文都发送完了,我才能发送连接释放请求,因此不能一起发送。故需要四步挥手

举例:
Browser:先告诉服务器 “我数据都发完了,你可以关闭连接了。”
Server:回复浏览器 “关闭的请求我收到了,我先看看我这边还有没有数据没传完。”
Server:确认过以后,再次回复浏览器 “我这边数据传输完成了,你可以关闭连接了。”
Browser:告诉服务器 “好的,那我关闭了。不用回复了。”
客户端又等了 2MSL,确认确实没有再收到请求了,才会真的关闭 TCP 连接。

为什么需要四次挥手?

1)TCP 使用四次挥手的原因,是因为 TCP 的连接是全双工的,所以需要双方分别释放掉对方的连接

2)单独一方的连接释放,只代 表不能再向对方发送数据,连接处于的是半释放的状态

3)最后一次挥手中,客户端会等待一段时间再关闭的原因,是为了防止客户端发送给服务器的确认报文段丢失或者出错,从而导致服务器端不能正常关闭

什么是 2MSL?

MSL 是 Maximum Segment Lifetime 英文的缩写,中文可以译为“报文最大生存时间”

四次挥手后,为什么客户端最后还要等待 2MSL?

1)保证客户端发送的最后一个 ACK 报文能够到达服务器,因为这个 ACK 报文可能丢失,如果服务端没有收到,服务端会重发一次,而客户端就能在这个 2MSL 时间段内收到这个重传的报文,接着给出回应报文,并且会重启 2MSL 计时器

2)防止“已经失效的连接请求报文段”出现在本连接中

客户端发送完最后一个确认报文后,在这个 2MSL 时间中,就可以使本连接持续的所产生的所有报文都从网络中消失。这样新的连接中不会出现旧连接的请求报文

TCP 的三次握手和四次挥手
TCP 的三次握手和四次挥手及常见面试题
什么是 2MSL

WebSocket

WebSocket 是 HTML5 提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议,WebSocket 没有跨域的限制

相比于接口轮训,需要不断的建立 http 连接,严重浪费了服务器端和客户端的资源

WebSocket 基于 TCP 传输协议,并复用 HTTP 的握手通道。浏览器和服务器只需要建立一次 http 连接,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

缺点
websocket 不稳定,要建立心跳检测机制,如果断开,自动连接

手摸手教你使用 WebSocket[其实 WebSocket 也不难]
socket 及 websocket 的握手过程

TCP 和 UDP 的区别

相同点: UDP 协议和 TCP 协议都是传输层协议

不同点:

1)TCP 面向有连接; UDP:面向无连接

2)TCP 要提供可靠的、面向连接的传输服务。TCP 在建立通信前,必须建立一个 TCP 连接,之后才能传输数据。TCP 建立一个连接需要 3 次握手,断开连接需要 4 次挥手,并且提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端

3)UDP 不可靠性,只是把应用程序传给 IP 层的数据报发送出去,但是不能保证它们能到达目的地

4)应用场景

TCP 效率要求相对低,但对准确性要求相对高的场景。如常见的接口调用、文件传输、远程登录等

UDP 效率要求相对高,对准确性要求相对低的场景。如在线视频、网络语音电话等

面试题:UDP&TCP 的区别
TCP 和 UDP 的区别及应用场景

keep-alive 持久连接

keep-alive 又叫持久连接,它通过重用一个 TCP 连接来发送/接收多个 HTTP 请求,来减少创建/关闭多个 TCP 连接的开销,启用 Keep-Alive 模式性能更高

在 HTTP1.1 协议中默认开启,可以在请求头上看到 Connection: keep-alive 开启的标识

在 HTTP1.0 中非 KeepAlive 模式时,每次请求都要新建一个 TCP 请求,请求结束后,要关闭 TCP 连接。效率很低

注意:持久连接采用阻塞模式,下次请求必须等到上次响应返回后才能发起,如果上次的请求还没返回响应内容,下次请求就只能等着(就是常说的线头阻塞)

HTTP keep-alive 二三事

http1、2、3 的区别

http1、2 的区别:

1)二进制传输,HTTP/2 采用二进制格式传输数据,而非 HTTP/1.x 里纯文本形式的报文 ,二进制协议解析起来更高效

2)Header 压缩

HTTP/1.x 的请求和响应头部带有大量信息,而且每次请求都要重复发送。HTTP2 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再每次请求和响应发送

3)多路复用

就是在一个 TCP 连接中可以发送多个请求,可以避免 HTTP 旧版本中的线头阻塞问题(下次请求必须等到上次响应返回后才能发起)

这样某个请求任务耗时严重,不会影响到其它连接的正常执行,极大的提高传输性能

在 HTTP/2 中,有两个非常重要的概念,分别是帧(frame)和流(stream)。 帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流(即请求),通过重新排序还原请求

4)服务端推送: 这里的服务端推送,是指把客户端所需要的 css/js/img 资源伴随着 index.html,一起发送到客户端,省去了客户端重复请求的步骤

Http3.0 的区别

http 协议是应用层协议,都是建立在传输层之上的。2.0 和 1.0 都是基于 TCP 的,而 Http3.0 则是建立在 UDP 的基础上

http3.0 新特性

1)多路复用,彻底解决 TCP 中队头阻塞的问题
2)集成了 TLS 加密功能
3)向前纠错机制

http1、2、3 总结:

1)HTTP/1.1 有两个主要的缺点:安全不足和性能不高

2)HTTP/2 完全兼容 HTTP/1,是“更安全的 HTTP、更快的 HTTPS",头部压缩、多路复用等技术可以充分利用带宽,降低延迟,从而大幅度提高上网体验

3)QUIC 基于 UDP 实现,是 HTTP/3 中的底层支撑协议,该协议基于 UDP,又取了 TCP 中的精华,实现了即快又可靠的协议

解密 HTTP/2 与 HTTP/3 的新特性
HTTP/3 新特性

HTTPS 握手过程

https 采用非对称加密+对称加密,非对称加密来传递密钥;对称加密来加密内容

1)客户端使用 https 的 url 访问 web 服务器,要求与服务器建立 ssl 连接

2)服务器收到客户端请求后, 会将网站的证书(包含公钥)传送一份给客户端

3)客户端收到网站证书后会检查证书的颁发机构以及过期时间, 如果没有问题就随机产生一个秘钥

4)客户端利用公钥将会话秘钥加密, 并传送给服务端

5)服务端利用自己的私钥解密出会话秘钥,之后服务器与客户端使用秘钥加密传输

加密速度对比

对称加密解密的速度比较快,适合数据比较长时的使用

非对称加密和解密花费的时间长、速度相对较慢,只适合对少量数据的使用

(非对称加密:有公钥私钥,公钥加密,私钥解密;对称加密:同一个秘钥进行加密或解密)

一个故事讲完 https

介绍下中间人攻击

中间人攻击过程如下:

1)客户端向服务器发送建立连接的请求
2)服务器向客户端发送公钥
3)攻击者截获公钥,保留在自己手上
4)然后攻击者自己生成一个【伪造的】公钥,发给客户端
5)客户端收到伪造的公钥后,生成加密的秘钥值发给服务器
6)攻击者获得加密秘钥,用自己的私钥解密获得秘钥
7)同时生成假的加密秘钥,发给服务器
8)服务器用私钥解密获得假秘钥
9)服务器用假秘钥加密传输信息

防范方法:

服务端在发送浏览器的公钥中加入 CA 证书,浏览器可以验证 CA 证书的有效性

介绍下 HTTPS 中间人攻击

DNS 解析过程

DNS 解析过程:将域名解析成 IP 地址

DNS 叫做域名系统,是域名和对应 ip 地址的分布式数据库。有了它,就可以用域名来访问对应的服务器

过程:

1)在浏览器中输入后 url 后,会优先在浏览器 dns 缓存中查找,如果有缓存,则直接响应用户的请求

2)如果没有要访问的域名,就继续在操作系统的 dns 缓存中查找,如果也没有,最后通过本地的 dns 服务器查到对应的 ip 地址

3)DNS 服务器完整的查询过程

本地 DNS 服务器向根域名服务器发送请求,根域名服务器会返回一个所查询域的顶级域名服务器地址

本地 DNS 服务器向顶级域名服务器发送请求,接受请求的服务器查询自己的缓存,如果有记录,就返回查询结果,如果没有就返回相关的下一级的权威域名服务器的地址

本地 DNS 服务器向权威域名服务器发送请求,权威域名服务器返回对应的结果

本地 DNS 服务器将返回结果保存在缓存中,便于下次使用

本地 DNS 服务器将返回结果返回给浏览器

DNS 预解析

DNS Prefetch 是一种 DNS 预解析技术,当你浏览网页时,浏览器会在对网页中的域名进行解析缓存,这样当页面中需要加载该域名的资源时就无需解析,减少用户等待时间,提高用户体验

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

DNS 完整的查询过程
dns-prefetch 对网站速度能提升有多少?

XSS(跨站脚本攻击)

XSS 攻击介绍: 攻击者通过在页面注入恶意脚本,使之在用户的浏览器上运行

攻击案例:

<div><script>alert('XSS')</script></div>
<a href="javascript:alert('XSS')">123456</a>
<a onclick='alert("xss攻击")'>链接</a>

XSS 攻击的几种方式

1)常见于带有用户提交数据的网站功能,如填写基本信息、论坛发帖、商品评论等;在可输入内容的地方提交如<script>alert('XSS')</script>之类的代码

XSS 的恶意代码存在数据库里,浏览器接收到响应后解析执行,混在其中的恶意代码也被执行

2)用户点击http://xxx/search?keyword="><script>alert('XSS');</script>,前端直接从 url 中将 keyword 后的参数取出来,并显示到页面上,但是没有做转义,就造成了 XSS 攻击。

XSS 攻击的防范

1)前端尽量对用户输入内容长度控制、输入内容限制(比如电话号码、邮箱、包括特殊字符的限制)

2)服务器对前端提交的内容做好必要的转义,避免将恶意代码存储到数据库中,造成存储性 xss 攻击

3)前端对服务器返回的数据做好必要的转义,保证显示到页面的内容正常

vue 中如何防止 XSS 攻击

1)vue 中使用{{}}模板渲染数据或通过 v-bind 给元素绑定属性时,都已将内容转义,防止 xss 攻击

// 案例
<h1>{{string}}</h1>
string = '<script>alert("hi")</script>'`
//被转义成为如下 &lt;script&gt;alert(&quot;hi&quot;)&lt;/script&gt;

2)尽量避免使用v-html,如果必须使用,可以使用vue-xss插件对文本内容进行转义,该插件可以同时去掉上面绑定的事件

// 案例
`<div v-html="$xss(xss)"></div>`;
// p标签正常显示,但上面绑定的事件已被去掉
xss = "<p onclick='console.log(document.cookie)'>123</p>";

前端安全系列(一):如何防止 XSS 攻击?

csrf 跨站请求伪造

csrf 的攻击原理:

诱导受害者进入钓鱼网站,在钓鱼网站中利用你在被攻击网站已登录的凭证(凭证存在 cookie 中),冒充用户发送恶意请求,这些请求因为携带有用户的登录信息,会被服务器当做正常的请求处理,从而使你的个人隐私泄露或财产损失

csrf 的攻击过程:

1)受害者登录 A 站点,并保留了登录凭证(Cookie)

2)攻击者诱导受害者访问了站点 B

3)站点 B 向站点 A 发送了一个请求,浏览器会默认携带站点 A 的 Cookie 信息

4)站点 A 接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者发送的请求

5)站点 A 以受害者的名义执行了站点 B 的请求,攻击完成,攻击者在受害者不知情的情况下,冒充受害者完成了攻击

csrf 的攻击的必要条件:

1)用户已登录过某网站,并且没有退出,登录信息存储在 cookie 中(发送请求时,浏览器会自动在请求头上带上要请求域名的 cookie)

2)在不登出 A 的情况下,访问危险网站 B

CSRF 如何防御

1)根据攻击的原理可以看出,csrf 通常是跨域请求(从钓鱼网站 B 发送请求网站 A 的请求),请求头上的 Referer 或 origin 字段可以判断请求的来源,如果服务器判断请求的域名不在白名单中,就拒绝对应的请求

2)添加 token 验证

CSRF 攻击之所以能够成功,是因为用户验证信息都存在 cookie 中,攻击者可以完全伪造用户的请求。从请求头或请求参数中添加用户的 token 用来验证用户,如果请求没有或 token 不对,就拒绝对应的请求

3)验证码

对于转账或支付的环节,强制用户必须与应用进行交互,才能完成最终请求

前端安全系列(二):如何防止 CSRF 攻击?
WEB 安全之-CSRF(跨站请求伪造)

jsonp 安全防范

jsonp 是以 callback 的形式,返回服务端的数据 如http://www.qq.com/getUserInfo?callback=action

1)白名单验证

通过请求头上的Referer或origin字段可以判断请求的来源,如果服务器判断请求的域名不在白名单中,就拒绝对应的请求

2)对返回的内容进行验证或转义

根据 jsonp 的原理,当拿到 callback 的参数后,会直接当 js 代码执行,如果 callback 后面的参数是 script 标签,就会变成 xss 攻击了,所以要对返回的内容进行转义并限制长度,防范类似的攻击

例如http://youdomain.com?callback=<script>alert(1)</script>

前端也需要了解的 JSONP 安全

浏览器如何验证 ca 证书的有效性

浏览器读取证书中的证书所有者、有效期等信息进行校验

1)校验证书的网站域名是否与证书颁发的域名一致

2)校验证书是否在有效期内

3)浏览器查找操作系统中已内置的受信任的证书发布机构,与服务器发来的证书中的颁发者做比对,用于校验证书是否为合法机构颁发

HTTPS 握手过程中,客户端如何验证证书的合法性

csp 内容安全策略

内容安全策略 CSP (Content Security Policy) ,CSP 防止 XSS 攻击, 浏览器自动禁止外部脚本注入

CSP 的实质就是白名单制度,开发者明确告诉客户端,哪些外部资源可以加载和执行,等同于提供白名单。它的实现和执行全部由浏览器完成,开发者只需提供配置

CSP 大大增强了网页的安全性。攻击者即使发现了漏洞,也没法注入脚本,除非还控制了一台列入了白名单的可信主机

配置方式:

1)通过 HTTP 头信息的Content-Security-Policy的字段
Content-Security-Policy: script-src 'self'; object-src 'none'; style-src cdn.example.org third-party.org; child-src https:

2)通过网页的标签

<meta http-equiv="Content-Security-Policy" content="script-src 'self'; object-src 'none'; style-src cdn.example.org third-party.org; child-src https:">

两种配置方式的效果一样

Content Security Policy 入门教程
WEB 安全之内容安全策略(CSP)详解

浏览器原理

浏览器原理.png

js 的单线程

js 是单线程,只是说 js 的执行是单线程的,但 js 的宿主环境,无论是 Node 还是浏览器都是多线程的

以 Chrome 浏览器中为例,当你打开一个页面,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。 当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。

JS 为什么设计成单线程?

如果有多个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时这两个节点会有很大冲突,为了避免这个冲突,所以决定了它只能是单线程

线程与进程

一句话:进程可以包含多个线程

进程是 CPU 资源分配的最小单位;线程是 CPU 调度的最小单位

浏览器进程包括:

1)浏览器主进程(Browser进程)

主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

2)浏览器渲染进程(Renderer进程)

浏览器渲染进程:即通常所说的浏览器内核

核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下

3)GPU 进程

GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制

4)第三方插件进程

主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,每种类型的插件对应一个进程, 以保证插件进程崩溃不会对浏览器和页面造成影响。

浏览器渲染进程(Renderer 进程)包含 5 种线程:

1)GUI 渲染线程

主要负责页面的渲染,解析 HTML、CSS,构建 DOM 树,布局和绘制等

2)JS 引擎线程

该线程主要负责处理 JavaScript 脚本,执行代码。该线程与 GUI 渲染线程互斥,当 JS 引擎线程执行 JavaScript 脚本时间过长,将导致页面渲染的阻塞。

3)事件触发线程

主要负责将准备好的事件交给 JS 引擎线程执行。比如 setTimeout 定时器计数结束, ajax 等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将整装待发的事件依次加入到任务队列的队尾,等待 JS 引擎线程的执行

4)定时器触发线程

负责执行异步定时器一类的函数的线程,如: setTimeout,setInterval

5)异步 http 请求线程

负责执行异步请求一类的函数的线程,如: Promise,axios,ajax 等

浏览器的线程和进程
浏览器相关原理(面试题)详细总结一
浏览器与 Node 的事件循环(Event Loop)有何区别?

浏览器页面渲染机制

浏览器有 GUI 渲染线程与 JS 引擎线程,这两个线程是互斥的关系

JavaScript 的加载、解析与执行会阻塞 DOM 的构建。也就是说,在构建 DOM 时,HTML 解析器若遇到了 JavaScript,那么它会暂停构建 DOM,将控制权移交给 JavaScript 引擎,等 JavaScript 引擎运行完毕,浏览器再从中断的地方恢复 DOM 构建

但是如果遇到带有 async 和 defer 的 script 标签,就会异步请求这些资源,不会阻塞页面渲染

浏览器渲染过程分为:构建 DOM -> 构建 CSSOM -> 构建渲染树 -> layout 布局 -> 绘制

script 标签 async defer 的区别

直接使用 script 会阻塞 DOM 渲染,在脚本加载&执行的过程中,会阻塞后续的 DOM 渲染

使用 async 和 defer,这两个属性使得 script 都不会阻塞 DOM 的渲染

async 和 defer 的区别

async 是无顺序的加载,而 defer 是有顺序的加载

1)执行顺序的区别

async 的执行,并不会按照 script 在页面中的顺序来执行,而是谁先加载完谁执行

defer 的执行,则会按照引入的顺序执行,即便是后面的 script 资源先返回

2)对 window.onload 的影响

使用 defer 的 script 标签,会在 window.onload 事件之前被执行

使用 async 的 script 标签,对 window.onload 事件没有影响,window.onload 可以在之前或之后执行

使用场景的区别

1)defer 可以用来控制 js 文件的加载顺序

比如 jq 和 Bootstrap,因为 Bootstrap 中的 js 插件依赖于 jqery,所以必须先引入 jQuery,再引入 Bootstrap js 文件

2)如果你的脚本并不关心页面中的 DOM 元素(文档是否解析完毕),并且也不会产生其他脚本需要的数据,使用 async, 如统计、埋点等功能

浅谈 script 标签中的 async 和 defer

DOM 事件流

DOM 事件流:事件流简单来说就是事件执行顺序

DOM 同时支持两种事件模型:捕获型事件流和冒泡型事件流

DOM2 事件流的三个阶段:

1)事件捕获阶段
2)处于目标阶段
3)事件冒泡阶段

DOM 事件捕获的具体流程:

window➡️document➡️html➡️body➡️ 目标元素;
事件冒泡:就是这个顺序反过来

运用: 事件委托,利用事件冒泡原理

事件委托:当一组元素要添加相同的事件时,可以在父元素上绑定一个事件,利用事件冒泡原理,达到父元素代理子元素事件,点击子元素,通过 e.target || e.srcElement 可以获取点击的具体子元素

时间委托的优点

可以减少事件的注册,节省内存,也可以实现当新增对象时无需再次对其绑定事件

addEventListener 的第三个参数

第三个参数默认是 false,表示在事件冒泡阶段调用;当该值为 true 表示在事件捕获阶段调用。

验证整个事件流执行顺序(先捕获再冒泡)

// 鼠标点击子元素后,打印顺序为
// 父捕获
// 子捕获
// 子冒泡
// 父冒泡

<html>
  <div class="parent">
    <div class="child">子元素</div>
  </div>
  <script>
     let parentDom = document.querySelector('.parent');
     parentDom.addEventListener('click', function () {console.log('父捕获'); }, true)
     parentDom.addEventListener('click', function () {console.log('父冒泡');}, false)

     let childDom = document.querySelector('.child')
     childDom.addEventListener('click', function () {console.log('子捕获');}, true)
     childDom.addEventListener('click', function () {console.log('子冒泡');}, false)
  </script>
</html>

浏览器空闲时间

页面是一帧一帧绘制出来的,一般情况下,设备的屏幕刷新率为 1s 60 次,而当 FPS 小于 60 时,会出现一定程度的卡顿现象

下面来看完整的一帧中,具体做了哪些事情:

1)首先需要处理输入事件,能够让用户得到最早的反馈

2)接下来是处理定时器,需要检查定时器是否到时间,并执行对应的回调

3)接下来处理 Begin Frame(开始帧),即每一帧的事件,包括 window.resize、scroll、media query change 等

4)接下来执行请求动画帧 requestAnimationFrame(rAF),即在每次绘制之前,会执行 rAF 回调

5)紧接着进行 Layout 操作,包括计算布局和更新布局,即这个元素的样式是怎样的,它应该在页面如何展示

6)接着进行 Paint 操作,得到树中每个节点的尺寸与位置等信息,浏览器针对每个元素进行内容填充

7)到这时以上的六个阶段都已经完成了,接下来处于空闲阶段(Idle Peroid)

requestIdleCallback

在空闲阶段(Idle Peroid)时,可以执行 requestIdleCallback 里注册的任务

requestIdleCallback 接收两个参数:
window.requestIdleCallback(callback, { timeout: 1000 })

1)第一个参数是一个函数,该函数的入参,可以获取当前帧的剩余时间,以及该任务是否超时

window.requestIdleCallback((deadline) => {
  // 返回当前帧还剩多少时间供用户使用
  deadline.timeRamining;
  // 返回 callback 任务是否超时
  deadline.didTimeout;
});

2)第二个参数,传入 timeout 参数自定义超时时间,如果到了超时时间,浏览器必须立即执行

例子:打印此帧的剩余时间

// 该函数的执行时间超过1s
function calc() {
  let start = performance.now();
  let sum = 0;
  for (let i = 0; i < 10000; i++) {
    for (let i = 0; i < 10000; i++) {
      sum += Math.random();
    }
  }
  let end = performance.now();
  let totolTime = end - start;
  // 得到该函数的计算用时
  console.log(totolTime, 'totolTime');
}

let tasks = [
  () => {
    calc();
    console.log(1);
  },
  () => {
    calc();
    console.log(2);
  },
  () => {
    console.log(3);
  }
];

let work = (deadline) => {
  console.log(`此帧的剩余时间为: ${deadline.timeRemaining()}`);

  // 如果此帧剩余时间大于0或者已经到了定义的超时时间(上文定义了timeout时间为1000,到达时间时必须强制执行),且当时存在任务,则直接执行这个任务
  // 如果没有剩余时间,则应该放弃执行任务控制权,把执行权交还给浏览器
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) {
    let fn = tasks.shift();
    fn();
  }
  // 如果还有未完成的任务,继续调用requestIdleCallback申请下一个时间片
  if (tasks.length > 0) {
    window.requestIdleCallback(work, { timeout: 500 });
  }
};
window.requestIdleCallback(work, { timeout: 500 });

执行结果:分 3 帧进行计算

requestIdcallback.jpg

走进 React Fiber 的世界

浏览器缓存

分为协商缓存和强制缓存

协商缓存的流程

1)第一次请求

1、客户端发送 GET 请求,去请求文件;
2、服务器处理请求,返回文件内容和一堆 Header,包括 Etag,状态码 200

2)第二次请求

1、客户端发起 HTTP GET 请求一个文件,注意这个时候客户端请求头上,会带上if-none-match值为Etagif-modified-since值为last-modified

2、服务器优先判断 Etag 和计算出来的 Etag 匹配,若匹配 status 状态为304,客户端继续使用本地缓存

Etag

Etag 是服务器文件的唯一标识,当文件内容变化时 Etag 值也会发生变化

Etag 主要为了解决 Last-Modified 无法解决的一些问题。一些文件也许会周期性的更改,但是它的内容并不改变(仅仅改变的修改时间),此时希望重用缓存,而不是重新请求

Etag 比 last-modified 哪个优先级更高?

当 ETag 和 Last-Modified 同时存在时,服务器优先检查 ETag

强缓存

强缓存是利用 http 头中的  Expires  和  Cache-Control  两个字段来控制的

当同时存在 Expires 和 Cache-Control:max-age 时 哪个优先级高?

Cache-Control:max-age 优先级高,Cache-Control:max-age 表示缓存内容在 xxx 秒后失效;Expires 表示服务端返回的到期时间

Expires 缺点:返回的是服务端时间,与客户端时间相比,可能会出现时间不一致

Etag 详解
为什么 Etag 比 last-modified 优先级更高?

Cache-Control: no-cache 和 no-store 的区别

Cache-Control: no-cache:这个很容易让人产生误解,使人误以为是响应不被缓存

实际上Cache-Control: no-cache是会被缓存的,只不过浏览器每次都会向服务器发起请求,来验证当前缓存的有效性

Cache-Control: no-store:这个才是响应不被缓存的意思

垃圾回收机制

GC 垃圾回收策略

1)标记清除

分为  标记  和  清除  两个阶段,标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁

在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为 0

然后从各个根对象开始遍历,把不是垃圾的节点改成 1,清理所有标记为 0 的垃圾,销毁并回收它们所占用的内存空间。最后,把所有内存中对象标记修改为 0,等待下一轮垃圾回收

2)引用计数

一个对象,如果没有其他对象引用到它,这个对象就是零引用,将被垃圾回收机制回收

它的策略是跟踪记录每个变量值被使用的次数

一个对象被其他对象引用时,这个对象的引用次数就为 1,如果同一个值又被赋给另一个变量,那么引用数加 1,如果该变量的值被其他的值覆盖了,则引用次数减 1

当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存

分代式垃圾回收机制

V8 采用了一种代回收的策略,将内存分为两个生代:新生代和老生代

新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象

内存回收的例子

假设代码中有一个对象 jerry ,这个对象从创建到被销毁,刚好走完了整个生命周期,通常会是这样一个过程

1)这个对象被分配到了新生代;随着程序的运行,新生代塞满了,GC 开始清理 新生代里的死对象,jerry 因为还处于活跃状态,所以没被清理出去;

2)GC 清理了两遍新生代,发现 jerry 依然还活跃着,就把 jerry 移动到了老生代

3)随着程序的运行,老生代也塞满了,GC 开始清理老生代,这时候发现 jerry 已经没有被引用了,就把 jerry 给清理出去了。

新老生代垃圾回收方式

新老生代采用不同的垃圾回收算法来提高效率,对象最开始都会先被分配到新生代(如果新生代内存空间不够,直接分配到老生代),新生代中的对象会在满足某些条件后,被移动到老生代,这个过程也叫晋升

新生代的垃圾回收方式

将内存空间一分为二,分为 From 空间(使用状态), To 空间(闲置状态)

当新生代内存不足时,会将 From 空间中存活的对象复制到到 To 空间,然后将 From 空间清空,交换 From 空间和 To 空间(将原来的 From 空间变为 To 空间),继续下一轮

老生代的垃圾回收方式

V8 在老生代中主要采用了 Mark-Sweep 和 Mark-Compact 相结合的方式

Mark-Sweep 遍历堆内存中的所有对象,并标记活着的对象,在随后的清除阶段,只清除没有被标记的对象

Mark-Sweep 最大的问题就是,在进行清除回收以后,内存空间会出现不连续的状态,会造成内存碎片化

Mark-Compact 用来解决内存碎片的问题,将将存活对象向内存一侧移动,清空内存的另一侧,这样空闲的内存都是连续的

分代内存

64 位系统,新生代内存大小为 32MB,老生代内存为 1.4G;32 位系统,新生代内存大小为 16MB,老生代内存为 0.7G

V8 内存浅析
「硬核 JS」你真的了解垃圾回收机制吗

总结

希望通过《10 万字前端知识体系总结》这 4 篇文章,让小伙伴们对前端知识体系有初步的了解

是的,这一切只是刚刚开始

笔记中还有几万字关于项目的总结与收获,未完待续,持续更新中……

如果觉得文章对你有帮助,可以点个 👍,你的「赞」是我创作的最大动力

10w 字总结的其他篇章

「历时 8 个月」10 万字前端知识体系总结(基础知识篇)

「历时 8 个月」10 万字前端知识体系总结(算法篇)

「历时 8 个月」10 万字前端知识体系总结(工程化篇)

厉害.jpg