# 常见面试题

# 说说你对 SPA 单页面的理解,优缺点是什么?

SPA仅在页面初始化加载相应的 HTML,JS 和 CSS 的。SPA 不会因为用户操作重新加载页面或者跳转,而是利用路由机制实现 HTML 的改变和 UI 和用户交互,避免页面重新加载。

优点

  • 用户体验好:内容改变并不会重新加载整个页面,避免了不必要的跳转和重复渲染

  • 前后端职责分离,架构清晰

  • 服务器压力会小很多

缺点

  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;

  • 前进后退路由管理:页面切换需要自己建立栈管理

  • SEO 难度大:所有的内容都在一个页面动态替换展示,有着天然弱势

# 聊一下 MVVM

img

MVVMModel-View-ViewModel 缩写,也就是把 MVC 中的 Controller 演变成ViewModel。Model 层代表数据模型,View 代表 UI 组件,ViewModel 是View和Model层的桥梁,数据会绑定到 viewModel 层并自动将数据渲染到页面中,视图变化的时候会通知 viewModel 层更新数据

# 虚拟 Dom 的实现原理

https://juejin.cn/post/6844903895467032589#heading-1

三个过程

  • 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;

  • diff 算法 — 比较两棵虚拟 DOM 树的差异;

  • pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。

# 虚拟 Dom 的优缺点

优点:

  • 性能较直接操作 dom 好的多,操作 dom 会涉及到 JS 引擎线程和渲染线程的互斥关系,会比较消耗性能,在保证不需要手动优化的情况下依然可以提供不错的性能

  • 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;

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

缺点:

  • 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化

# new Vue 发生了什么

new Vue 其实就是在执行函数,传入options 调用 _init 函数

function Vue(options) {
  if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
    warn("Vue is a constructor and should be called with the `new` keyword");
  }
  this._init(options);
}

Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。

Vue.prototype._init = function (options?: Object) {
......
  vm._self = vm
  // 核心
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  initState(vm)
  initProvide(vm) // resolve provide after data/props
  callHook(vm, 'created')
}

# Vue 模版编译原理知道吗,能简单说一下吗?

简单说,Vue 的编译过程就是将template 转化为 render 函数的过程。会经历以下阶段:

  • 生成 AST 树:首先解析模版,生成 AST 语法树(一种用 JavaScript 对象的形式来描述整个模板)。使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。

  • 优化:Vue 的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的 DOM 也不会变化。那么优化过程就是深度遍历 AST 树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用。

  • codegen:将优化后的 AST 树转换为可执行的代码

# Vue 的 diff 算法

https://juejin.cn/post/7010594233253888013此处需要重新整理

要知道渲染真实 DOM 的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实 dom 上会引起整个 dom 树的重绘和回流,有没有可能我们只更新我们修改的那一小块 dom而不要更新整个 dom 呢?diff 算法能够帮助我们。

diff 算法从刚开始的 O(n^3)优化到 O(n)是怎么做到的呢?

diff 算法遵循以下三大原则:

  • 只比较同一层级,不跨级比较

  • tag 不相同直接删除重建,不再深度比较

  • tag 和 key 相同则视为相同的节点,不再进行比较

img

正常 Diff 两个树的时间复杂度是O(n^3),但实际情况下我们很少会进行跨层级的移动 DOM,所以 Vue 将 Diff 进行了优化,从O(n^3) -> O(n),只有当新旧 children 都为多个子节点时才需要用核心的 Diff 算法进行同层级比较。

Vue2 的核心 Diff 算法采用了双端比较的算法,同时从新旧 children 的两端开始进行比较,借助 key 值找到可复用的节点,再进行相关操作。相比 React 的 Diff 算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。

# Vue 中的 Key 有什么作用

https://juejin.cn/post/7010594233253888013#heading-3

Vue 中 key 的作用是:key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速

key 为什么不能等于 index ?

key 为 index 会导致 diff 的重复渲染,重复渲染会导致重新更新响应值,重新触发 dep.notify触发子组件视图重新渲染等逻辑,还会额外的触发以下几个钩子,假设我们的组件上定义了一些dom 的属性或者类名、样式、指令,那么都会被全量的更新

  • key 的作用主要是为了更高效的更新虚拟 DOM,因为它可以非常精确的找到相同节点,因此 patch 过程会非常高效

  • Vue 在 patch 过程中会判断两个节点是不是相同节点时,key 是一个必要条件。比如渲染列表时,如果不写 key,Vue 在比较的时候,就可能会导致频繁更新元素,使整个 patch 过程比较低效,影响性能

  • Vue 判断两个节点是否相同时主要判断两者的元素类型和 key 等,如果不设置 key,就可能永远认为这两个是相同节点,只能去做更新操作,就造成大量不必要的 DOM 更新操作,明显是不可取的

# Vue mixin 混入策略

  • data 混入策略:两个 data 函数合并成一个函数返回,data 函数执行返回的数据对象也进行合并。

优先级:组件 data > 组件 mixin data > 组件 mixin -mixin data > ... > 全局 mixin data

  • 生命周期钩子混入策略:就是把所有的钩子函数保存进数组,虽然顺序执行

优先级:

[
全局 mixin hook,
... ,
组件 mixin-mixin hook,
组件 mixin hook,
组件 hook
],
  • watch 混入策略:和生命周期一致

优先级:

[
全局 mixin watch,
... ,
组件 mixin-mixin watch,
组件 mixin watch,
组件 watch
]
  • methods 混入策略:简单的对象合并,key 值相同,优先级高的覆盖优先级低的。组件 对象 > 组件 mixin 对象 > 组件 mixin -mixin 对象 > ... > 全局 mixin 对象。

# v-show 和 v-if 区别

v-if 是真正的条件渲染,确保切换过程中条件块内的时间监听器和子组件适当销毁和重建。但是也是惰性的,条件为真才开始渲染,为假什么都不做

v-show 只是简单的 CSS 的 display 的属性进行切换

所以 v-show 适用于非常频繁切换条件的场景

# v-model 的原理

vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件

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

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

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

<input v-model="something" />

相当于

<input :value="something" @input="something = $event.target.value" />

# Class 和 Style 如何动态绑定

  • 对象语法:
<div v-bind:class="{ currentIndex = index ? 'current' : 'notCurrent' }"></div>
  • 数组语法
<div v-bind:class="[isActive ? 'active' : '', errorClass]"></div>

# computed 和 watch 的区别和运用场景

computed:计算属性,依赖属性值,具有缓存,只有它依赖的属性值(就是 data 里的值)改变,才会重新计算 computed 的值,简称别人变化影响我自己。所以是一份数据受多份数据影响

computed 和 watch 公用一个 Watcher 类,在 computed 的情况下有一个 dep 收集依赖,从而达到更新 computed 属性的效果

适用场景: 一个数据受多个数据影响

watch:更多的是监听作用,数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作;immediate: true,立即调用。deep:true 深度监听

适用场景:数据变化时执行异步或开销较大的操作时(比如访问一个耗时久的 API,还要进行异步操作,如果不使用监听器第一时间是拿不到结果的),保证最终获得的结果是正确的而不是 undefined;一个数据影响多个数据

# watch 的属性使用箭头函数定义可以吗?

不可以,箭头函数的this指向定义时的 this,不会指向 Vue 实例的上下文。

# 谈谈你对生命周期的理解

img

  • beforeCreate: new Vue()之后执行的第一个钩子,这个期间**methods**,**computed**,**data**数据都无法获取

  • created: 实例创建完成后,完成数据侦测,虽然可以获取数据,但是无法完成**dom**交互

  • beforeMount: 发生在挂载之前,当前阶段虚拟**dom**已经创建完成,如果更改数据不会触发**updated**

  • mounted: 挂载结束,已完成双向绑定,可以访问到 DOM 节点,可以使用**$refs**操作**dom**

  • beforeUpdate: 响应式数据更新之前,虚拟**dom**重新渲染之前

  • updated: 发生在更新完成之后,当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。

  • beforeDestory: 发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器

  • destoryed: 发生在实例销毁之后,这个时候只剩下了 dom 空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁

# Vue 的父组件和子组件生命周期钩子函数执行顺序?

Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:

加载渲染过程 父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

子组件更新过程 父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

父组件更新过程 父 beforeUpdate -> 父 updated

销毁过程 父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

# 在哪个生命周期内调用异步请求?

接口请求一般放在 mounted 中,但需要注意的是服务端渲染时不支持 mounted,需要放到 created 中。

# 如何让父组件监听到子组件的生命周期钩子

@hook:生命周期名 = "doSomething"
//  Parent.vue
<Child @hook:mounted="doSomething" ></Child>

doSomething() {
   console.log('父组件监听到 mounted 钩子函数 ...');
},

//  Child.vue
mounted(){
   console.log('子组件触发 mounted 钩子函数 ...');
},

// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...

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

因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。

# 怎样理解 Vue 的单向数据流?

每当父级的 prop 改变时都会向下流向子级,但是反过来不行(每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值),这样会防止子组件随意改变父级组件状态。子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。

# Vue 是如何实现数据双向绑定的?

Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据

img

Vue 主要通过以下 4 个步骤来实现数据双向绑定的:

  1. 实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 settergetter。这样的话,给这个对象的某个属性赋值,就会触发 setter,那么就能监听到了数据变化。
function observer(target) {
  if (typeof target !== object || target == null) {
    return target;
  }
  for (let key in target) {
    defineReactive(target, key, target[key]);
  }
}

function defineReactive(data, key, value) {
  // value可能是复杂对象 递归监听
  observer(value);
  Object.definePropetry(data, key, {
    get: function() {
      return value;
    },
    set: function(newVal) {
      // 深度监听
      observer(value);
      if (newVal === value) {
        return;
      }
      value = newVal;
    }
  });
}

observer({
  name: "tom",
  parent: {
    children: {
      val: 1
    }
  }
});

defineProperty 缺点

  • 深度监听,一次性递归到底,计算量大

  • 无法监听新增删除,必须使用Vue.set或者Vue.delete

  • 无法实现数组监听,必须要改造

  1. 实现一个解析器 Compile:解析 Vue 模板指令,并将每个指令对应的节点绑定更新函数,它会添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。

  2. 实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数

  3. 实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理

# Object.defineProperty 和 Proxy 的区别

Object.defineProperty

function observer(target) {
  if (typeof target !== object || target == null) {
    return target;
  }
  for (let key in target) {
    defineReactive(target, key, target[key]);
  }
}

function defineReactive(data, key, value) {
  // value可能是复杂对象 递归监听
  observer(value);
  Object.definePropetry(data, key, {
    get: function() {
      return value;
    },
    set: function(newVal) {
      // 深度监听
      observer(value);
      if (newVal === value) {
        return;
      }
      value = newVal;
    }
  });
}
  • 不能监听到数组 length 属性的变化;

  • 不能监听对象的添加;

  • 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题

Proxy

img

  • 可以监听数组 length 属性的变化;

  • 可以监听对象的添加;

  • 可代理整个对象,不需要对对象进行遍历,极大提高性能;

  • 多达 13 种的拦截远超 Object.defineProperty 只有 get 和 set 两种拦截。

# 直接给一个数组项赋值,Vue 能监听到变化吗?

Vue 不能检测到以下数组的变动

  • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue,Vue 中解决方法: Vue.set(vm.items, indexOfItem, newValue)或者 vm.$set(vm.items, indexOfItem, newValue)
  • 当你修改数组的长度时,例如:vm.items.length = newLength ,解决方法: vm.items.splice(newLength)

# vue2.x 中如何监测数组变化

使用了函数劫持的方式,重写了数组的方法,Vue 将 data 中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组 api 时,可以通知依赖更新。

const oldArrayPrototype = Array.prototype;
// 将ArraayObject原型指向他,然后扩展方法 即继承数组方法,不影响定义新方法
const newArrayObject = Object.create(oldArrayPrototype)[
  // 扩展常用方法
  ("push", "shift", "unshift", "pop")
].forEach(methodName => {
  // 扩展的方法
  newArrayObject[methodName] = function() {
    console.log("视图更新");
    // 真正意义上的调用原生方法
    oldArrayPrototype[methodName].call(this, ...arguments);
  };
});

function observer(target) {
  if (typeof target !== object || target == null) {
    // 非对象或数组
    return target;
  }
  if (Array.isArray(target)) {
    // 使用的时候将原型指向改造之后的arrayob
    if (target instanceof Array) {
      target.__proto__ = newArrayObject;
    }
  }
  for (let key in target) {
    defineReactive(target, key, target[key]);
  }
}

function defineReactive(data, key, value) {
  // value可能是复杂对象 递归监听
  observer(value);
  Object.definePropetry(data, key, {
    get: function() {
      return value;
    },
    set: function(newVal) {
      // 深度监听
      observer(value);
      if (newVal === value) {
        return;
      }
      value = newVal;
    }
  });
}

const data = {
  nums: [1, 2]
};
observer(data);
data.nums.push(3); // 视图更新

# Vue 怎么用 vm.$set() 解决对象新增属性不能响应的问题 ?

  1. 如果是数组,调用 splice 方法触发响应式
target.splice(key, 1, val);
  1. 如果是对象,首先判断属性是否存在,然后判断对象是否是响应式的对象,如果不是,对属性通过 defineReactive 给属性动态添加 getttersetter 实现响应式

# Vue 组件间通信有哪几种方式?

父子组件通信:

  1. props$emit

  2. ref$parent / $children

  3. EventBus :这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。

  4. Vuex

隔代组件通信:

  1. provide / inject,祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量,主动提供依赖注入

  2. $attrs / $listenersimgattrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。

  3. Vuex

  4. EventBus

通用(父子,隔代,兄弟):

  1. Vuex
  2. EventBus

# 使用过 Vue SSR 吗?说说 SSR?

SSR 大致的意思就是vue 在客户端将标签渲染成的整个 html 片段的工作在服务端完成服务端形成的 html 片段直接返回给客户端这个过程就叫做服务端渲染。

优点:

  • 更好的 SEO: SPA 页面内容是通过 AJAX 异步获取内容的,但是搜索引擎是不会等待异步完成之后再抓取页面内容的。而 SSR 是直接通过服务端返回渲染好的页面,所以更利于搜索引擎抓取内容
  • 首屏加载时间更快: SPA 会等待所有 Vue 编译后的文件下载完才开始页面的渲染的,需要点时间。而 SSR 直接去显示有服务端提供好的渲染好的页面内容即可

缺点:

  • 更多开发条件限制: 例如服务端渲染只支持 beforCreatecreated 两个钩子函数;服务端渲染应用程序需要处于 Node.js Server 环境,跟 SPA(部署任何静态文件服务器都行)截然不同
  • 更多的服务器负载 渲染完整的应用程序会占用大量的 CPU 资源

# Vue-Router 的三种路由模式,实现原理和优缺点

https://developer.mozilla.org/zh-CN/docs/Web/API/History_API

  • hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器;

    • 原理: 基于浏览器的 **hashchange** 事件,通过 window.location.hash 获取地址的 hash 值,然后通过构造 Router类获取配置的 hash值对应的组件内容
    • 优点: 1、hash 值并不会包含在请求中,hash 值改变不会重新加载页面 2、hash 值改变触发hashchange事件能够控制浏览器的前进后退 3、兼容性好
    • 缺点:1、不美观 2、hash 有体积限制 3、设置的新 hash 值和原来不一样才能触发hashchange事件,并将记录添加到栈中 4、每次url改变不属于一次http请求,不利于SEO
  • history : 依赖 HTML5 History API 和服务器配置。

    • 原理: 基于 HTML5 新增的 pushState()replaceState() 两个 api,这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,以及浏览器的 popstate事件(浏览器的进后退能触发浏览器的 popstate 事件),地址变化时,通过 window.location.pathname 找到对应的组件。并通过构造 Router 类,配置 routes 对象设置 pathname 值与对应的组件内容。
    • 优点: 1、美观 2、pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中 3、可以设置额外的 title 属性
    • 缺点: 1、兼容性差 2、需要后端配合,URL 的改变属于 http 请求,借助 history.pushState 实现页面的无刷新跳转,因此会重新请求服务器。所以前端的 URL 必须和实际向后端发起请求的 URL 一致,否则就会 404
  • abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.

    • 原理: 支持所有javascript运行模式。vue-router 自身会对环境做校验,如果发现没有浏览器的 API,路由会自动强制进入 abstract 模式。在移动端原生环境中也是使用 abstract 模式。

# 谈谈你对 keep-alive 的了解?

keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:

  • 一般结合路由和动态组件一起使用,用于缓存组件;

  • 提供 includeexclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;

  • 对应两个钩子函数 activateddeactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。

# 怎么缓存当前打开的路由组件,缓存后想更新当前组件怎么办呢?

<keep-alive></keep-alive>内置组件包裹路由组件,在钩子函数 activated 中更新。

# Vuex 是什么

Vuex 是一个专为 Vue.js 应用程序开发的状态管理插件。它采用集中式存储管理应用的所有组件的状态,而更改状态的唯一方法是提交 mutation,例this.$store.commit('SET_VIDEO_PAUSE', video_pause),SET_VIDEO_PAUSE 为 mutations 属性中定义的方法

# Vuex 原理

img

Vuex 所有的数据操作必须通过 action -> mutation -> state(响应式数据) 的流程来进行,再结合 Vue 的数据视图双向绑定特性来实现页面的展示更新。

  1. Vue Components(组件),在组件中 $store**.**dispatch**(**'对应的 action 回调名'**)** 触发 Actions 来响应组件中的动作;

  2. 在 Actions 使用commit**(**'对应的 mutations 方法名'**)** 触发 mutations;

  3. mutations 内部操作数据 State,之后 Vue 内部将操作后的数据 render 渲染到页面上

# Vuex 解决了什么问题?

  • 多组件依赖同一个状态,跨级组件,对于多层嵌套的组件的传参将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。
  • 来自不同组件的行为需要变更同一状态。

# Vuex 中 action 和 mutation 有什么区别?

  • action 提交的是 mutation,而不是直接变更状态。mutation 可以直接变更状态。

  • action 可以包含任意异步操作。mutation 只能是同步操作。

  • 提交方式不同,action 是用this.$store.dispatch('ACTION_NAME',data)来提交。mutation 是用this.$store.commit('SET_NUMBER',10)来提交。

  • 接收参数不同,mutation 第一个参数是 state,而 action 第一个参数是 context

# 为什么 Vuex 的 mutation 中不能做异步操作?

  • Vuex 中所有的状态更新的唯一途径都是 mutation,异步操作通过 Action 来提交 mutation 实现,这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用
  • 每个 mutation 执行完成后都会对应到一个新的状态变更,这样 devtools 就可以打个快照存下来。如果 mutation 支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。

# 在 Vuex 插件中怎么监听组件中提交 mutation 和 action?

  • 用 Vuex.Store 的实例方法 subscribe 监听组件中提交 mutation
  • 用 Vuex.Store 的实例方法 subscribeAction 监听组件中提交 action
export default function createPlugin(param) {
  return store => {
    store.subscribe((mutation, state) => {
      console.log(mutation.type); //是那个mutation
      console.log(mutation.payload);
      console.log(state);
    });
    store.subscribeAction({
      before: (action, state) => {
        //提交action之前
        console.log(`before action ${action.type}`);
      },
      after: (action, state) => {
        //提交action之后
        console.log(`after action ${action.type}`);
      }
    });
  };
}

# Vue 和 React 区别和优缺点

原理上说

Vue 的数据绑定依赖于数据劫持的 Object.definePropetry()gettersetter ,更新视图使用的是发布订阅模式来监听值的变化,从而让虚拟 dom 驱动 Model 和 View 层的更新,利用 v-model 这一个语法糖轻易实现双向数据绑定

React 需要通过 onChangesetState模式来实现数据的双向数据绑定,因为它一开始就是被设计为单向数据流的

数据流的不同

Vue: 组件和 dom 之间是双向绑定的

React: 单向数据流,采用的是 onChange/setState() 模式

Hoc 和 mixins

Vue 中组合不同功能的方式是混入 mixin,react 是使用高阶组件

组件通信方式区别

Vue:

  • 父向子: props

  • 子向父级: $emit

  • 隔代: provide/inject

React:

  • 父向子: props

  • 子向父: props 定义好修改 state 数据的函数,子调用即可

  • 隔代: React 可以通过React.context 来进行跨层级通信

模板渲染方式

  • React 是在组件 JSX 代码中,通过原生 JS 实现模板中的常见语法,比如插值,条件,循环等,都是通过 JS 语法实现的
  • Vue 是在和组件 JS 代码分离的单独的模板中,通过指令来实现的,比如条件语句就需要 v-if 来实现

性能差异

  • 在 React 中组件的更新渲染是从数据发生变化的根组件开始往子组件逐层渲染,而组件的生命周期中有 shouldComponentUpdate 这一钩子函数可以给开发者优化组件在不需要更新的时候不要更新。
  • Vue 通过 watcher 监听到数据的变化之后,通过自己的 diff 算法,在 virtualDOM 中直接以最低成本更新视图

# Vue 性能优化有哪些

  1. 代码层面优化
  • v-if 和 v-show 区分使用场景

  • computed 和 watch 区分使用场景

  • v-for 遍历必须为 item 添加 key,且避免同时使用 v-if

  • 长列表性能优化

  • 事件的销毁

  • 图片资源懒加载

  • 路由懒加载

  • 第三方插件的按需引入

  • 服务端渲染 SSR or 预渲染

  1. Webpack 层的优化
  • Webpack 对图片进行压缩

  • 减少 ES6 转为 ES5 的冗余代码

  • 提取公共代码

  • 模板预编译

  • 提取组件的 CSS

  1. 基于 web 技术的优化
  • 开启 gzip 压缩

  • 浏览器缓存

  • CDN 的使用

# 在 Vue 事件中传入imgevent.target$event.currentTarget` 有什么区别?

$event.currentTarget 始终指向事件所绑定的元素,而$event.target 指向事件触发的元素。

# 写出你知道的表单修饰符和事件修饰符

事件修饰符

  • .stop:阻止事件传递;

  • .prevent: 阻止默认事件;

  • .capture :在捕获的过程监听,没有 capture 修饰符时都是默认冒泡过程监听;

  • .self:当前绑定事件的元素才能触发;

  • .once:事件只会触发一次;

  • .passive:默认事件会立即触发,不要把.passive 和.prevent 一起使用,因为.prevent 将不起作用。

表单修饰符

.number .lazy .trim

# Vue 如何监听键盘事件?

使用按键修饰符 <input @keyup.enter="submit"> 按下回车键时候触发 submit 事件。

  • .enter

  • .tab

  • .delete (捕获“删除”和“退格”键)

  • .esc

  • .space

  • .up

  • .down

  • .left

  • .right

# v-once 使用场景

其作用是只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。故当组件中有大量的静态的内容可以使用这个指令

# 你知道 style 上加 scoped 属性的原理吗?

vue 通过在 DOM 结构以及 css 样式上加上唯一的标记**data-v-xxxxxx**,保证唯一,达到样式私有化,不污染全局的作用。

# nextTick 知道吗,实现原理是什么?

原文解析 (opens new window) dom更新结束后执行的延时回调函数。主要使用了宏任务和微任务。

根据执行环境分别尝试采用

  • Promise

  • MutationObserver

  • setImmediate

  • 如果以上都不行则采用 setTimeout

概括来说:当数据更新了,在 dom 中渲染后, 自动执行该函数。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,Vue 是异步执行 DOM 更新的。有一个 timerFunc 这个函数用来执行 callbacks 里存储的所有回调函数,该方法依次尝试使用PromiseMutationObserversetImmediate,都不支持的话,就会降级为setTimeout。通过事件队列的机制,优先使用微任务,不支持再使用宏任务,来实现在当前主 js 执行完后,再异步去执行nextTick中的代码逻辑

使用场景:

  1. created 中操作 dom

  2. 对更新的 dom 进行一系列操作时

  3. 使用插件时,希望 dom 重新应用插件(比如数据变化后,使用的 better-scroll 插件需要在 nextTick 中调用 better-scroll 的刷新 api)

# Vue 根绝角色分配路由实现原理以及拦截逻辑伪代码

使用 vuex 和 addRoute 实现

router.beforeEach((to,from,next)=>{
  // 本地判断是否有token
  const hasToken = getToken()
  if(hasToken){
    // 判断当前页是否是登录页面
    if(to.path === '/login'){
      // 跳转至首页
      next({path:'/'})
    }else{
      // 不是登录页,判断当前Vuex是否保存了角色信息
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      // 如果有用户角色信息,可直接访问
      if(hasRoles){
        next()
      }else{
        try{
          // 调用获取用户信息action,然后将新生村的动态路由添加进入全局路由表
        const { roles } = await store.dispatch('user/getInfo')
        const accessRoutes = await store.dispatch('permisoon',roles)
        router.addRoutes(accessRoute)
        // replace方式访问路由
        next({...to,replace:true})
        }catch (error){
          next({path:'/login'})
        }
      }
    }
  }else{
    if(whilePathList.includes(to.path)){
      next()
    }else{
      next(`/login?redirect=${to.path}`)
    }
  }
})

其中动态路由生成的代码,其实就是通过过滤提前定义好的一章 asyncRoutes表,取出里面 meta.roles 包含当前角色 role 的路由表

具体来说:

  1. 首先拿到了角色信息之后,定义一个获取权限路由 action,传入 roles

  2. 如果当前角色为管理员则全部放行,如果不是的话去过滤出具有权限的路由。传入提前在路由表定义的权限路由,然后通过 meta.roles 提前设置的 roles 判断当前角色是否被包含在内,然后取出之中符合条件的路由表,这一步也使用到了递归

  3. 最后同步保存在 Vuex 之中,通过 addRoutes 添加路由表完成封装

const actions = {
  generatorRoute({commit},roles){
    return new Promise(resolve=>{
      let accessRoutes
      // 管理员全部放行
      if(roles.includes('admin')){
        accessRoutes = asyncRoutes || []
      }else{
        accessRoutes = filterAsyncRoutes(asyncRoutes,roles)
        commit('SET_ROUTES',accessRoutes)
        resolve(accessRoutes)
      }
    })
  }
}

filterAsyncRoutes(routes,roles){
  const res = []
  routes.forEach(route=>{
    const tmp = {...route}
    // 如果该条路由具有权限
    if(hasPermission(roles,tmp)){
      // 如果children,递归过滤
      if(tmp.children){
        tmp.children = filterAsyncRoutes(tmp.children,roles)
      }
      res.push(tmp)
    }
  })
}

hasPermission(roles,route){
  if(route.meta && route.meta.roles){
    return roles.some(role => route.meta.roles.includes(role))
  }else{
    return true
  }
}

# Vue 自定义指令如何实现,举例说明

https://v3.cn.vuejs.org/guide/custom-directive.html

Vue 使用自定义指令实现按钮级别的权限控制,需要设置两个值,一个是该按钮需要的权限,一个是当前用户用户角色

Vue3 指令钩子函数

img

  1. 首先定义 Vue 组件,这里可以使用 Vuex 来保存角色信息
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state: { userInfo: { name:
'Suk', roles: ['add', 'delete'] // 管理员有哪些权限 } }, mutations: {}, actions: {}, getters: {}, }) export default
store
  1. 编写 directives 实现自定义指令
directives: {
  // 指令名
  'permission':{
    //dom被插入元素时执行的钩子,el获取dom,binding.value拿到指令绑定的值,vnode.context可以拿到实例
    inserted:(el,binding,vnode)=>{
      // 获取保定的值
      const userRoles = bing.value
      // 获取按钮需要的权限
      const btnRole = el.getAttribute('data-rule')
      // 判断是否该角色是否有权限,无权限移除元素
      if(!userRoles.includes(btnRole)){
        el.parentNode.removeChild(el)
      }
    }
  }
}

export const permission: ObjectDirective = {
  mounted(el: HTMLButtonElement, binding) {
    if (binding.value == undefined) return;
    const { action, effect } = binding.value;
    const { hasPermission } = usePermission();
    if (!hasPermission(action)) {
      if (effect == "disabled") {
        el.disabled = true;
        el.style["disabled"] = "disabled";
        el.classList.add("n-button--disabled");
      } else {
        el.remove();
      }
    }
  }
};
  1. 应用指令
<template>
  <div class="test">
    {{ userInfo.name }}拥有的按钮权限:
    <el-button data-rule="add" v-permission="userInfo.roles">新增</el-button>
    <el-button data-rule="delete" v-permission="userInfo.roles">删除</el-button>
    <el-button data-rule="update" v-permission="userInfo.roles">修改</el-button>
  </div>
</template>
@param seffect 'disabled' 禁用按钮 不传 默认移除按钮

<n-button
  v-permissions="{ action: ['RoleEnum.ADMIN, RoleEnum.NORMAL'], effect: 'disabled' }"
  type="primary"
  class="mx-4"
> 拥有admin,normal角色权限可见</n-button>

# vue 缓存页面并保留页面当前位置

这里可以借助 vue 提供的 keep-alive 组件来缓存组件。

第一种方法: 利用 scrollBehavior

  1. 首先在需要缓存的路由文件定义 meta 信息,比如列表页
{
    path: '/news ',
    name: 'news ',
    component: resolve => require(['@/view/news'], resolve),
    meta: {
        keepAlive: true  // 通过此字段判断是否需要缓存当前组件
    }
},

  1. 使用 keep-alive 包住 router-view
<keep-alive>      
    <router-view v-if="$route.meta.keepAlive"/>    
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" />
  1. 借助 router 提供的钩子 - scrollscrollBehavior 实现滚动行为
scrollBehavior(to,from,savePosition){
  // 如果meta上一次保存了savePosition,直接返回上一次位置
  if(savePosition){
    return savePosition
  }else{
    // 需要缓存
    if(from.meta.keepAlive){
      from.meta.savePosition = document.body.scrollTop
    }
    // 新打开页面如果meta.savepositon有值,则滚动到相应位置
    return {x:0,y: to.meta.savePosition || 0}
  }
}

第二种方法: 使用 better-scroll

  1. 使用 keep-alive 包住 router-view 缓存组件
  2. 在组件的 activated 钩子中滚动到上一次位置,deactivated 钩子保存当前的位置
activated(){
  this.$refs.scroll.scrollTo(0,this.saveY.0)
  this.$refs.scroll.refresh()
},
deactivated(){
  this.saveY = this.$refs.scroll.y // 保存位置
}

# Vue3

https://www.zhihu.com/people/evanyou/answers

# Vue3 有哪些新特性

  • 新增了三个组件:Fragment 支持多个根节点、Suspense 可以在组件渲染之前的等待时间显示指定内容、Teleport 可以让子组件能够在视觉上跳出父组件(如父组件 overflow:hidden)

  • 新增指令 v-memo,可以缓存 html 模板,比如 v-for 列表不会变化的就缓存,简单说就是用内存换时间

  • 支持 Tree-Shaking,会在打包时去除一些无用代码,没有用到的模块,使得代码打包体积更小

  • 新增Composition API 可以更好的逻辑复用和代码组织,同一功能的代码不至于像以前一样太分散,虽然 Vue2 中可以用 minxin 来实现复用代码,但也存在问题,比如方法或属性名会冲突,代码来源也不清楚等

  • Proxy 代替 Object.defineProperty 重构了响应式系统,可以监听到数组下标变化,及对象新增属性,因为监听的不是对象属性,而是对象本身,还可拦截 apply、has 等 13 种方法

  • 重构了虚拟 DOM,在编译时会将事件缓存、将 slot 编译为 lazy 函数、保存静态节点直接复用(静态提升)、以及添加静态标记、Diff 算法使用 最长递增子序列 优化了对比流程,使得虚拟 DOM 生成速度提升 200%

  • 新的生命周期

  • 新增了开发环境的两个钩子函数,在组件更新时 onRenderTracked 会跟踪组件里所有变量和方法的变化、每次触发渲染时 onRenderTriggered 会返回发生变化的新旧值,可以让我们进行有针对性调试

  • Vue3 不兼容 IE11,对 TS 支持更好

# Vue3 做了哪些方面的优化

img

# 一、编译阶段

回顾 Vue2,我们知道每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把用到的数据 property 记录为依赖,当依赖发生改变,触发 setter,则会通知 watcher,从而使关联的组件重新渲染

img

Vue3 在编译阶段,做了进一步优化。主要有如下:

  • diff 算法优化

  • 静态提升

  • 事件监听缓存

  • SSR 优化

# diff 算法优化

vue3 在 diff 算法中相比 vue2 增加了静态标记

关于这个静态标记,其作用是为了会发生变化的地方添加一个 flag 标记,下次发生变化的时候直接找该地方进行比较

下图这里,已经标记静态节点的 p 标签在 diff 过程中则不会比较,把性能进一步提高

img

# 静态提升

Vue3 中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用

这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用

没有做静态提升之前

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createBlock(
      _Fragment,
      null,
      [_createVNode("span", null, "你好"), _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)],
      64 /* STABLE_FRAGMENT */
    )
  );
}

做了静态提升之后

const _hoisted_1 = /*#__PURE__*/ _createVNode("span", null, "你好", -1 /* HOISTED */);

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createBlock(
      _Fragment,
      null,
      [_hoisted_1, _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)],
      64 /* STABLE_FRAGMENT */
    )
  );
}

// Check the console for the AST

静态内容_hoisted_1 被放置在 render 函数外,每次渲染的时候只要取 _hoisted_1 即可

同时 _hoisted_1 被打上了 PatchFlag ,静态标记值为 -1 ,特殊标志是负整数表示永远不会用于 Diff

# 事件监听缓存

默认情况下绑定事件行为会被视为动态绑定,所以每次都会去追踪它的变化

没开启事件监听器缓存

export const render = /*#__PURE__*/ _withId(function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createBlock("div", null, [
      _createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])
      // PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
    ])
  );
});

开启了缓存后,没有了静态标记。也就是说下次 diff 算法的时候直接使用

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createBlock("div", null, [
      _createVNode(
        "button",
        {
          onClick: _cache[1] || (_cache[1] = (...args) => _ctx.onClick(...args))
        },
        "点我"
      )
    ])
  );
}
# SSR 优化

当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染

div>
	<div>
		<span>你好</span>
	</div>
	...  // 很多个静态属性
	<div>
		<span>{{ message }}</span>
	</div>
</div>

编译后

import { mergeProps as _mergeProps } from "vue";
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer";

export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
  const _cssVars = { style: { color: _ctx.color } };
  _push(
    `<div${_ssrRenderAttrs(
      _mergeProps(_attrs, _cssVars)
    )}><div><span>你好</span>...<div><span>你好</span><div><span>${_ssrInterpolate(_ctx.message)}</span></div></div>`
  );
}

# 二、源码体积

相比 Vue2,Vue3 整体体积变小了,除了移出一些不常用的 API,最重要的是Tree shanking

任何一个函数,如refreavtivedcomputed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小

# 三、响应式系统

  • vue2 中采用 defineProperty来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加 getter 和 setter,实现响应式

  • vue3 采用 proxy 重写了响应式系统,因为 proxy 可以对整个对象进行监听,所以不需要深度遍历

    • 可以监听动态属性的添加
    • 可以监听到数组的索引和数组 length 属性
    • 可以监听删除属性

# Composition-api

[

](https://www.yuque.com/yangzhihao-cmbps/poe3oq/pg7zqp#31cc29a3)为什么会有 composition-api,想一下有一个这样的场景,一个页面的功能有很多个,这些功能都是由一个又一个的组件拼凑起来的,这个时候代码的复用性非常重要,如果按照常规写法一定是一个功能都维护一份自己独有的 data,method,computed,watch 但是这样写下去的后果就是会将整个逻辑部分撑得很大,导致组件难以阅读和理解,尤其是那些没有编写过这些组件的人。或许你会想到使用 vue 的 mixin ,没错它一定意义地解决了复用抽离的问题,但是通常情况下我们混入整个 mixin,不管是变量还是 method ,都无法直接读出整个变量或者函数的真正作用和意义,很难读懂源码。composition-api 就是用来解决这种问题的。

img

# setup

setup 是 Vue3.x 新增的一个选项, 他是组件内使用 Composition API的入口。

setup 执行时机beforeCreate 之前

setup 参数

  1. props: 传入的属性
  2. context:context中就提供了this中最常用的三个属性:attrsslotemit,分别对应 Vue2.x 中的 $attr属性、slot插槽 和$emit发射事件。并且这几个属性都是自动同步最新的值,所以我们每次使用拿到的都是最新值。
export default defineComponent({
  name: "App",
  props: {
    name: String
  },
  // 接受两个参数 props 和 context
  setup(props, context) {
    // props 属性
    console.log(props.name);
    // Attribute (非响应式对象)
    console.log(context.attrs);

    // 插槽 (非响应式对象)
    console.log(context.slots);

    // 触发事件 (方法)
    console.log(context.emit);
  }
});

里面通过 ref 和 reactive 代替以前的 data 语法,return 出去的内容,可以在模板直接使用,包括变量和方法

# watch 和 watchEffect 的区别

  • watch作用是对传入的某个或多个值的变化进行监听;触发时会返回新值和老值;也就是说第一次不会执行,只有变化时才会重新执行
  • watchEffect 是传入一个立即执行函数,所以默认第一次也会执行一次;不需要传入监听内容,会自动收集函数内的数据源作为依赖,在依赖变化的时候又会重新执行该函数,如果没有依赖就不会执行;而且不会返回变化前后的新值和老值

共同点是 watch 和 watchEffect 会共享以下四种行为:

  • 停止监听:组件卸载时都会自动停止监听

  • 清除副作用:onInvalidate 会作为回调的第三个参数传入

  • 副作用刷新时机:响应式系统会缓存副作用函数,并异步刷新,避免同一个 tick 中多个状态改变导致的重复调用

  • 监听器调试:开发模式下可以用 onTrack 和 onTrigger 进行调试

# Vue3 Diff 算法和 Vue2 的区别

https://juejin.cn/post/7010594233253888013

编译阶段的优化:

  • 事件缓存:将事件缓存(如: @click),可以理解为变成静态的了

  • 静态提升:第一次创建静态节点时保存,后续直接复用

  • 添加静态标记:给节点添加静态标记,以优化 Diff 过程

由于编译阶段的优化,除了能更快的生成虚拟 DOM 以外,还使得 Diff 时可以跳过"永远不会变化的节点",Diff 优化如下

  • Vue2 是全量 Diff,Vue3 是静态标记 + 非全量 Diff
  • 使用最长递增子序列优化了对比流程

根据尤大公布的数据就是 Vue3 update 性能提升了 1.3~2

Last update: 3/7/2021, 4:01:04 PM