【前端面试】之Vue篇

216 阅读18分钟

整理一份任何人都能看明白的Vue知识点大全,帮助大家在面试中能够更加对答如流,在工作中能轻松应对各种复杂需求。

1.什么是MVC

MVC 全名是 Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范

  • Model(模型):是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据
  • View(视图):是应用程序中处理数据显示的部分。通常视图是依据模型数据创建的
  • Controller(控制器):是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据
  • 一句话描述就是Controller负责将Model的数据用View显示出来

2.什么是MVVM

MVVM 全名是 Model View ViewModel,是模型(model)-视图(view)-视图模型(view-model)的缩写,一种软件设计典范

  • Model(模型):是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据
  • View(视图):是应用程序中处理数据显示的部分。通常视图是依据模型数据创建的
  • ViewModel(视图-模型):做了两件事达到了数据的双向绑定 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听

3.MVC和MVVM的区别是什么

MVVM 与 MVC 最大的区别就是:它实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变(对应Vue数据驱动的思想)

整体看来,MVVM 比 MVC 精简很多,不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器操作 DOM 元素。因为在 MVVM 中,View 不知道 Model 的存在,Model 和 ViewModel 也观察不到 View,这种低耦合的模式提高了代码的可重用性

拓展知识:Vue 并没有完全遵循 MVVM 的思想 这一点官网自己也有说明。因为严格的 MVVM 要求 View 不能和 Model 直接通信,而 Vue 提供了$refs 这个属性,让 Model 可以直接操作 View,违反了这一规定,所以说 Vue 没有完全遵循 MVVM。

4.为什么data是一个函数

对象为引用类型,当复用组件时,由于数据对象都指向同一个data对象,当在一个组件中修改data时,其他重用的组件中的data会同时被修改,就会造成一个变了全都会变的结果;而使用返回对象的函数,由于每次返回的都是一个新对象(Object的实例),引用地址不同,则不会出现这个问题。

5.Vue组件通信有哪几种方式

  • (1)porps/$emit: 父组件通过props向子组件传递数据,子组件通过$emit和父组件通信。
<!-- 父组件 -->
<template>
  <div class="section">
    <com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article>
    <p>{{currentIndex}}</p>
  </div>
</template>

<script>
import comArticle from './test/article.vue'
export default {
  name: 'comArticle',
  components: { comArticle },
  data () {
    return {
      currentIndex: -1,
      articleList: ['红楼梦', '西游记', '三国演义']
    }
  },
  methods: {
    onEmitIndex (idx) {
      this.currentIndex = idx
    }
  }
}
</script>


<!-- 子组件 -->
<template>
  <div>
    <div 
    v-for="(item, index) in articles" 
    :key="index" 
    @click="emitIndex(index)"
  >
    {{item}}
  </div>
  </div>
</template>

<script>
export default {
  props: ['articles'],
  methods: {
    emitIndex (index) {
      // 触发父组件的方法,并传递参数index
      this.$emit('onEmitIndex', index) 
    }
  }
}
</script>
  • (2)eventBus: 事件总线。适用于父子组件、非父子组件等之间的通信,通过$emit$on。下面代码中,相当于将num值存储在了事件总线上,在其他组件中可以直接访问。该通信方式在大项目中,后期的维护性会比较低,因为同名的情况可能会十分常见。
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
<template>
  <div>
    <first-com></first-com>
    <second-com></second-com>
  </div>
</template>

<script>
import firstCom from './firstCom.vue'
import secondCom from './secondCom.vue'
export default {
  components: { firstCom, secondCom }
}
</script>


<!-- 发送事件 -->
<template>
  <div>
    <button @click="add">加法</button>    
  </div>
</template>
<script>
// 引入事件中心
import {EventBus} from './event-bus.js' 
export default {
  data(){
    return{
      num:0
    }
  },
  methods:{
    add(){
      EventBus.$emit('addition', {
        num:this.num++
      })
    }
  }
}
</script>


<!-- 接收事件 -->
<template>
  <div>求和: {{count}}</div>
</template>
<script>
import { EventBus } from './event-bus.js'
export default {
  data() {
    return {
      count: 0
    }
  },
  mounted() {
    EventBus.$on('addition', param => {
      this.count = this.count + param.num;
    })
  }
}
</script>
  • (3)project/inject: 依赖注入。在组件层数很深的情况下,可以使用这种方法来进行传值,就不用一层一层的传递了。需要注意的是依赖注入所提供的属性是非响应式的。(官方不推荐在实际业务中使用,但是写组件库时很常用)
<!-- 父组件 -->
<template>
  <div>
    <child></child>
  </div>
</template>

<script>
import Child from './Child.vue'
export default {
  provide () {
    return {
      num: this.num,
      // app: this //抛出父组件的所有data属性
    }
  },
  data () {
    return {
      num: 99,
      city: '深圳'
    }
  },
  components: {
    Child
  },
}
</script>


<!-- 子组件 -->
<template>
  <div>{{num}}</div>
</template>

<script>
export default {
  inject: ['num'],
  // inject: ['app'] //接收父组件的所有data属性
  data () {
    return {}
  },
  created() {
    console.log(this.num)
    // console.log(this.app.city)
  },
}
</script>
  • (4)ref/$refs: ref这个属性用在组件上,它的引用就指向了子组件的实例,可以通过实例来访问组件的数据和方法。
<!-- 父组件 -->
<template>
  <div>
    <child ref="childDom"></child>
  </div>
</template>

<script>
import Child from './Child.vue'
export default {
  mounted() {
    console.log(this.$refs.childDom.name)
    this.$refs.childDom.sayHello()
  },
  components: {
    Child
  },
}
</script>


<!-- 子组件 -->
<template>
  <div>{{name}}</div>
</template>

<script>
export default {
  data () {
    return {
      name: 'childPage'
    }
  },
  methods: {
    sayHello () {
      console.log('hello')
    }
  }
}
</script>
  • (5)$parent/$children: 使用$parent可以让组件访问到父组件的实例;使用$children可以让组件访问子组件的实例,但是$children并不能保证顺序,并且访问的数据也不是响应式的。(在实践发现vue3不能支持$children
<!-- 父组件 -->
<template>
  <div class="hello_world">
    <div>{{msg}}</div>
    <child></child>
    <button @click="change">点击改变子组件值</button>
  </div>
</template>
<script>
import Child from './Child.vue'
export default {
  components: { Child },
  data () {
    return {
      msg: 'parent-Vue'
    }
  },
  methods: {
    change () {
      // 改变子组件的值
      this.$children[0].message = 'changeValue'
    }
  }
}
</script>


<!-- 子组件 -->
<template>
  <div>
    <span>{{message}}</span>
    <p>获取父组件的值为: {{parentVal}}</p>
  </div>
</template>
<script>
export default {
  data () {
    return {
      message: 'child-Vue'
    }
  },
  computed: {
    parentVal () {
      return this.$parent.msg;
    }
  }
}
</script>
  • (6)$attrs/$listeners: 隔代组件传值的最佳方式。$attrs是继承所有的父组件属性(除了prop传递的属性、class 和 style ),一般用在子组件的子元素上;$listeners该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合v-on="$listeners"将所有的事件监听器指向这个组件的某个特定的子元素,相当于子组件继承父组件的事件。(在实践中发现vue3对此特性也不支持了)
<!-- 父组件 -->
<template>
  <div id="app">
    <!-- 此处监听了两个事件,可以在B组件或者C组件中直接触发  -->
    <child1 
      :p-child1="child1" 
      :p-child2="child2" 
      @test1="onTest1" 
      @test2="onTest2"
    />
  </div>
</template>
<script>
import Child1 from './Child1.vue';
export default {
  components: { Child1 },
  data() {
    return {
      child1: 'child1-value',
      child2: 'child2-value'
    }
  },
  methods: {
    onTest1 () {
      console.log('test1 running');
    },
    onTest2 () {
      console.log('test2 running');
    }
  }
};
</script>


<!-- 子组件 -->
<template>
  <div class="child-1">
    <p>child1-props: {{pChild1}}</p>
    <p>child1-$attrs: {{$attrs}}</p>
    <child2 v-bind="$attrs" v-on="$listeners"></child2>
  </div>
</template>
<script>
import Child2 from './Child2.vue';
export default {
  props: ['pChild1'],
  // false 只继承class属性
  inheritAttrs: false,
  mounted () {
    // 触发父组件中的test1方法
    this.$emit('test1'); 
  },
  components: { Child2 },
};
</script>


<!-- 孙子组件 -->
<template>
  <div class="child-2">
    <p>child2-props: {{pChild2}}</p>
    <p>child2-$attrs: {{$attrs}}</p>
  </div>
</template>
<script>
export default {
  props: ['pChild2'],
  inheritAttrs: true,
  mounted () {
    // 触发父组件中的test2方法
    this.$emit('test2');
  }
};
</script>
  • (7)Vuex状态管理

6.Vue2的生命周期

  • beforeCreate:在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问
  • created:实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el,如果非要想与 Dom 进行交互,可以通过 vm.$nextTick 来访问 Dom
  • beforeMount:在挂载开始之前被调用:相关的 render 函数首次被调用。
  • mounted:在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点
  • beforeUpdate:数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁(patch)之前。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程
  • updated:发生在更新完成之后,当前阶段组件 Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新,该钩子在服务器端渲染期间不被调用。
  • beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用。我们可以在这时进行善后收尾工作,比如清除计时器。
  • destroyed:Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
  • activatedkeep-alive 专属,组件被激活时调用
  • deactivatedkeep-alive 专属,组件被销毁时调用

7.Vue3的生命周期

  • beforeCreate -> 不需要(setup()代替)
  • created -> 不需要(setup()代替)
  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeUnmount -> onBeforeUnmount
  • unmounted -> onUnmounted
  • errorCaptured -> onErrorCaptured
  • renderTracked -> onRenderTracked
  • renderTriggered -> onRenderTriggered(属性监测)
  • activated -> onActivated
  • deactivated -> onDeactivated

8.异步请求在哪一步发起?

可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。

如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间;
  • ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

9.v-if和v-show的区别

实现原理:

  • v-if 动态向 DOM 树中添加或者删除 DOM 元素
  • v-show 是通过设置 DOM 元素的 display 样式控制显隐

编译过程:

  • v-if 在编译过程中会被转化为三元表达式,render 的时候不满足条件不进行渲染
  • v-show 会被编译为指令,不满足条件时控制样式隐藏对应节点 (display: none)

适用场景:

  • v-if 适用于很少改变条件,不需要频繁切换条件的场景
  • v-show 适用于需要非常频繁切换条件的场景

10.说一下Vue内置指令



11.怎样理解Vue的单向数据流

数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

如果实在要改变父组件的 prop 值, 可以在 data 里面定义一个变量, 并用 prop 的值初始化它, 之后用$emit 通知父组件去修改。

注意: 在子组件直接用 v-model 绑定父组件传过来的 prop 这样是不规范的写法,开发环境会报警告。

12.什么是computed,它有什么特点

computed 是计算属性,依赖其他属性计算值,并且 computed 的值有缓存,只有当计算值变化才会返回内容,它可以设置 getter 和 setter。特点有:

  • 支持缓存,只有依赖的数据发生了变化,才会重新计算
  • 不支持异步,当 computed 中有异步操作时,无法监听数据的变化
  • computed 的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于 data 声明过,或者父组件传递过来的 props 中的数据进行计算的。
  • 如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用 computed
  • 如果 computed 属性的属性值是函数,那么默认使用 get 方法,函数的返回值就是属性的属性值;在computed 中,属性有一个 get 方法和一个 set 方法,当数据发生变化时,会调用 set 方法。
  • 原理: 手写Vue2.0源码(十)-计算属性原理

13.什么是watch,它有什么特点

watch 监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。特点有:

  • 不支持缓存,数据变化时,它就会触发相应的操作
  • 支持异步监听
  • 监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
  • 监听数据必须是 data 中声明的或者父组件传递过来的 props 中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
immediate:组件加载立即触发回调函数

deep:深度监听,发现对象内部值的变化。但如果直接通过索
引修改数组项,或者修改数组长度时,无法监听到。

14.computed和watch的区别是什么

  • computed 计算属性:依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
  • watch 侦听器:更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。

15.v-if 与 v-for 为什么不建议一起使用

v-for 和 v-if 不要在同一个标签中使用,因为解析时先解析 v-for 再解析 v-if。如果遇到需要同时使用时可以考虑写成计算属性的方式。

  • Vue 处理指令时,v-for 比 v-if 有更高优先级 (Vue3 中相反)
  • 编译有三个过程:parse -> potimize -> codegen。在codegen 过程中,会先解析 AST 树中的与 v-for 相关的属性,再解析与 v-if 相关的属性。
  • 因此,即便我们每次只渲染出一小部分符合条件的元素,也需要在每次重新渲染的时候遍历整个列表

16.Vue2.0响应式数据的原理

整体思路是数据劫持+观察者模式。对象内部通过 defineReactive 方法,使用 Object.defineProperty 将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。当页面使用对应属性时,每个属性都拥有自己的 dep 属性,存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)。更多内容请看 手写Vue2.0源码(一)-响应式数据原理|技术点评

相关代码如下:

class Observer {
  // 观测值
  constructor(value) {
    this.walk(value);
  }
  walk(data) {
    // 对象上的所有属性依次进行观测
    let keys = Object.keys(data);
    for (let i = 0; i < keys.length; i++) {
      let key = keys[i];
      let value = data[key];
      defineReactive(data, key, value);
    }
  }
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
  observe(value); // 递归关键
  // --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止
  // 思考?如果Vue数据嵌套层级过深 >> 性能会受影响
  Object.defineProperty(data, key, {
    get() {
      console.log("获取值");
      // 需要做依赖收集过程 这里代码没写出来
      return value;
    },
    set(newValue) {
      if (newValue === value) return;
      console.log("设置值");
      // 需要做派发更新过程 这里代码没写出来
      value = newValue;
    },
  });
}
export function observe(value) {
  // 如果传过来的是对象或者数组 进行属性劫持
  if (
    Object.prototype.toString.call(value) === "[object Object]" ||
    Array.isArray(value)
  ) {
    return new Observer(value);
  }
}

17.Vue 如何检测数组变化

更改了数组原型,重写了数组中的方法(push、shift、unshift、pop、splice、sort、reverse),首先获取到这个数组的的 Observer 对象,如果有新的值,就调用 observeArray 继续对新的值观察变化(也就是通过target__proto__ == arrayMethods来改变了数组实例的型),然后手动调用 notify,通知渲染 watcher,执行update。

数组考虑性能原因没有用 defineProperty 对数组的每一项进行拦截,而是选择对 7 种数组(push,shift,pop,splice,unshift,sort,reverse)方法进行重写(AOP 切片思想)

所以在 Vue 中修改数组的索引和长度是无法监控到的。需要通过以上 7 种变异方法修改数组才会触发数组对应的 watcher 进行更新。更多内容请看 手写Vue2.0源码(一)-响应式数据原理|技术点评

相关代码如下:

// src/obserber/array.js

// 先保留数组原型
const arrayProto = Array.prototype;
// 然后将arrayMethods继承自数组原型
// 这里是面向切片编程思想(AOP)--不破坏封装的前提下,动态的扩展功能
export const arrayMethods = Object.create(arrayProto);
let methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "reverse",
  "sort",
];
methodsToPatch.forEach((method) => {
  arrayMethods[method] = function (...args) {
    // 这里保留原型方法的执行结果
    const result = arrayProto[method].apply(this, args);

    // 这句话是关键
    // this代表的就是数据本身 比如数据是{a:[1,2,3]} 那么我们使用a.push(4)
    // this就是a,ob就是a.__ob__ 这个属性就是上段代码增加的
    // 代表的是该数据已经被响应式观察过了指向Observer实例
    const ob = this.__ob__;

    // 这里的标志就是代表数组有新增操作
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
      default:
        break;
    }
    // 如果有新增的元素 inserted是一个数组 调用Observer实例的
    // observeArray对数组每一项进行观测
    if (inserted) ob.observeArray(inserted);
    // 之后咱们还可以在这里检测到数组改变了之后从而触发视图更新的操作--后续源码会揭晓
    return result;
  };
});

18.vue3.0 用过吗?了解多少

  • 响应式原理的改变: Vue3.x 使用 Proxy 取代 Vue2.x 版本的 Object.defineProperty
  • 组件选项声明方式: Vue3.x 使用 Composition API setup 是 Vue3.x 新增的一个选项, 他是组件内使用 Composition API 的入口。
  • 模板语法变化、slot 具名插槽语法、自定义指令、v-model 升级
  • 其它方面的更改: Suspense支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。
  • 基于 treeshaking 优化,提供了更多的内置功能。
  • 更多Vue3.0 新特性以及使用经验总结

19.Vue3.0 和 2.0 的响应式原理区别

Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达 13 种拦截方法,而不是和Vue2.x一样遍历监听对象中的属性值。

相关代码如下:

// 代理相关逻辑
import { mutableHandlers } from "./baseHandlers"; 
// 工具方法
import { isObject } from "./util"; 

export function reactive(target) {
  // 根据不同参数创建不同响应式对象
  return createReactiveObject(target, mutableHandlers);
}
function createReactiveObject(target, baseHandler) {
  if (!isObject(target)) {
    return target;
  }
  const observed = new Proxy(target, baseHandler);
  return observed;
}

const get = createGetter();
const set = createSetter();

function createGetter() {
  return function get(target, key, receiver) {
    // 对获取的值进行放射
    const res = Reflect.get(target, key, receiver);
    console.log("属性获取", key);
    if (isObject(res)) {
      // 如果获取的值是对象类型,则返回当前对象的代理对象
      return reactive(res);
    }
    return res;
  };
}
function createSetter() {
  return function set(target, key, value, receiver) {
    const oldValue = target[key];
    const hadKey = hasOwn(target, key);
    const result = Reflect.set(target, key, value, receiver);
    if (!hadKey) {
      console.log("属性新增", key, value);
    } else if (hasChanged(value, oldValue)) {
      console.log("属性值被修改", key, value);
    }
    return result;
  };
}
export const mutableHandlers = {
  get, // 当获取属性时调用此方法
  set, // 当修改属性时调用此方法
};

20.Vue 的父子组件生命周期钩子函数执行顺序

加载渲染过程

  • 父 beforeCreate
  • 父 created
  • 父 beforeMount
  • 子 beforeCreate
  • 子 created
  • 子 beforeMount
  • 子 mounted
  • 父 mounted

子组件更新过程

  • 父 beforeUpdate
  • 子 beforeUpdate
  • 子 updated
  • 父 updated

父组件更新过程

  • 父 beforeUpdate
  • 父 updated

销毁过程

  • 父 beforeDestroy
  • 子 beforeDestroy
  • 子 destroyed
  • 父 destroyed

21.虚拟 DOM 是什么 有什么优缺点

由于在浏览器中操作 DOM 是很昂贵的。频繁的操作 DOM,会产生一定的性能问题。这就是虚拟 Dom 的产生原因。Vue2 的 Virtual DOM 借鉴了开源库 snabbdom 的实现。Virtual DOM 本质就是用一个原生的 JS 对象去描述一个 DOM 节点,是对真实 DOM 的一层抽象。

优点

  • 保证性能下限:框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
  • 无需手动操作 DOM:我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
  • 跨平台:虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。

缺点:

  • 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
  • 首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。

22.v-model 原理

v-model 只是语法糖而已,v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:

  • text 和 textarea 元素使用 value、property 和 input 事件;
  • checkbox 和 radio 使用 checked、property 和 change 事件;
  • select 字段将 value 作为 prop 并将 change 作为事件。
<!-- 在普通标签上 -->
<input v-model="sth" />
<input v-bind:value="sth" v-on:input="sth = $event.target.value" />

<!-- 在组件上 -->
<currency-input v-model="price" />
<currency-input :value="price" @input="price = arguments[0]" />

<!-- 子组件定义 -->
<script>
Vue.component('currency-input', {
  template: `
    <span>
    <input
      ref="input"
      :value="value"
      @input="$emit('input', $event.target.value)"
    >
    </span>
  `,
  props: ['value'],
})
</script>

23.v-for 为什么要加 key

如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速

更准确: 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。

更快速: 利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快

相关代码如下:

// 判断两个vnode的标签和key是否相同,如果相同,就可以认为是同一节点就地复用
function isSameVnode(oldVnode, newVnode) {
  return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;
}

// 根据key来创建老的儿子的index映射表  类似 {'a':0,'b':1} 代表key为'a'的节
// 点在第一个位置 key为'b'的节点在第二个位置
function makeIndexByKey(children) {
  let map = {};
  children.forEach((item, index) => {
    map[item.key] = index;
  });
  return map;
}
// 生成的映射表
let map = makeIndexByKey(oldCh);

更多内容 手写Vue2.0源码(六)-diff算法原理

24.Vue 事件绑定原理

原生事件绑定是通过 addEventListener 绑定给真实元素的,组件事件绑定是通过 Vue 自定义的$on 实现的。如果要在组件上使用原生事件,需要加.native 修饰符,这样就相当于在父组件中把子组件当做普通 html 标签,然后加上原生事件。

$on、$emit 是基于发布订阅模式的,维护一个事件中心,on 的时候将事件按名称存在事件中心里,称之为订阅者,然后 emit 将对应的事件进行发布,去执行事件中心里的对应的监听器。

代码如下:

// 手写发布订阅模式 EventEmitter
class EventEmitter {
  constructor() {
    this.events = {};
  }
  // 实现订阅
  on(type, callBack) {
    if (!this.events) this.events = Object.create(null);

    if (!this.events[type]) {
      this.events[type] = [callBack];
    } else {
      this.events[type].push(callBack);
    }
  }
  // 删除订阅
  off(type, callBack) {
    if (!this.events[type]) return;
    this.events[type] = this.events[type].filter(item => {
      return item !== callBack;
    });
  }
  // 只执行一次订阅事件
  once(type, callBack) {
    function fn() {
      callBack();
      this.off(type, fn);
    }
    this.on(type, fn);
  }
  // 触发事件
  emit(type, ...rest) {
    this.events[type] && this.events[type].forEach(fn => fn.apply(this, rest));
  }
}
// 使用如下
const event = new EventEmitter();

const handle = (...rest) => {
  console.log(rest);
};

event.on("click", handle);

event.emit("click", 1, 2, 3, 4);

event.off("click", handle);

event.emit("click", 1, 2);

event.once("dbClick", () => {
  console.log(123456);
});

event.emit("dbClick");
event.emit("dbClick");

25.vue-router 路由钩子函数是什么 执行顺序是什么

路由钩子的执行流程, 钩子函数种类有:全局守卫、路由守卫、组件守卫

完整的导航解析流程:

  • 导航被触发。
  • 在失活的组件里调用 beforeRouteLeave 守卫。
  • 调用全局的 beforeEach 守卫。
  • 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  • 在路由配置里调用 beforeEnter。
  • 解析异步路由组件。
  • 在被激活的组件里调用 beforeRouteEnter。
  • 调用全局的 beforeResolve 守卫 (2.5+)。
  • 导航被确认。
  • 调用全局的 afterEach 钩子。
  • 触发 DOM 更新。
  • 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

26.vue-router 动态路由是什么

我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果:

const User = {
  template: "<div>User</div>",
};

const router = new VueRouter({
  routes: [
    // 动态路径参数 以冒号开头
    { path: "/user/:id", component: User },
  ],
});

27.vue-router 组件复用导致路由参数失效怎么办?

(1)通过 watch 监听路由参数再发请求

// 通过watch来监听路由变化
watch: {
  "$route": function(){
    this.getData(this.$route.params.xxx);
  }
}

(2)用 :key 来阻止“复用”

<router-view :key="$route.fullPath" />

28.谈一下对 vuex 的个人理解

vuex 是专门为 vue 提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。(无法持久化、内部核心原理是通过创造一个全局实例 new Vue)

主要包括以下几个模块:

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

29.Vuex 页面刷新数据丢失怎么解决

需要做 vuex 数据持久化 一般使用本地存储的方案来保存数据 可以自己设计存储方案 也可以使用第三方插件

推荐使用 vuex-persist 插件,它就是为 Vuex 持久化存储而生的一个插件。不需要你手动存取 storage ,而是直接将状态保存至 cookie 或者 localStorage 中。

30.Vuex 为什么要分模块并且加命名空间

模块: 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。

命名空间: 默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。

31.使用过 Vue SSR 吗?说说 SSR

SSR 也就是服务端渲染,也就是将 Vue 在客户端把标签渲染成 HTML 的工作放在服务端完成,然后再把 html 直接返回给客户端。

优点:SSR 有着更好的 SEO、并且首屏加载速度更快

缺点:开发条件会受到限制,服务器端渲染只支持 beforeCreate 和 created 两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 Node.js 的运行环境。并且服务器会有更大的负载需求

32.Vue 中使用了哪些设计模式

  • 工厂模式-传入参数即可创建实例:虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
  • 单例模式-整个程序有且仅有一个实例:vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
  • 发布-订阅模式 (vue 事件机制)
  • 观察者模式 (响应式数据原理)
  • 装饰模式 (@装饰器的用法)
  • 策略模式: 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案,比如选项的合并策略

33.你都做过哪些 Vue 的性能优化

编码阶段:

  • 尽量减少 data 中的数据,data 中的数据都会增加 getter 和 setter,会收集对应的 watcher
  • 不需要响应式的数据不要放到 data 中(可以用 Object.freeze() 冻结数据)
  • v-if 和 v-for 不能连用
  • v-if 和 v-show 区分使用场景
  • computed 和 watch 区分使用场景
  • v-for 遍历必须加 key,key 最好是 id 值,保证key值的唯一性
  • 如果需要使用 v-for 给每项元素绑定事件时使用事件代理
  • 大数据列表和表格性能优化-虚拟列表/虚拟表格
  • 长列表滚动到可视区域动态加载
  • 防止内部泄漏,组件销毁后把全局变量和事件销毁
  • 图片懒加载
  • 路由懒加载
  • 使用异步组件
  • 第三方插件的按需引入
  • 适当采用 keep-alive 缓存组件
  • 防抖、节流运用

SEO优化:

  • 服务端渲染 SSR
  • 预渲染

打包优化:

  • 压缩代码
  • Tree Shaking/Scope Hoisting
  • 使用 cdn 加载第三方模块
  • splitChunks 抽离公共文件
  • sourceMap 优化

用户体验:

  • 骨架屏
  • PWA
  • 还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启 gzip 压缩等。

34.Vue.mixin 的使用场景和原理

在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立,可以通过 Vue 的 mixin 功能抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用 mergeOptions 方法进行合并,采用策略模式针对不同的属性进行合并。当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。

相关代码如下:

export default function initMixin(Vue){
  Vue.mixin = function (mixin) {
    // 合并对象
    this.options=mergeOptions(this.options,mixin)
  };
}

// src/util/index.js
// 定义生命周期
export const LIFECYCLE_HOOKS = [
  "beforeCreate",
  "created",
  "beforeMount",
  "mounted",
  "beforeUpdate",
  "updated",
  "beforeDestroy",
  "destroyed",
];

// 合并策略
const strats = {};
// mixin核心方法
export function mergeOptions(parent, child) {
  const options = {};
  // 遍历父亲
  for (let k in parent) {
    mergeFiled(k);
  }
  // 父亲没有 儿子有
  for (let k in child) {
    if (!parent.hasOwnProperty(k)) {
      mergeFiled(k);
    }
  }

  //真正合并字段方法
  function mergeFiled(k) {
    if (strats[k]) {
      options[k] = strats[k](parent[k], child[k]);
    } else {
      // 默认策略
      options[k] = child[k] ? child[k] : parent[k];
    }
  }
  return options;
}

Vue.mixin 原理详解 传送门

35.nextTick 使用场景和原理

nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法

相关代码如下:

// src/util/next-tick.js

let callbacks = [];
let pending = false;
function flushCallbacks() {
  // 把标志还原为false
  pending = false; 
  // 依次执行回调
  for (let i = 0; i < callbacks.length; i++) {
    callbacks[i]();
  }
}

// 定义异步方法,采用优雅降级
let timerFunc; 
if (typeof Promise !== "undefined") {
  // 如果支持promise
  const p = Promise.resolve();
  timerFunc = () => {
    p.then(flushCallbacks);
  };
} else if (typeof MutationObserver !== "undefined") {
  // MutationObserver 主要是监听dom变化 也是一个异步方法
  let counter = 1;
  const observer = new MutationObserver(flushCallbacks);
  const textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {
    characterData: true,
  });
  timerFunc = () => {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
} else if (typeof setImmediate !== "undefined") {
  // 如果前面都不支持 判断setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks);
  };
} else {
  // 最后降级采用setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}

export function nextTick(cb) {
  // 除了渲染watcher  还有用户自己手动调用的nextTick 一起被收集到数组
  callbacks.push(cb);
  if (!pending) {
    // 如果多次调用nextTick  只会执行一次异步 等异步队列清空之后再把标志变为false
    pending = true;
    timerFunc();
  }
}

36.keep-alive 使用场景和原理

keep-alive 是 Vue 内置的一个组件,可以实现组件缓存,当组件切换时不会对当前组件进行卸载。

  • 常用的两个属性 include/exclude,允许组件有条件的进行缓存。
  • 两个生命周期 activated/deactivated,用来得知当前组件是否处于活跃状态。
  • keep-alive 的中还运用了 LRU(最近最少使用) 算法,选择最近最久未使用的组件予以淘汰。

相关代码如下:

export default {
  name: "keep-alive",
  abstract: true, //抽象组件

  props: {
    include: patternTypes, //要缓存的组件
    exclude: patternTypes, //要排除的组件
    max: [String, Number], //最大缓存数
  },

  created() {
    //缓存对象  {a:vNode,b:vNode}
    this.cache = Object.create(null); 
    //缓存组件的key集合 [a,b]
    this.keys = []; 
  },

  destroyed() {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys);
    }
  },

  mounted() {
    //动态监听include、exclude
    this.$watch("include", (val) => {
      pruneCache(this, (name) => matches(val, name));
    });
    this.$watch("exclude", (val) => {
      pruneCache(this, (name) => !matches(val, name));
    });
  },

  render() {
    //获取包裹的插槽默认值
    const slot = this.$slots.default; 
    //获取第一个子组件
    const vnode: VNode = getFirstComponentChild(slot); 
    const componentOptions:?VNodeComponentOptions =
      vnode && vnode.componentOptions;
    if (componentOptions) {
      // check pattern
      const name:?string = getComponentName(componentOptions);
      const { include, exclude } = this;
      // 不走缓存
      if (
        // not included  不包含
        (include && (!name || !matches(include, name))) ||
        // excluded  排除里面
        (exclude && name && matches(exclude, name))
      ) {
        //返回虚拟节点
        return vnode;
      }

      const { cache, keys } = this;
      const key:?string = vnode.key == null ? 
        componentOptions.Ctor.cid +
        (componentOptions.tag ? `::${componentOptions.tag}` : "")
        : vnode.key;
      if (cache[key]) {
        //通过key 找到缓存 获取实例
        vnode.componentInstance = cache[key].componentInstance;
        // make current key freshest
        remove(keys, key); //通过LRU算法把数组里面的key删掉
        keys.push(key); //把它放在数组末尾
      } else {
        cache[key] = vnode; //没找到就换存下来
        keys.push(key); //把它放在数组末尾
        // prune oldest entry  //如果超过最大值就把数组第0项删掉
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode);
        }
      }
      vnode.data.keepAlive = true; //标记虚拟节点已经被缓存
    }
    // 返回虚拟节点
    return vnode || (slot && slot[0]);
  },
};

37.扩展补充:LRU 算法是什么?

LRU 的核心思想是如果数据最近被访问过,那么将来被访问的几率也更高,所以我们将命中缓存的组件 key 重新插入到 this.keys 的尾部,这样一来,this.keys 中越往头部的数据即将来被访问几率越低,所以当缓存数量达到最大值时,我们就删除将来被访问几率最低的数据,即 this.keys 中第一个缓存的组件。

38.Vue.set方法原理是什么

在Vue2版本里,有两种情况下修改数据 Vue 是不会触发视图更新的

  • 在实例创建之后添加新的属性到实例上(给响应式对象新增属性)
  • 直接更改数组下标来修改数组的值

Vue.set 或者说是$set 原理如下:

因为响应式数据 我们给对象和数组本身都增加了__ob__属性,代表的是 Observer 实
例。当给对象新增不存在的属性 首先会把新的属性进行响应式跟踪 然后会触发对象
__ob__的 dep 收集到的 watcher 去更新,当修改数组索引时我们调用数组本身的 
splice 方法去更新数组

相关代码如下:

export function set(target: Array | Object, key: any, val: any): any {
  // 如果是数组 调用我们重写的splice方法 (这样可以更新视图)
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val;
  }
  // 如果是对象本身的属性,则直接添加即可
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  const ob = (target: any).__ob__;

  // 如果不是响应式的也不需要将其定义成响应式属性
  if (!ob) {
    target[key] = val;
    return val;
  }
  // 将属性定义成响应式的
  defineReactive(ob.value, key, val);
  // 通知视图更新
  ob.dep.notify();
  return val;
}

39.Vue.extend 作用和原理

官方解释:Vue.extend 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

其实就是一个子类构造器 是 Vue 组件的核心 api 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并

相关代码如下

export default function initExtend(Vue) {
  //组件的唯一标识
  let cid = 0; 
  // 创建子类继承Vue父类 便于属性扩展
  Vue.extend = function (extendOptions) {
    // 创建子类的构造函数 并且调用初始化方法
    const Sub = function VueComponent(options) {
      //调用Vue初始化方法
      this._init(options); 
    };
    Sub.cid = cid++;
    // 子类原型指向父类
    Sub.prototype = Object.create(this.prototype); 
    // constructor指向自己
    Sub.prototype.constructor = Sub; 
    // 合并自己的options和父类的options
    Sub.options = mergeOptions(this.options, extendOptions); 
    return Sub;
  };
}

Vue 组件原理详解 传送门

40.写过自定义指令吗?原理是什么

指令本质上是装饰器,是 vue 对 HTML 元素的扩展,给 HTML 元素增加自定义功能。vue 编译 DOM 时,会找到指令对象,执行指令的相关方法。

自定义指令中的生命周期(也叫钩子函数):

  • created: 在绑定元素的 attribute 或事件监听器被应用之前调用
  • beforeMount: 当指令第一次绑定到元素并且在挂载父组件之前调用
  • mounted: 在绑定元素的父组件被挂载后调用
  • beforeUpdate: 在更新包含组件的 VNode 之前调用
  • updated: 在包含组件的 VNode及其子组件的 VNode更新后调用
  • beforeUnmount: 在卸载绑定元素的父组件之前调用
  • unmounted: 当指令与元素解除绑定且父组件已卸载时,只调用一次

原理:

  • 在生成 ast 语法树时,遇到指令会给当前元素添加 directives 属性
  • 通过 genDirectives 生成指令代码
  • 在 patch 前将指令的钩子提取到 cbs 中,在 patch 过程中调用对应的钩子
  • 当执行指令对应钩子函数时,调用对应指令定义的方法

41.Vue 修饰符有哪些

事件修饰符:

  • .stop:阻止事件继续传播
  • .prevent:阻止标签默认行为
  • .capture:使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理
  • .self:只当在 event.target 是当前元素自身时触发处理函数
  • .once:事件将只会触发一次
  • .passive:告诉浏览器你不想阻止事件的默认行为

v-model 的修饰符:

  • .lazy:通过这个修饰符,转变为在 change 事件再同步
  • .number:自动将用户的输入值转化为数值类型
  • .trim:自动过滤用户输入的首尾空格

键盘事件的修饰符:

  • .enter
  • .tab
  • .delete (捕获“删除”和“退格”键)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

系统修饰键:

  • .ctrl
  • .alt
  • .shift
  • .meta

鼠标按钮修饰符:

  • .left
  • .right
  • .middle

42.Vue 模板编译原理

Vue 的编译过程就是将 template 转化为 render 函数的过程 分为以下三步

第一步是将 模板字符串 转换成 element ASTs(解析器)
第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)
第三步是 使用 element ASTs 生成 render 函数代码字符串(代码生成器)

相关代码如下:

export function compileToFunctions(template) {
  // 我们需要把html字符串变成render函数
  // 1.把html代码转成ast语法树  ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法
  // 很多库都运用到了ast 比如 webpack babel eslint等等
  let ast = parse(template);
  // 2.优化静态节点
  // 这个有兴趣的可以去看源码  不影响核心功能就不实现了
  // if (options.optimize !== false) {
  //   optimize(ast, options);
  // }

  // 3.通过ast 重新生成代码
  // 我们最后生成的代码需要和render函数一样
  // 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
  // _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本
  let code = generate(ast);
  // 使用with语法改变作用域为this  之后调用render函数可以使用call改变this 方便code里面的变量取值
  let renderFn = new Function(`with(this){return ${code}}`);
  return renderFn;
}

模板编译原理详解 传送门

43.Vue路由的hash模式

  • 简介: 在vue中默认的路由模式是hash模式。该模式的URL带着一个#,例如:www.abc.com/#/vue,它的hash值就是#/vue
  • 特点: hash值会出现在 URL 里面,但是不会出现在HTTP请求中,对后端完全没有影响。改变 hash 值,不会重新加载页面。这种模式的浏览器支持度很好,低版本的IE浏览器也支持这种模式。hash 路由被称为是前端路由,已经成为 SPA(单页面应用)的标配。
  • 原理: hash模式的主要原理就是onhashchange()事件
window.onhashchange = function(event){
  console.log(event.oldURL, event.newURL);
  let hash = location.hash.slice(1);
}

使用 onhashchange() 事件的好处就是,在页面的hash值发生变化时,无需向后端发起请求,window就可以监听事件的改变,并按规则加载相应的代码。除此之外,hash 值变化对应的 URL 都会被浏览器记录下来,这样浏览器就能实现页面的前进和后退。虽然是没有请求后端服务器,但是页面的 hash 值和对应的 URL 关联起来了。

44.Vue路由的history模式

  • 简介: history 模式的 URL 中没有#,它使用的是传统的路由分发模式,即用户在输入一个URL时,服务器会接收这个请求,并解析这个URL,然后做出相应的逻辑处理。
  • 特点: 当使用history模式时,URL就像这样:abc.com/user/id。相比hash模式更加好看。但是,history模式需要后台配置支持。如果后台没有正确配置,访问时会返回404。
  • API: 分为两大部分,切换和修改
1.切换历史状态:
包括 back,forward,go三个方法,对应浏览器的前进,后退,跳转操作。很多小
伙伴只知道浏览器有前进和后退,其实在前进后退上长按鼠标,会弹出历史记录,
从而完成跳转.

2.修改历史状态:
包括了pushState,replaceState两个方法,这两个方法接受三个参数:stateObj,title,url



45.Vue路由hash模式和history模式的区别

使用history模式时,调用 history.pushState() 相比于直接修改 hash,存在以下优势:

  • pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL;
  • pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中;
  • pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串;
  • pushState() 可额外设置 title 属性供后续使用。

当然啦,history 也不是样样都好。SPA 虽然在浏览器里游刃有余,但真要通过 URL 向后端发起 HTTP 请求时,两者的差异就来了。尤其在用户手动输入 URL 后回车,或者刷新(重启)浏览器的时候。

  • hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 http://www.abc.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。
  • history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 http://www.abc.com/book/id。如果后端缺少对 /book/id 的路由处理,将返回 404 错误。Vue-Router 官网里如此描述:“不过这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。”