面试题系列手记

439 阅读29分钟

vue相关

vue 生命周期

vue生命周期也就是:开始创建->初始化数据->编译模板->挂载dom->数据更新重新渲染虚拟 dom->最后销毁。这一系列的过程就是vue的生命周期。所以在mounted阶段真实的DOM就已经存在了。

每个生命周期什么时候被调用

  • beforeCreate:在实例初始化之后,数据观察(data observer)之前被调用。vue实例的挂载元素el和数据对象data都还没有进行初始化,还是一个 undefined状态也就是说不能访问到data、computed、watch、methods上的方法和数据。
  • created: 此时vue实例的数据对象data已经有了,可以访问里面的数据和方法, 但是此时渲染得节点还未挂载到 DOM,所以不能访问到 $el 属性。
  • beforeMount: 在这里vue实例的元素el和数据对象都有了,只不过在挂载之前还是虚拟的dom节点
  • mounted: vue实例已经挂在到真实的dom上,可以通过对 dom操作来获取dom节点
  • beforeUpdate: 响应式数据更新时调用,发生在虚拟dom打补丁之前。
  • updated: 虚拟dom重新渲染和打补丁之后调用。
  • beforeDestory: vue实例在销毁前调用,在这里还可以使用实例this。
  • destoryed:vue实例销毁后调用,调用后所有事件监听器会被移除,所有的子实例都会被销毁。该钩子在服务端渲染期间不被调用。
  • activated:keep-alive页面第一次进入的时候,钩子触发的顺序是created->mounted->activated
  • deactivated: 页面退出的时候会触发deactivated,当再次前进或者后退的时候只触发activated 每个生命周期可以做什么事情
  • created: 实例已经创建完成,因为他是最早触发的原因可以进行一些数据,资源的请求。
  • mounted: 实例已经挂载完成,可以进行一些DOM操作。
  • beforeUpdate: 适合在更新之前访问现有的 dom,比如手动移除已添加的事件监听器,这不会触发附加的重渲染过程。
  • updated: 大多数情况下应该避免在这个钩子函数中操作数据,防止死循环。该钩子在服务器端渲染期间不被调用。
  • beforeDestory: 可以在这里对一些不用的定时器进行清除,解绑原生事件。

子父生命周期

创建实例从外到内(从父组件到子组件) 渲染是从内到外(子组件渲染完 父组件才会渲染)

挂载阶段

  • 父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

子组件更新阶段

  • 父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

父组件更新阶段

  • 父 beforeUpdate -> 父 updated

销毁阶段

  • 父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

谈谈MVVM与MVC区别,及如何实现双向数据绑定。

MVVM与MVC

  1. 什么是MVVM:
  2. 什么是MVC:
  3. 有什么区别:

双向数据绑定

Object.defineProperty :默认Vue在初始化数据的时候,会给data中的属性使用Object.defineProperty重新定义所有属性,当页面取到对应属性时,会进行依赖收集(收集当前组件的watcher)如果属性发生变化会通知相关依赖进行更新操作。 源码具体流程:

  1. 初始化用户传入的data数据
  2. 用Observer类对数据进行观察
  3. 进行对象的处理
  4. 用defineReactive方法循环对象属性定义响应式变化
  5. 使用Object.defineProperty包装数据,在get方法中拦截属性的获取,使用dep.depend方法进行依赖收集。在set方法中拦截属性的更新操作,使用dep.notify方法通知相关依赖,然后进行更新视图操作。

vue中如何检测数组变化?

使用函数劫持的方式重写了数组原型中的7种(push,pop,shift,unshift,splice,reverse,sotr)方法。 在Observer类中会改变数组原型链的指向,将原型链指向自己定义的数组原型方法,在这些方法中可以通知依赖更新,如果添加新数据时包含引用类型,则会再次对新数据中引用类型进行遍历观察。

为什么只重写七个数组方法:因为这七个方法会改变原数组内容,只要改了数组内容就更新,而其他像concat这种就不会改变原数组所以不需要。

为什么不监听数组下标:实际defineproperty可以对数组下标进行监听,因为监听数组需要遍历数组每一项,性能与用户操作优化不成正比,故抛弃。

proxy与defineproperty 区别及优劣势

  1. Proxy 可以直接监听对象而非属性;
  2. Proxy 可以直接监听数组的变化;
  3. Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
  4. Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
  5. Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
  6. 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。

为什么vue采用异步渲染

因为Vue是组件级更新,一个组件可能同时有多个值会更新,如果每次值发生变化就更新视图的话会导致更新太频繁从而影响性能,所以为了性能考虑,Vue会在数据全部更新后在统一进行一次更新视图。个人理解类似防抖节流,有异曲同工之妙。

  1. 当被观察的数据发生变化后通知dep.notify方法进行更新操作。
  2. notify方法通知依赖管理器中对应的依赖项update方法进行更新。
  3. update方法并不会立即执行而是会放入queueWatcher队列中。
  4. 在queueWatcher中会过滤相同的watcher,然后调用nextTick方法更新数据。

nextTick的原理

nextTick方法主要是使用了宏任务和微任务,定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。所以这个nextTick方法就是异步方法。

  1. nextTick(cb):视图更新时框架内部调用或者用户主动使用nextTick方法时。
  2. callbacks.push(cb): 会将nextTick中的回调函数放入callbacks队列中(可能同时有多个nextTick所以用队列存储)。
  3. timerFunc():等代码都执行完后再调用timerFunc方法清空callbacks队列。
  4. timerFunc中会判断会做一些判断并在最后调用flushCallbacks方法清空队列。
    1. 如果支持的话会返回promise,并在.then中异步执行队列中的方法。
    2. 如果不支持promise并且当前不是ie浏览器,则会使用MutationObserver方法,
    3. 如果还不支持则使用setImmediate方法
    4. 以上方法都不支持时则使用最原始的setTimeout方法。
  5. flushCallbacks中会依次按顺序执行callbacks队列中的回调函数。

谈谈你对VirtualDOM 原理理解

  1. 虚拟mod是什么 Virtual DOM是对DOM的抽象,本质上是JavaScript对象,这个对象就是更加轻量级的对DOM的描述。

  2. 为什么需要虚拟dom 首先,我们都知道在前端性能优化的一个秘诀就是尽可能少地操作DOM,不仅仅是DOM相对较慢,更因为频繁变动DOM会造成浏览器的回流或者重回,这些都是性能的杀手,因此我们需要这一层抽象,在patch过程中尽可能地一次性将差异更新到DOM中,这样保证了DOM不会出现性能很差的情况.其次,现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率.最后,也是Virtual DOM最初的目的,就是更好的跨平台,比如Node.js就没有DOM,如果想实现SSR(服务端渲染),那么一个方式就是借助Virtual DOM,因为Virtual DOM本身是JavaScript对象.

vue DOM-diff 算法

  1. 先进行同级比较,在比较子节点。
  2. 先判断一方有childen一方没有childen的情况。
  3. 在比较双方都有childen的情况。
  4. 递归比较子节点。
  5. 优化递归比较。
  • 在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较,时间复杂度是O(n)。

  • 一般的diff算法都是使用深度优先遍历,对新旧两棵树进行一次深度优先的遍历,这样每个节点都会有一个唯一的标记。在遍历的时候,每遍历到一个节点就把该节点和新的树的同一个位置的节点进行对比,如果有差异的话就记录到一个对象里面。

vue 计算属性(computed)和 侦听属性(watch)

computed: 本质上也是一个watch,是具备缓存的,只有依赖的值改变才会重新执行函数,计算属性是取返回值作为最新结果,所以里面不能异步的返回结果。不能写异步逻辑。

原理:

  1. vue在初始化数据时会new Watcher生成一个watch并带有lazy:true、dirty:true的标识。lazy表示是否为计算属性。

  2. 默认不执行computed中的方法,使用defineComputed将属性定义到实例上。

  3. 当用户取值时判断dirty属性:

    1. 为true:会开始计算并返回结果,计算时会进行依赖收集,并将dirty设置为false。
    2. 为false:直接返回上一次的计算结果。
    3. 当依赖发生变化时会将dirty设置为true,重新计算结果。

watch:侦听属性是侦听的值改变会重新执行函数,将一个值重新赋值作为最新结果,所以赋值的时候可以进行一些异步操作。 在做依赖收集时由于watch可能是引用类型,所以在取引用类型中的属性值时,可能就不会通知依赖进行更新,例如侦听了对象obj,但是我们修改了obj对象中的a属性,是不会触发侦听效果的。因为在依赖收集时并没有收集引用类型内部的属性。因此vue提供了一个属性deep,设置了deep:true的话就会对引用类型进行深度遍历收集依赖,这样虽然能收集所有的依赖,但是由于是深度遍历导致性能消耗比较大。

两者的区别

  1. 两者都是new Watcher生成的,通过lazy表示是否为计算属性,通过user表示是否为用户自己定义的watch。
  2. computed具有缓存结果功能, watch没有。
  3. computed默认不执行,watch默认开始就会执行。
  4. watch可以通过设置deep属性进行深度观察,computed由于在{{xxx}}取值时内部会使用JSON.stringify获取,这样可以获取到引用类型内部所有的属性,所以都会被观察,不需要手动设置。
  5. watch可以异步返回结果,computed不能异步返回。

vue中模板编译原理

  1. 将模板转化成ast树,使用一个对象来描述虚拟dom。
  2. 优化树,标记静态节点。
  3. 将ast生成js代码。

Vue中的的通信方式有几种?隔代组件的通信你用那种方式解决?

  1. props/$emit 适用父子组件通信
  2. ref与 $parent/$children适用父子组件通信
  3. EventBus(事件总线) 适用于父子、隔代、兄弟组件通信
  4. attrs/listeners 适用于隔代组件通信
  5. provide/inject 适用于隔代组件通信
  6. vuex 适用于父子、隔代、兄弟组件通信
  7. 作用域slot插槽方式

Vue中的常见指令有那些?

v-text/v-html/v-for/v-show/v-if/v-else/v-cloak/v-bind/v-on/v-model/v-slot

v-show和v-if有什么区别?

  • v-show只是切换当前dom的显示隐藏。
  • v-if是完整的销毁和重新创建。
  • 如果频繁切换时用v-show,运行时较少改变用v-if。

为什么v-for和v-if不能在同一标签中同时使用

因为v-for的优先级比v-if要高,因此在编译完成后生成的标签都会判断一次v-if,如果for的次数较多时会影响性能,所以一般会在v-for的标签外包一层template标签进行v-if的判断,这样可以避免for生成的标签都判断v-if,并且在v-if不成立时不进行循环生成标签,从而提高性能。

描述组件渲染和更新过程

渲染组件时,会通过Vue.extend方法构建子组件的构造函数,并进行实例化。最终手动调用$mount()进行挂载。 更新组件会进行patchVnode流程。

谈谈你对vuex的理解

vuex是一个专门为vue.js开发的状态管理模式,每一个vuex应用核心就是store(仓库)。store基本上就是一个容器,它包含着你的应用中大部分的state(状态)

  • vuex的状态存储是响应式的,当 vue组件中store中读取状态时候,若store中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 改变store中的状态的唯一途径就是显示 commit(提交)mutation,这样使得我们可以方便地跟踪每一个状态的变化。

主要有以下几个模块:

  • State: 定义了应用状态的数据结构,可以在这里设置默认的初始状态
  • Getter: 允许组件从Stroe中获取数据, mapGetters辅助函数仅仅是将store中的getter映射到计算属性。
  • Mutation: 唯一更改store中状态的方法,且必须是同步函数。
  • Action: 用于提交muatation, 而不是直接变更状态,可以包含任意异步操作。
  • Module: 允许将单一的store拆分为多个 sotre且同时保存在单一的状态树中

问题: vuex刷新数据丢失,如何解决?

vue中事件绑定的原理

  1. 原生dom事件的绑定:采用的是addEventListener实现
  2. 组件绑定事件采用的是vue独有的$on方法 可以通过@click.nativeOn修饰符将组件的事件绑定通过addEventListener方式实现。

v-bind和v-model的区别, v-model原理(双向数据绑定)

  • v-bind用来绑定数据和属性以及表达式。

  • v-model一般使用在表单或者组件中,实现双向数据绑定的。 v-model原理:我们在vue项目中主要使用v-model指令在表单 input、textarea、select、等表单元素或者组件上创建双向数据绑定, v-model本质上就是语法糖,v-model在内部为不同的输入元素使用不同的属性并抛出不同的事件:

  • text和 textarea元素使用value属性和input事件。

  • checkbox和 radio使用checked属性和change事件。

  • slect字段将 value作为prop并将change作用事件。

  • 组件中默认使用的是value+input实现,当然我们也可以自定义v-model的实现:

    model: {
        prop: 'check',
        event: 'change'
    },
    props:{
        check: Boolen
    }

Vue中的组件data为什么必须是函数?

同一个组件被复用多次,会创建多个实例,这些实例是同一个构造函数创建出来的,为了被构造出来的实例之间互不干扰。 js里对象是引用关系,如果组件data是一个对象,那么子组件中的data会共用一个对象,属性值会互相污染。所以一个组件中的data必须是一个函数,因此每个实例可以维护一份被返回对象独立的拷贝。

也因为new Vue的实例是不会被复用,所以不存在以上问题,data属性可以是对象。官方文档讲的很详细,想要深入了解可以看下文档中的解释。

mixin

一般用于给组件的每个生命周期,函数等混入一些公共逻辑。 副作用是可能会不知道一些变量或者函数的来源。 合并生明周期时源码内部是使用数组形式存储的,通过concat进行合并,然后依次执行,所以mixin中的生命周期会比组件的生命周期要先执行。

路由懒加载(异步组件)

可以解决单页面应用第一次会把所有资源进行加载,导致首页加载慢的问题。

  • 使用require 进行引入 可进行懒加载 生成一个js文件
  • 使用import 进行引入 每个组件生成一个js文件
  • 使用import + webpackChunkName 按组件生成js文件 (checkName相同生成同一个文件)
  • 使用requir.ensure()

插槽

分为默认插槽,具名插槽,作用域插槽。 v-slot 可以缩写成#。例如v-slot:header 可以被重写为 #header

  1. 默认插槽,具名插槽:

    创建组件虚拟节点时,会将组件的儿子的虚拟节点保存起来,当初始化组件时会按插槽属性将子虚拟节点替换到对应的标签中。(插槽的作用域为父组件,插槽中HTML模板显示不显示、以及怎样显示由父组件来决定)

  2. 作用域插槽slot-scope:

    作用域插槽在解析的时候,不会作为组件的孩子节点。会解析成函数,当子组件渲染时,会调用此函数进行渲染。 或者可以说成作用域插槽是子组件,可以在slot标签上绑定属性值,在父组件可以拿到子组件的数据,通过子组件绑定数据传递给父组件。(插槽的作用域为子组件

image.png

vue-router

  • hash模式 监听hashchange实现, url带#号
  • history模式 根据H5新出的 pushState/replaceState 监听实现 没有#号 需要后台配置 nginx需要配置 root / try_files 和 index vue-router导航守卫有哪些作用? image.png

keep-alive 组件缓存

作用:

  • keep-alive用于保存组件的渲染状态,实现组件缓存。
  • include定义缓存白名单,keep-alive会缓存命中的组件;exclude定义缓存黑名单,被命中的组件将不会被缓存;max定义缓存组件上限,超出上限使用LRU的策略置换缓存数据。

原理:

  • 本质就是一个抽象组件,不会出现在父组件链中。
  • 第一步:获取keep-alive包裹着的第一个子组件对象及其组件名;
  • 第二步:根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode),否则执行第三步;
  • 第三步:根据组件ID和tag生成缓存Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该key在this.keys中的位置(更新key的位置是实现LRU置换策略的关键),否则执行第四步;
  • 第四步:在this.cache对象中存储该组件实例并保存key值,之后检查缓存的实例数量是否超过max设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key);
  • 第五步:最后并且很重要,将该组件实例的keepAlive属性值设置为true。

Vuex原理

使用install进行注册,给vue实例添加$store,生成一个单独的vuex响应(类似eventbas)

vue中常见的性能优化

vue编码优化:
  1. 不要将所有的数据放在data中,因为在初始化组件时会递归遍历data中的数据,且都会增加getter和setter,会收集对应的watcher。
  2. 在使用v-for时,如果要给每项元素绑定事件可以使用事件代理。
  3. 合理运用keep-alive进行组件缓存。
  4. 拆分组件。提高复用性,增加代码的可维护性,减少不必要的渲染。
  5. 按需展示组件时,如果频繁切换时用v-show,运行时较少改变用v-if,因为v-if当值是false时内部指令不会执行,具有阻断功能。
  6. 使用v-for时提供唯一的key属性,如果不进行删减操作可使用index作为key,否则需要唯一的key。
  7. 合理使用路由懒加载、异步组件。
  8. 尽量使用runtime运行时版本。
  9. 数据持久化的问题(防抖、节流)。
vue加载性能优化:
  • 第三方模块按需导入 [bable-plugin-component]。
  • 使用虚拟列表动态加载 [vue-virtual-scroll-list]。
  • 图片懒加载 [vue-lazyload]。
SEO优化:
  • 预渲染插件 prerender-spa-plugin
  • 服务端渲染ssr。
打包优化:
  • 使用cdn的方式加载第三方模块。
  • 多线程打包happypack。
  • splitChunks抽离公共文件。
  • sourceMap生成

Vue3 常用新特性

  • 组件v-model支持参数 在Vue2.x时,我们要想给子组件传值,还得单独传入。Vue3.x直接以v-model:xxx形式传入参数,并且配合修饰符.sync进行数据同步更新。
  • 组件支持多个v-model 在Vue3.x中支持在单个组件上可以创建多个v-model绑定。
  • setup setup函数是一个新的组件选项。作为在组件内使用Composition Api的入口点。
    • 调用时机 创建组件实例,然后初始化props,紧接着就调用setup函数。 从生命周期的角度来看,它会在beforeCreate之前执行。也就是创建组件先执行setup、beforeCreate、create。
    • this指向 由于不能在setup函数中使用data、methods,为了避免使用Vue出错,所以把setup函数中this修改为了undefined。
    • 参数 props context
    • proprs 接收组件传递过来的所有数据,并且都是响应式的。(注意一点,props数据不能使用解构,否则响应式数据失效);
    • context 该参数提供一个上下文对象,从原来的2.x中选择性的暴露了一些属性。(attrs,slots,emit)
    • 返回值 可以将setup函数返回值渲染到页面上。但前提是,setup返回值必须是一个对象,否则返回其它值则渲染无效。
  • reactive 该方法接收一个参数{}创建一个响应式对象。跟Vue2.x的Vue.observable一样。如果该参数不是对象的话,也可以渲染到模板上,但不是响应式的数据。
  • ref 该方法接收一个参数,可以是单个值,也可以是一个对象,并且都是响应式的数据。当传入一个对象时{},内部将调用reactive方法进行转换为响应式数据。返回值里面带有.value属性取值,当使用模板渲染的时候可省去.value。
  • computed 该方法可以传入一个函数,默认该函数就是getter,不管getter返回值为一个ref响应式数据还是一个普通变量,数据都是只读不能改变。只有传入一个对象set和get函数方法,这样才可以修改。
  • readonly 该方法接收传入一个对象,默认是只读功能,是深层对象只读,不管嵌套多少层的属性都是只读状态。
  • watchEffect 该方法接收一个函数并且立即执行,并当该函数里的变量变更时,重新执行该函数。该方法无法获取到原值,只能是改变之后的值。有时候我们想在触发一定的条件后取消监听。这时可以执行watchEffect的返回值。
  • watch watch等同于Vue2.x中的this.$watch,watch需要监听特定数据,默认情况是懒执行,也就是只有当数据发生变化时才执行第二个参数函数。(可以监听多个值,返回的值是一个数组)
  • 生命周期系列 在Vue3.X也可以在setup函数下使用生命周期,这些钩子函数写法跟之前的生命周期写法不同。
    • 与Vue2.x版本生命周期相对应的组合式Api
      • beforeCreate --> setup
      • created --> setup
      • beforeMount --> onBeforeMount
      • mounted --> onMounted
      • beforeUpdate --> onBeforeUpdate
      • updated --> onUpdated
      • beforeDestroy --> onBeforeUnmount
      • destroyed --> onUnmount
  • provide & inject 该方法和Vue2.x的 provide、inject一样的。但是Vue3新特性这俩方法只能在setup中使用。
  • refs 该方法相当于Vue2.x的refs一样获取元素,那么在setup中配合使用ref对象进行获取。
  • isReadonly 用于检测该数据是不是可读数据。返回一个Boolean类型。
  • isRef 用于检测该数据是不是ref响应式数据。返回一个Boolean类型。
  • isReactive 用于检测该数据是不是reacttive响应式数据。返回一个Boolean类型。
  • 移除过滤器filters 在Vue3.x中移除过滤器,不在支持。建议使用computed去替代。贴一个官网例子
  • 不再限制Template一个根节点 Vue3.x中将不在限制模板中只有一个根节点,根组件可以任意多个元素。
  • 自定义v-model修饰符 Vue3.x中,添加了可以自定义修饰符,如Api提供的内置方法.trim,新特性我们也可以自定义啦。
  • 废弃on,off,once实例方法 Vue3.x中 onon,off 和 $once 实例方法已被移除,应用实例不再实现事件触发接口。
  • 自定义指令更改

js相关

js异步解决方案的发展历程及优缺点

  • 回调函数

    缺点:回调地狱,不能用 try catch 捕获错误,不能 return

    回调地狱的根本问题在于:

    • 缺乏顺序性:回调地狱导致的调试困难,和大脑的思维方式不符
    • 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转)
    • 嵌套函数过多的多话,很难处理错误 优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。)
  • promise

    Promise就是为了解决callback的问题而产生的。

    Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装

    优点:解决了回调地狱的问题

    缺点:无法取消 Promise ,错误需要通过回调函数来捕获

  • Generator

    特点:可以控制函数的执行,可以配合 co 函数库使用

  • Async/await

async、await 是异步的终极解决方案

优点是:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题

缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

问题:4种方式是实现一个sleep函数

  1. Promise
//Promise
const sleep = time => {
  return new Promise(resolve => setTimeout(resolve,time))
}
sleep(1000).then(()=>{
  console.log(1)
})
  1. Generator
//Generator
function* sleepGenerator(time) {
  yield new Promise(function(resolve,reject){
    setTimeout(resolve,time);
  })
}
sleepGenerator(1000).next().value.then(()=>{console.log(1)})
  1. async/await
//async
function sleep(time) {
  return new Promise(resolve => setTimeout(resolve,time))
}
async function output() {
  let out = await sleep(1000);
  console.log(1);
  return out;
}
output();
  1. ES5
//ES5
function sleep(callback,time) {
  if(typeof callback === 'function')
    setTimeout(callback,time)
}
function output(){
  console.log(1);
}
sleep(output,1000);

bind、call、apply 区别

首先说下前两者的区别。

callapply 都是为了解决改变 this 的指向。作用都是相同的,只是传参的方式不同。

除了第一个参数外,call 可以接收一个参数列表,apply 只接受一个参数数组。

bind 和其他两个方法作用也是一致的,只是该方法会返回一个函数。并且我们可以通过 bind 实现柯里化。

问题:

如何实现一个 bind 函数

对于实现以下几个函数,可以从几个方面思考

  • 不传入第一个参数,那么默认为 window
  • 改变了 this 指向,让新的对象可以执行该函数。那么思路是否可以变成给新的对象添加一个函数,然后在执行完以后删除?
Function.prototype.myBind = function (context) {
    if (typeof this !== 'function') {
        throw new TypeError('Error')
    }
    var _this = this
    var args = [...arguments].slice(1)
    // 返回一个函数
    return function F() {
        // 因为返回了一个函数,我们可以 new F(),所以需要判断
        if (this instanceof F) {
            return new _this(...args, ...arguments)
        }
        return _this.apply(context, args.concat(...arguments))
    }
}

如何实现一个 call 函数

Function.prototype.myCall = function (context) {
    var context = context || window
    // 给 context 添加一个属性
    // getValue.call(a, 'yck', '24') => a.fn = getValue
    context.fn = this
    // 将 context 后面的参数取出来
    var args = [...arguments].slice(1)
    // getValue.call(a, 'yck', '24') => a.fn('yck', '24')
    var result = context.fn(...args)
    // 删除 fn
    delete context.fn
    return result
}

如何实现一个 apply 函数

Function.prototype.myApply = function (context) {
    var context = context || window
    context.fn = this
    var result
    // 需要判断是否存储第二个参数
    // 如果存在,就将第二个参数展开
    if (arguments[1]) {
        result = context.fn(...arguments[1])
    } else {
        result = context.fn()
    }
    delete context.fn
    return result
}

简单说下原型链?

原型链图解.jpg

每个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型。

每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 __proto__ 来访问。

对象可以通过 __proto__ 来寻找不属于该对象的属性,__proto__ 将对象连接起来组成了原型链。

如果你想更进一步的了解原型,可以仔细阅读 深度解析原型中的各个难点

怎么判断对象类型?

  • 可以通过 Object.prototype.toString.call(xx)。这样我们就可以获得类似 [object Type] 的字符串。
  • instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype

箭头函数的特点

function a() {
    return () => {
        return () => {
          console.log(this)
        }
    }
}
console.log(a()()())

箭头函数其实是没有 this 的,这个函数中的 this 只取决于他外面的第一个不是箭头函数的函数的 this。在这个例子中,因为调用 a 符合前面代码中的第一个情况,所以 this 是 window。并且 this 一旦绑定了上下文,就不会被任何代码改变。

This

this 是很多人会混淆的概念,但是其实他一点都不难,你只需要记住几个规则就可以了。

function foo() {
  console.log(this.a)
}
var a = 1
foo()
var obj = {
  a: 2,
  foo: foo
}
obj.foo()
// 以上两者情况 `this` 只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况
// 以下情况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)
// 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new

async、await 优缺点

async 和 await 相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码。缺点在于滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。

下面来看一个使用 await 的代码。

var a = 0
var b = async () => {
  a = a + await 10
  console.log('2', a) // -> '2' 10
  a = (await 10) + a
  console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1

对于以上代码你可能会有疑惑,这里说明下原理

  • 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为在 await 内部实现了 generators ,generators 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  • 因为 await 是异步操作,遇到await就会立即返回一个pending状态的Promise对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,所以会先执行 console.log('1', a)
  • 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10
  • 然后后面就是常规执行代码了

generator 原理

Generator 是 ES6 中新增的语法,和 Promise 一样,都可以用来异步编程

// 使用 * 表示这是一个 Generator 函数
// 内部可以通过 yield 暂停代码
// 通过调用 next 恢复执行
function* test() {
  let a = 1 + 2;
  yield 2;
  yield 3;
}
let b = test();
console.log(b.next()); // >  { value: 2, done: false }
console.log(b.next()); // >  { value: 3, done: false }
console.log(b.next()); // >  { value: undefined, done: true }

从以上代码可以发现,加上 *的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。以下是 Generator 函数的简单实现

// cb 也就是编译过的 test 函数
function generator(cb) {
  return (function() {
    var object = {
      next: 0,
      stop: function() {}
    };
    return {
      next: function() {
        var ret = cb(object);
        if (ret === undefined) return { value: undefined, done: true };
        return {
          value: ret,
          done: false
        };
      }
    };
  })();
}
// 如果你使用 babel 编译后可以发现 test 函数变成了这样
function test() {
  var a;
  return generator(function(_context) {
    while (1) {
      switch ((_context.prev = _context.next)) {
        // 可以发现通过 yield 将代码分割成几块
        // 每次执行 next 函数就执行一块代码
        // 并且表明下次需要执行哪块代码
        case 0:
          a = 1 + 2;
          _context.next = 4;
          return 2;
        case 4:
          _context.next = 6;
          return 3;
    // 执行完毕
        case 6:
        case "end":
          return _context.stop();
      }
    }
  });
}

Promise

Promise 是 ES6 新增的语法,解决了回调地狱的问题。

可以把 Promise 看成一个状态机。初始是 pending 状态,可以通过函数 resolvereject ,将状态转变为 resolved 或者 rejected 状态,状态一旦改变就不能再次变化。

then 函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。

对于 then 来说,本质上可以把它看成是 flatMap

如何实现一个 Promise

// 三种状态
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一个函数参数,该函数会立即执行
function MyPromise(fn) {
  let _this = this;
  _this.currentState = PENDING;
  _this.value = undefined;
  // 用于保存 then 中的回调,只有当 promise
  // 状态为 pending 时才会缓存,并且每个实例至多缓存一个
  _this.resolvedCallbacks = [];
  _this.rejectedCallbacks = [];
  _this.resolve = function (value) {
    if (value instanceof MyPromise) {
      // 如果 value 是个 Promise,递归执行
      return value.then(_this.resolve, _this.reject)
    }
    setTimeout(() => { // 异步执行,保证执行顺序
      if (_this.currentState === PENDING) {
        _this.currentState = RESOLVED;
        _this.value = value;
        _this.resolvedCallbacks.forEach(cb => cb());
      }
    })
  };
  _this.reject = function (reason) {
    setTimeout(() => { // 异步执行,保证执行顺序
      if (_this.currentState === PENDING) {
        _this.currentState = REJECTED;
        _this.value = reason;
        _this.rejectedCallbacks.forEach(cb => cb());
      }
    })
  }
  // 用于解决以下问题
  // new Promise(() => throw Error('error))
  try {
    fn(_this.resolve, _this.reject);
  } catch (e) {
    _this.reject(e);
  }
}
MyPromise.prototype.then = function (onResolved, onRejected) {
  var self = this;
  // 规范 2.2.7,then 必须返回一个新的 promise
  var promise2;
  // 规范 2.2.onResolved 和 onRejected 都为可选参数
  // 如果类型不是函数需要忽略,同时也实现了透传
  // Promise.resolve(4).then().then((value) => console.log(value))
  onResolved = typeof onResolved === 'function' ? onResolved : v => v;
  onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;
  if (self.currentState === RESOLVED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      // 规范 2.2.4,保证 onFulfilled,onRjected 异步执行
      // 所以用了 setTimeout 包裹下
      setTimeout(function () {
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }
  if (self.currentState === REJECTED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      setTimeout(function () {
        // 异步执行onRejected
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }
  if (self.currentState === PENDING) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      self.resolvedCallbacks.push(function () {
        // 考虑到可能会有报错,所以使用 try/catch 包裹
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });
      self.rejectedCallbacks.push(function () {
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });
    }));
  }
};
// 规范 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
  // 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
  if (promise2 === x) {
    return reject(new TypeError("Error"));
  }
  // 规范 2.3.2
  // 如果 x 为 Promise,状态为 pending 需要继续等待否则执行
  if (x instanceof MyPromise) {
    if (x.currentState === PENDING) {
      x.then(function (value) {
        // 再次调用该函数是为了确认 x resolve 的
        // 参数是什么类型,如果是基本类型就再次 resolve
        // 把值传给下个 then
        resolutionProcedure(promise2, value, resolve, reject);
      }, reject);
    } else {
      x.then(resolve, reject);
    }
    return;
  }
  // 规范 2.3.3.3.3
  // reject 或者 resolve 其中一个执行过得话,忽略其他的
  let called = false;
  // 规范 2.3.3,判断 x 是否为对象或者函数
  if (x !== null && (typeof x === "object" || typeof x === "function")) {
    // 规范 2.3.3.2,如果不能取出 then,就 reject
    try {
      // 规范 2.3.3.1
      let then = x.then;
      // 如果 then 是函数,调用 x.then
      if (typeof then === "function") {
        // 规范 2.3.3.3
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            // 规范 2.3.3.3.1
            resolutionProcedure(promise2, y, resolve, reject);
          },
          e => {
            if (called) return;
            called = true;
            reject(e);
          }
        );
      } else {
        // 规范 2.3.3.4
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 规范 2.3.4,x 为基本类型
    resolve(x);
  }
}

== 和 ===区别,什么情况用 ==

这里来解析一道题目 [] == ![] // -> true ,下面是这个表达式为何为 true 的步骤

// [] 转成 true,然后取反变成 false
[] == false
// 根据第 8 条得出
[] == ToNumber(false)
[] == 0
// 根据第 10 条得出
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根据第 6 条得出
0 == 0 // -> true

===用于判断两者类型和值是否相同。 在开发中,对于后端返回的 code,可以通过 == 去判断。

垃圾回收

V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代和老生代两部分。

新生代算法

新生代中的对象一般存活时间较短,使用 Scavenge GC 算法。

在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。

老生代算法

老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。

在讲算法前,先来说下什么情况下对象会出现在老生代空间中:

  • 新生代中的对象是否已经经历过一次 Scavenge 算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。
  • To 空间的对象占比大小超过 25 %。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。

老生代中的空间很复杂,有如下几个空间

enum AllocationSpace {
  // TODO(v8:7464): Actually map this space's memory as read-only.
  RO_SPACE,    // 不变的对象空间
  NEW_SPACE,   // 新生代用于 GC 复制算法的空间
  OLD_SPACE,   // 老生代常驻对象空间
  CODE_SPACE,  // 老生代代码对象空间
  MAP_SPACE,   // 老生代 map 对象
  LO_SPACE,    // 老生代大空间对象
  NEW_LO_SPACE,  // 新生代大空间对象
  FIRST_SPACE = RO_SPACE,
  LAST_SPACE = NEW_LO_SPACE,
  FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
  LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};

在老生代中,以下情况会先启动标记清除算法:

  • 某一个空间没有分块的时候
  • 空间中被对象超过一定限制
  • 空间不能保证新生代中的对象移动到老生代中 在这个阶段中,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。在标记大型对内存时,可能需要几百毫秒才能完成一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工作分解为更小的模块,可以让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿情况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可以让 GC 扫描和标记对象时,同时允许 JS 运行,你可以点击 该博客 详细阅读。

清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象像一端移动,直到所有对象都移动完成然后清理掉不需要的内存。

闭包

闭包的定义很简单:函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。

function A() {
  let a = 1
  function B() {
      console.log(a)
  }
  return B
}
var b = A()
b() // 1

你是否会疑惑,为什么函数 A 已经弹出调用栈了,为什么函数 B 还能引用到函数 A 中的变量。因为函数 A 中的变量这时候是存储在堆上的。现在的 JS 引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上,哪些需要存储在栈上。

经典面试题,循环中使用闭包解决 var 定义函数的问题

for ( var i=1; i<=5; i++) {
  setTimeout( function timer() {
    console.log( i );
  }, i*1000 );
}

首先因为 setTimeou 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

解决办法有以下几种,第一种使用闭包

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}

第二种就是使用 setTimeout 的第三个参数

for ( var i=1; i<=5; i++) {
  setTimeout( function timer(j) {
    console.log( j );
  }, i*1000, i);
}

第三种就是使用 let 定义 i 了

for ( let i=1; i<=5; i++) {
  setTimeout( function timer() {
    console.log( i );
  }, i*1000 );
}

因为对于 let 来说,他会创建一个块级作用域,相当于

{ // 形成块级作用域
  let i = 0
  {
    let ii = i
    setTimeout( function timer() {
        console.log( ii );
    }, i*1000 );
  }
  i++
  {
    let ii = i
  }
  i++
  {
    let ii = i
  }
  ...
}

基本数据类型和引⽤类型在存储上的差别

前者存储在栈上,后者存储在堆上

浏览器 Eventloop 和 Node 中的有什么区别

众所周知 JS 是门非阻塞单线程语言,因为在最初 JS 就是为了和浏览器交互而诞生的。如果 JS 是门多线程的语言话,我们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另一个线程中删除节点),当然可以引入读写锁解决这个问题。 JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。

以上代码虽然 setTimeout 延时为 0,其实还是异步。这是因为 HTML5 标准规定这个函数第二个参数不得小于 4 毫秒,不足会自动增加。所以 setTimeout 还是会在 script end 之后打印。 不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。

以上代码虽然 setTimeout 写在 Promise 之前,但是因为 Promise 属于微任务而 setTimeout 属于宏任务,所以会有以上的打印。 微任务包括 process.nextTick ,promise ,Object.observe ,MutationObserver 宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering 很多人有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务。 所以正确的一次 Event loop 顺序是这样的 执行同步代码,这属于宏任务 执行栈为空,查询是否有微任务需要执行 执行所有微任务 必要的话渲染 UI 然后开始下一轮 Event loop,执行宏任务中的异步代码 通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的 界面响应,我们可以把操作 DOM 放入微任务中。

Node 中的 Event loop

Node 中的 Event loop 和浏览器中的不相同。

Node 的 Event loop 分为6个阶段,它们会按照顺序反复运行

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<──connections───     │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

timer timers 阶段会执行 setTimeout 和 setInterval 一个 timer 指定的时间并不是准确时间,而是在达到这个时间后尽快执行回调,可能会因为系统正在执行别的事务而延迟。 下限的时间有一个范围:[1, 2147483647] ,如果设定的时间不在这个范围,将被设置为1。 **I/O ** I/O 阶段会执行除了 close 事件,定时器和 setImmediate 的回调 idle, prepare idle, prepare 阶段内部实现 poll poll 阶段很重要,这一阶段中,系统会做两件事情 执行到点的定时器 执行 poll 队列中的事件 并且当 poll 中没有定时器的情况下,会发现以下两件事情 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者系统限制 如果 poll 队列为空,会有两件事发生 如果有 setImmediate 需要执行,poll 阶段会停止并且进入到 check 阶段执行 setImmediate 如果没有 setImmediate 需要执行,会等待回调被加入到队列中并立即执行回调 如果有别的定时器需要被执行,会回到 timer 阶段执行回调。 check check 阶段执行 setImmediate close callbacks close callbacks 阶段执行 close 事件 并且在 Node 中,有些情况下的定时器执行顺序是随机的

当然在这种情况下,执行顺序是相同的

上面介绍的都是 macrotask 的执行情况,microtask 会在以上每个阶段完成后立即执行。

Node 中的 process.nextTick 会先于其他 microtask 执行。

setTimeout 倒计时误差 JS 是单线程的,所以 setTimeout 的误差其实是无法被完全解决的,原因有很多,可能是回调中的,有可能是浏览器中的各种事件导致。这也是为什么页面开久了,定时器会不准的原因,当然我们可以通过一定的办法去减少这个误差。 以下是一个相对准备的倒计时实现

防抖 你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。 这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。 PS:防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的 情况会每隔一定时间(参数wait)调用函数。 我们先来看一个袖珍版的防抖理解一下防抖的实现:

这是一个简单版的防抖,但是有缺陷,这个防抖只能在最后调用。一般的防抖会有immediate选项,表示是否立即调用。这两者的区别,举个栗子来说: 例如在搜索引擎搜索问题的时候,我们当然是希望用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数,它总是在一连串(间隔小于wait的)函数触发之后调用。 例如用户给interviewMap点star的时候,我们希望用户点第一下的时候就去调用接口,并且成功之后改变star按钮的样子,用户就可以立马得到反馈是否star成功了,这个情况适用立即执行的防抖函数,它总是在第一次调用,并且下一次调用必须与前一次调用的时间间隔大于wait才会触发。 下面我们来实现一个带有立即执行选项的防抖函数

整体函数实现的不难,总结一下。 对于按钮防点击来说的实现:如果函数是立即执行的,就立即调用,如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都重新计时。一旦你点累了,定时器时间到,定时器重置为 null,就可以再次点击了。 对于延时执行函数来说的实现:清除定时器ID,如果是延迟调用就调用函数 数组降维

如果想将一个多维数组彻底的降维,可以这样实现

深拷贝 这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决。

但是该方法也是有局限性的: 会忽略 undefined 会忽略 symbol 不能序列化函数 不能解决循环引用的对象

如果你有这么一个循环引用对象,你会发现你不能通过该方法深拷贝

在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化

你会发现在上述情况中,该方法会忽略掉函数和 undefined 。 但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。当然如果你的数据中含有以上三种情况下,可以使用 lodash 的深拷贝函数。 如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel

typeof 于 instanceof 区别 typeof 对于基本类型,除了 null 都可以显示正确的类型

typeof 对于对象,除了函数都会显示 object

对于 null 来说,虽然它是基本类型,但是会显示 object,这是一个存在很久了的 Bug

PS:为什么会出现这种情况呢?因为在 JS 的最初版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。 instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。 我们也可以试着实现一下 instanceof

Service worker Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步API。 目前该技术通常用来做缓存文件,提高首屏速度,可以试着来实现这个功能。

打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了

webpack

0.有哪些常见的Loader?你用过哪些Loader?

(我开始熟悉的报起了菜名)

  • raw-loader:加载文件原始内容(utf-8)
  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)
  • url-loader:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值会交给 file-loader 处理,小于阈值时返回文件 base64 形式编码 (处理图片和字体)
  • source-map-loader:加载额外的 Source Map 文件,以方便断点调试
  • svg-inline-loader:将压缩后的 SVG 内容注入代码中
  • image-loader:加载并且压缩图片文件
  • json-loader 加载 JSON 文件(默认包含)
  • handlebars-loader: 将 Handlebars 模版编译成函数并返回
  • babel-loader:把 ES6 转换成 ES5
  • ts-loader: 将 TypeScript 转换成 JavaScript
  • awesome-typescript-loader:将 TypeScript 转换成 JavaScript,性能优于 ts-loader
  • sass-loader:将SCSS/SASS代码转换成CSS
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS
  • postcss-loader:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀
  • eslint-loader:通过 ESLint 检查 JavaScript 代码
  • tslint-loader:通过 TSLint检查 TypeScript 代码
  • mocha-loader:加载 Mocha 测试用例的代码
  • coverjs-loader:计算测试的覆盖率
  • vue-loader:加载 Vue.js 单文件组件
  • i18n-loader: 国际化
  • cache-loader: 可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里 更多 Loader 请参考官网

(面试官:挺好,知道的还挺多)

1.有哪些常见的Plugin?你用过哪些Plugin?

(这大兄弟好像听上瘾了,继续开启常规操作)

  • define-plugin:定义环境变量 (Webpack4 之后指定 mode 会自动配置)
  • ignore-plugin:忽略部分文件
  • html-webpack-plugin:简化 HTML 文件创建 (依赖于 html-loader)
  • web-webpack-plugin:可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用
  • uglifyjs-webpack-plugin:不支持 ES6 压缩 (Webpack4 以前)
  • terser-webpack-plugin: 支持压缩 ES6 (Webpack4)
  • webpack-parallel-uglify-plugin: 多进程执行代码压缩,提升构建速度
  • mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载 (替代extract-text-webpack-plugin)
  • serviceworker-webpack-plugin:为网页应用增加离线缓存功能
  • clean-webpack-plugin: 目录清理
  • ModuleConcatenationPlugin: 开启 Scope Hoisting
  • speed-measure-webpack-plugin: 可以看到每个 Loader 和 Plugin 执行耗时 (整个打包耗时、每个 Plugin 和 Loader 耗时)
  • webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)

2.那你再说一说Loader和Plugin的区别?

(就知道你会问这个,我用手掩盖着嘴角的微笑)

Loader 本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。 因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。

Plugin 就是插件,基于事件流框架 Tapable,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

Loader 在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。

Plugin 在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。

3.Webpack构建流程简单说一下

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  • 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  • 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  • 确定入口:根据配置中的 entry 找出所有的入口文件
  • 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  • 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统 在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
  • 简单说
  • 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
  • 编译:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
  • 输出:将编译后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中

4.使用webpack开发时,你用过哪些可以提高效率的插件?

(这道题还蛮注重实际,用户的体验还是要从小抓起的)

  • webpack-dashboard:可以更友好的展示相关打包信息。
  • webpack-merge:提取公共配置,减少重复配置代码
  • speed-measure-webpack-plugin:简称 SMP,分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。
  • size-plugin:监控资源体积变化,尽早发现问题
  • HotModuleReplacementPlugin:模块热替换

5.source map是什么?生产环境怎么用?

source map 是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。 map文件只要不打开开发者工具,浏览器是不会加载的。

线上环境一般有三种处理方案:

  • hidden-source-map:借助第三方错误监控平台 Sentry 使用
  • nosources-source-map:只会显示具体行数以及查看源代码的错误栈。安全性比 sourcemap 高
  • sourcemap:通过 nginx 设置将 .map 文件只对白名单开放(公司内网) 注意:避免在生产中使用 inline- 和 eval-,因为它们会增加 bundle 体积大小,并降低整体性能。

6.模块打包原理知道吗?

Webpack 实际上为每个模块创造了一个可以导出和导入的环境,本质上并没有修改 代码的执行逻辑,代码执行顺序与模块加载顺序也完全一致。

7.文件监听原理呢?

在发现源码发生变化时,自动重新构建出新的输出文件。

Webpack开启监听模式,有两种方式:

  • 启动 webpack 命令时,带上 --watch 参数
  • 在配置 webpack.config.js 中设置 watch:true 缺点:每次需要手动刷新浏览器

原理:轮询判断文件的最后编辑时间是否变化,如果某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout 后再执行。

module.export = {
    // 默认false,也就是不开启
    watch: true,
    // 只有开启监听模式时,watchOptions才有意义
    watchOptions: {
        // 默认为空,不监听的文件或者文件夹,支持正则匹配
        ignored: /node_modules/,
        // 监听到变化发生后会等300ms再去执行,默认300ms
        aggregateTimeout:300,
        // 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
        poll:1000
    }
}

8.说一下 Webpack 的热更新原理吧

(敲黑板,这道题必考)

Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS 与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。

后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。

9.如何对bundle体积进行监控和分析?

VSCode 中有一个插件 Import Cost 可以帮助我们对引入模块的大小进行实时监测,还可以使用 webpack-bundle-analyzer 生成 bundle 的模块组成图,显示所占体积。 bundlesize 工具包可以进行自动化资源体积监控。

10.文件指纹是什么?怎么用?

文件指纹是打包后输出的文件名的后缀。

  • Hash:和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改
  • Chunkhash:和 Webpack 打包的 chunk 有关,不同的 entry 会生出不同的 chunkhash
  • Contenthash:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变
JS的文件指纹设置

设置 output 的 filename,用 chunkhash。

module.exports = {
    entry: {
        app: './scr/app.js',
        search: './src/search.js'
    },
    output: {
        filename: '[name][chunkhash:8].js',
        path:__dirname + '/dist'
    }
}
CSS的文件指纹设置

设置 MiniCssExtractPlugin 的 filename,使用 contenthash。

module.exports = {
    entry: {
        app: './scr/app.js',
        search: './src/search.js'
    },
    output: {
        filename: '[name][chunkhash:8].js',
        path:__dirname + '/dist'
    },
    plugins:[
        new MiniCssExtractPlugin({
            filename: `[name][contenthash:8].css`
        })
    ]
}
图片的文件指纹设置

设置file-loader的name,使用hash。

占位符名称及含义

  • ext 资源后缀名
  • name 文件名称
  • path 文件的相对路径
  • folder 文件所在的文件夹
  • contenthash 文件的内容hash,默认是md5生成
  • hash 文件内容的hash,默认是md5生成
  • emoji 一个随机的指代文件内容的emoj
const path = require('path');
module.exports = {
    entry: './src/index.js',
    output: {
        filename:'bundle.js',
        path:path.resolve(__dirname, 'dist')
    },
    module:{
        rules:[{
            test:/\.(png|svg|jpg|gif)$/,
            use:[{
                loader:'file-loader',
                options:{
                    name:'img/[name][hash:8].[ext]'
                }
            }]
        }]
    }
}

11.在实际工程中,配置文件上百行乃是常事,如何保证各个loader按照预想方式工作?

可以使用 enforce 强制执行 loader 的作用顺序,pre 代表在所有正常 loader 之前执行,post 是所有 loader 之后执行。(inline 官方不推荐使用)

12.如何优化 Webpack 的构建速度?

  • 使用高版本的 Webpack 和 Node.js
  • 多进程/多实例构建:HappyPack(不维护了)、thread-loader
  • 压缩代码
    • 多进程并行压缩
      • webpack-paralle-uglify-plugin
      • uglifyjs-webpack-plugin 开启 parallel 参数 (不支持ES6)
      • terser-webpack-plugin 开启 parallel 参数
    • 通过 mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件,通过 css-loader 的 minimize 选项开启 cssnano 压缩 CSS。
  • 图片压缩
    • 使用基于 Node 库的 imagemin (很多定制选项、可以处理多种图片格式)
    • 配置 image-webpack-loader
  • 缩小打包作用域
    • exclude/include (确定 loader 规则范围)
    • resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找)
    • resolve.mainFields 只采用 main 字段作为入口文件描述字段 (减少搜索步骤,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段)
    • resolve.extensions 尽可能减少后缀尝试的可能性
    • noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)
    • IgnorePlugin (完全排除模块)
    • 合理使用alias
  • 提取页面公共资源
    • 基础包分离:
      • 使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中
      • 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件
  • DLL
    • 使用 DllPlugin 进行分包,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间。
    • HashedModuleIdsPlugin 可以解决模块数字id问题
  • 充分利用缓存提升二次构建速度
    • babel-loader 开启缓存
    • terser-webpack-plugin 开启缓存
    • 使用 cache-loader 或者 hard-source-webpack-plugin
  • Tree shaking
    • 打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率
    • 禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking
    • 使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码
      • purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)
  • Scope hoisting
    • 构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突
    • 必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法,为了充分发挥 Scope hoisting 的作用,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法
  • 动态Polyfill
    • 建议采用 polyfill-service 只给用户返回需要的polyfill,社区维护。 (部分国内奇葩浏览器UA可能无法识别,但可以降级返回所需全部polyfill) 13.你刚才也提到了代码分割,那代码分割的本质是什么?有什么意义呢? 代码分割的本质其实就是在源代码直接上线和打包成唯一脚本main.bundle.js这两种极端方案之间的一种更适合实际场景的中间状态。

阿卡丽:荣耀剑下取,均衡乱中求

「用可接受的服务器性能压力增加来换取更好的用户体验。」

源代码直接上线:虽然过程可控,但是http请求多,性能开销大。

打包成唯一脚本:一把梭完自己爽,服务器压力小,但是页面空白期长,用户体验不好。

14.是否写过Loader?简单描述一下编写loader的思路?

Loader 支持链式调用,所以开发上需要严格遵循“单一职责”,每个 Loader 只负责自己需要负责的事情。

  • Loader 运行在 Node.js 中,我们可以调用任意 Node.js 自带的 API 或者安装第三方模块进行调用
  • Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串,当某些场景下 Loader 处理二进制文件时,需要通过 exports.raw = true 告诉 Webpack 该 Loader 是否需要二进制数据
  • 尽可能的异步化 Loader,如果计算量很小,同步也可以
  • Loader 是无状态的,我们不应该在 Loader 中保留状态
  • 使用 loader-utils 和 schema-utils 为我们提供的实用工具
  • 加载本地 Loader 方法
    • Npm link
    • ResolveLoader

15.是否写过Plugin?简单描述一下编写Plugin的思路?

webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在特定的阶段钩入想要添加的自定义功能。Webpack 的 Tapable 事件流机制保证了插件的有序性,使得整个系统扩展性良好。

Plugin的API 可以去官网查阅

  • compiler 暴露了和 Webpack 整个生命周期相关的钩子
  • compilation 暴露了与模块和依赖有关的粒度更小的事件钩子
  • 插件需要在其原型上绑定apply方法,才能访问 compiler 实例
  • 传给每个插件的 compiler 和 compilation对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件
  • 找出合适的事件点去完成想要的功能
    • emit 事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(emit 事件是修改 Webpack 输出资源的最后时机)
    • watch-run 当依赖的文件发生变化时会触发
  • 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住

16.聊一聊Babel原理吧

大多数JavaScript Parser遵循 estree 规范,Babel 最初基于 acorn 项目(轻量级现代 JavaScript 解析器) Babel大概分为三大部分:

  • 解析:将代码转换成 AST
    • 词法分析:将代码(字符串)分割为token流,即语法单元成的数组
    • 语法分析:分析token流(上面生成的数组)并生成 AST
  • 转换:访问 AST 的节点进行变换操作生产新的 AST
    • Taro就是利用 babel 完成的小程序语法转换
  • 生成:以新的 AST 为基础生成代码 想了解如何一步一步实现一个编译器的同学可以移步 Babel 官网曾经推荐的开源项目the-super-tiny-compiler

17.Tree-shaking 原理

js手写题

css相关 浏览器相关

网络基础HTTP

对于http请求有了解过嘛?常见的状态码都有那些?

  • 200 OK 成功(最理想的状态)
  • 301 Moved Permanently 永久转移(永久重定向)
  • 302 Move temporarily 临时转移
  • 304 Not Modified 设置缓存
  • 400 Bad Request 请求参数错误
  • 401 Unauthorized 无权限访问
  • 404 Not Found 找不到资源(最不理想的状态)
  • 405 Method Not Allowed 请求行中指定的请求方法不能被用于请求相应的资源,但是该响应必须返回一个Allow头信息来表示出当前资源能够接受请求方法的列表。
  • 500 Internal Server Error 未知的服务器错误
  • 503 Service Unavailable 服务器超负荷

cookie和localSrorage、session、indexDB 的区别

特性cookielocalStoragesessionStorageindexDB
数据生命周期一般由服务器生成,可以设置过期时间除非被清理,否则一直存在页面关闭就清理除非被清理,否则一直存在
数据存储大小4K5M5M无限
与服务端通信每次都会携带在 header 中,对于请求性能影响不参与不参与不参与

从上表可以看到,cookie 已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStoragesessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储,对于 cookie,我们还需要注意安全性。

  • 存储有效期 Cookie有有效期的限制,而Storage没有,sessionStorage只在窗口关闭会消失 LocalStorage始终有效即使浏览器关闭也有,是存储在硬盘中的。存储位置:C:\Users\你的计算机名\AppData\Local\Google\Chrome\User Data\Default\Local Storage\leveldb
  • 作用域不同 sessionStorage不在不同的浏览器窗口共享,即使是同一个页面,LocalStorage在所有同源窗口中都是共享的,cookie也是在所以同源窗口共享。

cookie 可设置参数有那些

参数描述
name必需。规定 cookie 的名称。
value必需。规定 cookie 的值。
expire可选。规定 cookie 的有效期。
path可选。规定 cookie 的服务器路径。
domain可选。规定 cookie 的域名。
secure可选。规定是否通过安全的 HTTPS 连接来传输 cookie。设置1(HTTPS可用)或0(都可用)
怎么判断页面是否加载完成?

Load 事件触发代表页面中的 DOM,CSS,JS,图片已经全部加载完毕。

DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSS,JS,图片加载。

如何解决跨域

因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败。

我们可以通过以下几种常用方法解决跨域的问题

JSONP

JSONP 的原理很简单,就是利用 <script> 标签没有跨域限制的漏洞。通过 <script> 标签指向一个需要访问的地址并提供一个回调函数来接收数据当需要通讯时。

JSONP 使用简单且兼容性不错,但是只限于 get 请求。 在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP,以下是简单实现

CORS

CORS需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。 浏览器会自动进行 CORS 通信,实现CORS通信的关键是后端。只要后端实现了 CORS,就实现了跨域。 服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。

document.domain

该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。 只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域

postMessage

这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息

// 发送消息端
window.parent.postMessage('message', 'http://test.com');
// 接收消息端
var mc = new MessageChannel();
mc.addEventListener('message', (event) => {
    var origin = event.origin || event.originalEvent.origin; 
    if (origin === 'http://test.com') {
        console.log('验证通过')
    }
});

浏览器缓存

缓存对于前端性能优化来说是个很重要的点,良好的缓存策略可以降低资源的重复加载提高网页的整体加载速度。

通常浏览器缓存策略分为两种:强缓存和协商缓存。

强缓存

实现强缓存可以通过两种响应头实现:ExpiresCache-Control 。强缓存表示在缓存期间不需要请求,state code 为 200

Expires: Wed, 22 Oct 2018 08:41:00 GMT

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 并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件。

从输入URL到页面展示,这中间发生了什么?

简单版:

  1. 浏览器进程发出URL请求给网络进程

  2. 网络进程接收到URL请求之后,便发起网络请求,然后服务器返回HTTP数据到网络进程,网络进程解析HTTP出来响应头数据,并将其转发给浏览器进程

  3. 浏览器进程接收到网络进程的响应头数据之后,发送CommitNavigation消息到渲染进程,发送CommitNavigation时会携带响应头、等基本信息。

  4. 渲染进程接收到CommitNavigation消息之后,便开始准备接收HTML数据,接收数据的方式是直接和网络进程建立数据管道

  5. 最后渲染进程会像浏览器进程“确认提交”,这是告诉浏览器进程,说我已经准备好接受和解析页面数据了

  6. 最后浏览器进程更新页面状态

细致版:

  1. 用户输入url并回车
  2. 浏览器进程检查url,组装协议,构成完整的url
  3. 浏览器进程通过进程间通信(IPC)把url请求发送给网络进程
  4. 网络进程接收到url请求后检查本地缓存是否缓存了该请求资源,如果有则将该资源返回给浏览器进程
  5. 如果没有,网络进程向web服务器发起http请求(网络请求),请求流程如下:
    1. 进行DNS解析,获取服务器ip地址,端口(端口是通过dns解析获取的吗?这里有个疑问)
    2. 利用ip地址和服务器建立tcp连接
    3. 构建请求头信息
    4. 发送请求头信息
    5. 服务器响应后,网络进程接收响应头和响应信息,并解析响应内容
  6. 网络进程解析响应流程;
    1. 检查状态码,如果是301/302,则需要重定向,从Location自动中读取地址,重新进行第4步 (301/302跳转也会读取本地缓存吗?这里有个疑问),如果是200,则继续处理请求。
    2. 200响应处理: 检查响应类型Content-Type,如果是字节流类型,则将该请求提交给下载管理器,该导航流程结束,不再进行 后续的渲染,如果是html则通知浏览器进程准备渲染进程准备进行渲染。
  7. 准备渲染进程,浏览器进程检查当前url是否和之前打开的渲染进程根域名是否相同,如果相同,则复用原来的进程,如果不同,则开启新的渲染进程
  8. 传输数据、更新状态
    1. 渲染进程准备好后,浏览器向渲染进程发起“提交文档”的消息,渲染进程接收到消息和网络进程建立传输数据的“管道”
    2. 渲染进程接收完数据后,向浏览器发送“确认提交”
    3. 浏览器进程接收到确认消息后更新浏览器界面状态:安全、地址栏url、前进后退的历史状态、更新web页面。

浏览器性能问题

重绘(Repaint)和回流(Reflow)

重绘和回流是渲染步骤中的一小节,但是这两个步骤对于性能影响很大。

  • 重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘
  • 回流是布局或者几何属性需要改变就称为回流。 回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列回流。

所以以下几个动作可能会导致性能问题:

  • 改变 window 大小
  • 改变字体
  • 添加或删除样式
  • 文字改变
  • 定位或者浮动
  • 盒模型 很多人不知道的是,重绘和回流其实和 Event loop 有关。
  1. 当 Event loop 执行完 Microtasks 后,会判断 document 是否需要更新。因为浏览器是 60Hz 的刷新率,每 16ms 才会更新一次。
  2. 然后判断是否有 resize 或者 scroll ,有的话会去触发事件,所以 resize 和 scroll 事件也是至少 16ms 才会触发一次,并且自带节流功能。
  3. 判断是否触发了 media query
  4. 更新动画并且发送事件
  5. 判断是否有全屏操作事件
  6. 执行 requestAnimationFrame 回调
  7. 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好
  8. 更新界面
  9. 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调。 以上内容来自于 HTML 文档

减少重绘和回流

  • 使用 translate 替代 top
<div class="test"></div>
<style>
  .test {
    position: absolute;
    top: 10px;
    width: 100px;
    height: 100px;
    background: red;
  }
</style>
<script>
  setTimeout(() => {
        // 引起回流
    document.querySelector('.test').style.top = '100px'
  }, 1000)
</script>
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
  • 把 DOM 离线后修改,比如:先把 DOM 给 display:none (有一次 Reflow),然后你修改100次,然后再把它显示出来
  • 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量
for(let i = 0; i < 1000; i++) {
    // 获取 offsetTop 会导致回流,因为需要去获取正确的值
    console.log(document.querySelector('.test').style.offsetTop)
}
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
  • CSS 选择符从右往左匹配查找,避免 DOM 深度过深 将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video 标签,浏览器会自动将该节点变为图层。

图片优化

计算图片大小

对于一张 100 * 100 像素的图片来说,图像上有 10000 个像素点,如果每个像素的值是 RGBA 存储的话,那么也就是说每个像素有 4 个通道,每个通道 1 个字节(8 位 = 1个字节),所以该图片大小大概为 39KB(10000 * 1 * 4 / 1024)。

但是在实际项目中,一张图片可能并不需要使用那么多颜色去显示,我们可以通过减少每个像素的调色板来相应缩小图片的大小。

了解了如何计算图片大小的知识,那么对于如何优化图片,想必大家已经有 2 个思路了:

  • 减少像素点
  • 减少每个像素点能够显示的颜色

图片加载优化

  1. 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。
  2. 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
  3. 小图使用 base64 格式
  4. 将多个图标文件整合到一张图片中(雪碧图)
  5. 选择正确的图片格式:
    • 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
    • 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
    • 照片使用 JPEG

其他文件优化

  • CSS 文件放在 head
  • 服务端开启文件压缩功能
  • script 标签放在 body 底部,因为 JS 文件执行会阻塞渲染。当然也可以把 script 标签放在任意位置然后加上 defer ,表示该文件会并行下载,但是会放到 HTML 解析完成后顺序执行。对于没有任何依赖的 JS 文件可以加上 async ,表示加载和渲染后续文档元素的过程将和 JS 文件的加载与执行并行无序进行。
  • 执行 JS 代码过长会卡住渲染,对于需要很多时间计算的代码可以考虑使用 WebworkerWebworker 可以让我们另开一个线程执行脚本而不影响渲染。

CDN

静态资源尽量使用 CDN 加载,由于浏览器对于单个域名有并发请求上限,可以考虑使用多个 CDN 域名。对于 CDN 加载静态资源需要注意 CDN 域名要与主站不同,否则每次请求都会带上主站的 Cookie。

使用 Webpack 优化项目

  • 对于 Webpack4,打包项目使用 production 模式,这样会自动开启代码压缩
  • 使用 ES6 模块来开启 tree shaking,这个技术可以移除没有使用的代码
  • 优化图片,对于小图可以使用 base64 的方式写入文件中
  • 按照路由拆分代码,实现按需加载
  • 给打包出来的文件名添加哈希,实现浏览器缓存文件

webpack

优化打包速度
  • 减少文件搜索范围
    • 比如通过别名
    • loader 的 test,include & exclude
  • Webpack4 默认压缩并行
  • Happypack 并发调用
  • babel 也可以缓存编译

Babel 原理

本质就是编译器,当代码转为字符串生成 AST,对 AST 进行转变最后再生成新的代码

  • 分为三步:词法分析生成 Token,语法分析生成 AST,遍历 AST,根据插件变换相应的节点,最后把 AST 转换为代码

如何实现一个插件

  • 调用插件 apply 函数传入 compiler 对象
  • 通过 compiler 对象监听事件 比如你想实现一个编译结束退出命令的插件
class BuildEndPlugin {
  apply (compiler) {
    const afterEmit = (compilation, cb) => {
      cb()
      setTimeout(function () {
        process.exit(0)
      }, 1000)
    }
    compiler.plugin('after-emit', afterEmit)
  }
}
module.exports = BuildEndPlugin

HTTP

0.那你说一下OSI 网络分层模型是怎样分层的?

应用层、表示层、会话层、传输层、网络层、数据链路层、物理层

application layer、presentation layer、session layer、transport layer、network layer、data link layer、physical layer

1.TCP/IP 网络分层模型是怎样分层的?

应用层、传输层、网际层、链接层

application layer、transport layer、internet layer、link layer

2.TCP 和 UDP 区别?

TCP 和 UDP 都是传输层的协议,但二者有着截然不同的基因。

TCP:

  • 面向连接
  • 面向字节流
  • 有状态
  • 保证可靠交付
  • 具备拥塞控制
  • 点对点传播
  • 有序 UDP:
  • 无连接
  • 面向数据报
  • 无状态
  • 不保证可靠交付
  • 不具备拥塞控制
  • 广播、多播
  • 无序

3.TCP 的三次握手和四次挥手简单说一下

三次握手

  1. 客户端主动发起 SYN
  2. 服务端收到并返回 SYN 以及 ACK 客户端的 SYN
  3. 客户端收到服务端的 SYN 和 ACK 后,发送 ACK 的 ACK 给服务端,服务端收到后连接建立
  • Client -> SYN -> Server
  • Server -> SYN/ACK -> Client
  • Client -> ACK -> Server 四次挥手
  1. 客户端发送 FIN 给服务端
  2. 服务端收到后发送 ACK 给客户端
  3. 服务端发送 FIN 给客户端
  4. 客户端收到后,发送 ACK 的 ACK 给服务端,服务端关闭,客户端等待 2MSL 后关闭
  • Client -> FIN -> Server
  • Server -> ACK -> Client
  • Server -> FIN -> Client
  • Client -> ACK -> Server -> CLOSED
  • Client -> 2MSL 的时间 -> CLOSED

4.什么是HTTP协议?

(小白回答版)

HTTP 就是超文本传输协议呀,它的英文是 HyperText Transfer Protocol

敲黑板!

(罗剑锋老师的完美回答版)

HTTP 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范。

(面试官:理解的不错)

5.你知道哪些 HTTP 的请求方法?

  • GET 获取资源 (幂等)
  • POST 新增资源
  • HEAD 获取HEAD元数据 (幂等)
  • PUT 更新资源 (带条件时幂等)
  • DELETE 删除资源 (幂等)
  • CONNECT 建立 Tunnel 隧道
  • OPTIONS 获取服务器支持访问资源的方法 (幂等)
  • TRACE 回显服务器收到的请求,可以定位问题。(有安全风险)

6.说一下HTTP/0.9、HTTP/1.0、HTTP/1.1、HTTP/2、HTTP/3各版本之间的区别?

请移步我的另一篇专栏

7.说一下你对HTTPS的了解

HTTPS 就是在 HTTP 和 TCP 协议中间加入了 SSL/TLS 安全套接层。

结合非对称加密和对称加密的各自优点,配合证书。既保证了安全性,也保证了传输效率。

目前应用最广泛的是TLS1.2,实现原理如下:

  1. Client 发送 random1+对称加密套件列表+非对称加密套件列表
  2. Server 收到信息, 选择 对称加密套件+非对称加密套件 并和 random2+证书(公钥在证书中) 一起返回
  3. Client 验证证书有效性,并用 random1+random2 生成 pre-master 通过服务器公钥加密+浏览器确认 发送给 Server
  4. Server 收到 pre-master,根据约定的加密算法对 random1+random2+pre-master(解密)生成 master-secret,然后发送服务器确认
  5. Client 收到生成同样的 master-secert,对称加密秘钥传输完毕 TLS1.3 则简化了握手过程,完全握手只需要一个消息往返,提升了性能。不仅如此,还对部分不安全的加密算法进行了删减。

8.你所谓的约定的加密算法应该是 ECDHE 椭圆算法吧?HTTP 传输消息都是明文的,黑客完全可以作为中间人劫持消息,再利用 ECDHE 算法,这样不就将密钥*解了吗?

ECDHE 算法利用了椭圆曲线和离散对数等思想,按照当下的计算机算力,很难在短时间进行*解。且每次握手时生成的都是一对临时的公钥和私钥,这样就保证每次的密钥对也不同。 即使大费力气破解了一次的密钥,之前的历史消息也不会受到影响,保证了前向安全。 当然,TLS 协议的安全性受限于当下最快的计算机运行速度,理论上绝对安全的是量子通讯传递密钥。 (面试官:小伙子有点东西) (基操,勿6)

9.说一说你对DNS的理解?

DNS (Domain Name System)域名系统,是互联网中的重要基础设施,负责对域名的解析工作。 利用DNS协议,可以将域名解析成对应的IP地址。 可以基于UDP协议也可以基于TCP协议。

为了保证高可用、高并发和分布式,它设计成了树状的层次结构,由根DNS服务器、顶级域 DNS 服务器、二级DNS服务器和权威 DNS 服务器组成。

解析顺序是首先从浏览器缓存、操作系统缓存以及本地 DNS 缓存 (/etc/hosts) 逐级查找,然后从本地 DNS 服务器、根 DNS、顶级 DNS 、二级DNS以及权威 DNS层层递归查询。

还可以基于域名在内网、外网进行负载均衡。

不过传统的 DNS 有很多问题(解析慢、更新不及时),HTTPDNS 通过客户端 SDK 和服务端配合,直接通过 HTTP 调用解析 DNS 的方式,可以绕过传统 DNS 这些缺点,实现智能调度。

(面试官:小伙子理解的挺细啊)

10.说一说你对 CDN 的理解?

CDN(Content Delivery Network)就是内容分发网络。

为了突破现实生活中的光速、传输距离等物理限制,CDN 投入了大量资金,在全球范围内各大枢纽城市建立机房,部署大量高存储高带宽的节点,构建跨运营商、跨地域的专用高速传输网络。

其中分为中心节点、区域节点、边缘节点等,在用户接入网络后,首先通过全局负载均衡 (Global Sever Load Balance),简称 GSLB 算法负责调度,找到离用户最合适的节点。然后通过 HTTP 缓存代理技术进行缓存,缓存命中就返回给用户,否则就回源站去取。CDN 擅长缓存静态资源(图片、音频等),当然也支持动态内容的缓存。

11.说一说你对 WebSocket 的理解?

WebSocket 是一种基于 TCP 的轻量级网络通信协议。和 HTTP/2 一样,都是为了解决 HTTP 某些方面的缺陷而诞生的。不过解决方式略有不同,HTTP/2 针对的是“队头阻塞 ”,WebSocket 针对的是“请求-应答”的通信模式

我们知道“请求-应答”是半双工的通信模式,不具备服务器推送能力。这就限制了 HTTP 在实时通信领域的发展。虽然可以使用轮询来不停的向服务器发送 HTTP 请求,但是缺点也很大,反复的无效请求占用了大量的带宽和 CPU 资源。所以,WebSocket 应运而生。

WebSocket 是一个全双工通信协议,具备服务端主动推送的能力。本质上是对 TCP 做了一层包装,让它可以运行在浏览器环境里。

看过我这篇专栏的同学们一定知道,Webpack 的热更新中就利用了这种协议。当然,在即时通讯、游戏以及可视化大屏展示等领域中也都有着 WebSocket 的身影。

(关于 WebSocket 的过多细节这里不再展开,后续有机会在专栏中详细介绍,感兴趣的同学们可以先自行学习)

12.HTTP 的缓存策略知道吗?

强缓存

服务器使用 Cache-Control 来设置缓存策略,常用 max-age 来表示资源的有效期。

(这里的 max-age 的时间计算起点是响应报文的创建时刻,而不是客户端收到报文的时刻。)

(浏览器也可以发送 Cache-Control 字段,使用 max-age=0 或 no-cache 来刷新数据)

如果想更精确的控制缓存策略,还可以使用 Cache-Control 的其他属性:

  • no-store:不允许缓存 (用于秒杀页面等变化频率非常高的场景)
  • no-cache:可以缓存,使用前必须要去服务端验证是否过期,是否是最新版本
  • must-revalidate:如果缓存不过期就可以继续使用,过期了就必须去服务端验证

协商缓存

验证资源是否失效就需要使用条件请求。常用的是 If-Modified-Since 和 If-None-Match,收到 304 状态码就可以复用缓存里的资源。

(If-None-Match 比 If-Modified-Since 优先级更高)

验证资源是否被修改的条件有两个 Last-modified 和 ETag (ETag 比 Last-modified 的精确度更高),需要预先在服务端的响应报文里设置,配合条件请求使用。

13.HTTP 如何进行内容协商?

内容协商就是每个 URI 指向的资源可以是任何事物,可以有很多不同的表述。对于文档来说,可以有不同的语言、不同的媒体格式,并针对不同的浏览器提供不同的压缩编码。

  • 主动式内容协商
    • 客户端在请求头部中提出需要的表述形式,服务器根据其来进行特定表述
  • 响应式内容协商
    • 服务端返回 300 或者 406,由客户端选择一种表述

协商要素

  • 质量因子q:内容的质量、可接受类型的优先级
  • 媒体资源的 MIME 类型
  • 字符编码 (UTF-8)
  • 内容编码 (Accept-Encoding:gzip,deflate,br)
  • 表述语言 (Accept-Language:zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7)
  • 国际化与本地化 (i18n,l10n)

14.说一说 HTTP 的重定向

重定向是服务器发起的跳转,要求客户端使用新的 URI 重新发送请求。在响应头字段 Location 中指示了要跳转的 URI。使用 Refresh 字段,还可以实现延时重定向。 301 / 302 是常用的重定向状态码。分别代表永久性重定向临时性重定向。 除此之外还有:

  • 303:类似于 302,重定向后的请求方法改为 GET 方法
  • 307:类似于 302,含义比 302 更明确,重定向后请求的方法和实体不允许变动
  • 308:类似于 301,代表永久重定向,重定向后请求的方法和实体不允许变动
  • 300:是一个特殊的重定向状态码,会返回一个有多个链接选项的页面,由用户自行选择
  • 304:是一个特殊的重定向状态码,服务端验证过期缓存有效后,要求客户端使用该缓存

15.你知道哪些 HTTP 的常用的首部字段?

(上文中提到过一些,这里只列举一些常用的)

(开始报菜名)

通用首部字段

  • Cache-Control 控制缓存
  • Connection 连接管理
  • Transfor-Encoding 报文主体的传输编码格式
  • Date 创建报文的时间
  • Upgrade 升级为其他协议

请求首部字段

  • Host 请求资源所在的服务器 (唯一一个HTTP/1.1规范里要求必须出现的字段)
  • Accept 客户端或者代理能够处理的媒体类型
  • If-Match 比较实体标记 (ETag)
  • If-None-Match 比较实体标记 (ETag),与 If-Match 相反
  • If-Modified-Since 比较资源更新时间 (Last-Modified)
  • If-Unmodified-Since 比较资源更新时间 (Last-Modified), 与 If-Modified-Since 相反
  • Range 实体的字节范围请求
  • User-Agent 客户端信息

响应首部字段

  • Accept-Ranges 能接受的字节范围
  • Location 命令客户端重定向的 URI
  • ETag 能够表示资源唯一资源的字符串
  • Server 服务器的信息

实体首部字段

  • Allow 资源可支持 HTTP 请求方法
  • Last-Modified 资源最后修改时间
  • Expires 实体主体过期时间
  • Content-Language 实体资源语言
  • Content-Encoding 实体编码格式
  • Content-Length 实体大小
  • Content-Type 实体媒体类型

16.你知道哪些 HTTP 状态码?

(上文中提到过一些,这里只列举一些常用的)

(开始报菜名)

1xx

  • 1xx:请求已经接收到,需要进一步处理才能完成,HTTP/1.0 不支持
  • 100 Continue:上传大文件前使用
  • 101 Switch Protocols:协议升级使用
  • 102 Processing:服务器已经收到并正在处理请求,但无响应可用

2xx

  • 2xx:成功处理请求
  • 200 OK:成功返回响应
  • 201 Created:有新资源在服务器端被成功创建
  • 202 Accepted:服务器接受并开始处理请求,但请求未处理完成
  • 206 Partial Content:使用range协议时返回部分响应内容时的响应码

3xx

请查阅上文重定向部分,这里不再赘述。

4xx

  • 4xx:客户端出现错误
  • 400 Bad Request:服务器认为客户端出现了错误,但不明确,一般是 HTTP 请求格式错误
  • 401 Unauthorized:用户认证信息确实或者不正确
  • 403 Forbidden:服务器理解请求的含义,但没有权限执行
  • 407 Proxy Authentication Required:对需要经由代理的请求,认证信息未通过代理服务器的验证
  • 404 Not Found:服务器没有找到对应的资源
  • 408 Request Timeout:服务器接收请求超时

5xx

  • 5xx:服务器端出现错误
  • 500 Internal Server Error:服务器内部错误,且不属于以下错误类型
  • 502 Bad Gateway:代理服务器无法获取到合法响应
  • 503 Service Unavailable:服务器资源尚未准备好处理当前请求
  • 505 HTTP Version Not Supported:请求使用的 HTTP 协议版本不支持

其他

1. 前端的异常处理有做过嘛,如何优雅处理前端异常?

  • JS 语法错误、代码异常
  • AJAX 请求异常
  • 静态资源加载异常
  • Promise 异常
  • Iframe 异常
  • 跨域 Script error
  • 崩溃和卡顿
  • 可疑区域增加 Try-Catch
  • 全局监控 JS 异常 window.onerror
  • 全局监控静态资源异常 window.addEventListener
  • 捕获没有 Catch 的 Promise 异常:unhandledrejection
  • VUE errorHandler 和 React componentDidCatch
  • 监控网页崩溃:window 对象的 load 和 beforeunload
  • 跨域 crossOrigin 解决

2. 如果页面卡顿,你觉得可能是什么原因造成的?有什么办法锁定原因并解决吗?(如何排查内存泄漏)

3. 前端如何对异常进行监听

  • 可疑区域增加 Try-Catch
  • 全局监控 JS 异常 window.onerror
  • 全局监控静态资源异常 window.addEventListener
  • 捕获没有 CatchPromise 异常:unhandledrejection
  • VUE errorHandlerReact componentDidCatch
  • 监控网页崩溃:window 对象的 load 和 beforeunload
  • 跨域 crossOrigin 解决