Vue常见面试问题及解析(持续更新中····)

1、如何理解 MVVM 原理

MVVM

  • MModel,数据层对数据的处理;
  • VView,视图层,是 HMTL 显示页面;
  • VMViewModel:业务逻辑层(一切 JS 可视为业务逻辑,比如表单按钮提交,自定义事件的注册和处理逻辑都在ViewModel 里面负责监听两边的数据)。

2、响应式数据的原理是什么

响应式的基本机制:

  • 通过 Object.defineProperty() 替换配置对象属性的 setget 方法。
  • watcher 在执行 getter 函数是触发 get 方法,从而建立依赖关系。
  • 写入数据时触发 set 方法,从而借助 dep 发布通知,进而 getter 进行更新。

Vue响应式

3、Vue中是如何检测数组和对象变化

由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。

对于对象

  • 使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property
Vue.set(someObject, "b", 2);
  • 使用 Vue.$set(this.someObject, propeytyName, value) 实例方法,这也是全局 Vue.set() 的别名。
Vue.$set(someObject, "b", 2);
  • 为已有对象赋值多个新的 property
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 });

对于数组

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

  • 当你利用索引值直接设置一个数组项时: vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时:vm.item.length = newLength

解决第一类问题:

Vue.set(vm.items, indexOfItems, newValue);
vm.items.splice(indexOfItems, 1, newValue);

解决第二类问题:

vm.items.splice(newLength);

4、为何Vue采用异步渲染

如果不采用异步渲染,那么每次数据更新都会对当前组件进行重写渲染,所以为了性能考虑,会在本轮数据更新后,再去异步更新视图。

Vue异步更新

5、nextTick实现原理

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

nextTick的实现

6、Vue组件的生命周期

Vue生命周期

7、Ajax 请求放在那个生命周期中

mounted 生命周期中请求 Ajax,此时 html 已经渲染出来了,可以直接操作 dom 节点。

8、何时需要使用beforeDestroy

实例销毁之前调用。在这一步,实例仍然完全可用。该钩子在啊服务器渲染期间不被调用

9、Vue父子组件生命周期调用顺序

  • 加载渲染过程
父beforeCreate -> 父create -> 父beforeMounted -> 子beforeCreate -> 子create -> 子mounted -> 父mounted
  • 子组件更新过程
父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
  • 父组件更新过程
父beforeUpdate -> 父updated

销毁过程

父beforeDestory -> 子beforeDestory -> 子destoryed -> 父destoryed

10、Vuecomputed中的特点

特点

- 监控自己定义的变量,不用再 `data` 里面声明,函数名就是变量名。
- 适合多个变量会对象处理后返回一个值。若这个多个变量只要有一个发生变化,结果都回变化。
- 计算的结果具有缓存、依赖响应式属性变化,响应式属性没有变化,直接从缓存中读取结果。
- 在函数内部调用的时候不用加 `()`- 必须用 `return` 返回。
- 不要在 `computed` 中对 `data` 中的数据进行赋值操作,这回形成一个死循环。

应用场景

- 一个需要的结果受多个数据影响的时候,比如购物车结算的金额。
- 操作某个属性,执行一些复杂的逻辑,并在多处使用这个结果。
- 内部函数中多处要使用到这个结果的。

11、watch的用法及deep: true是如何实现的

watch 是一个侦听的动作,用来观察和响应 Vue 实例上的数据变动。

handler 方法和 immediate 属性

<div id="demo">{{ fullName }}</div>
var vm = new Vue({
  el: "#demo",
  data: {
    firstName: "Foo",
    lastName: "Bar",
    fullName: "Foo Bar",
  },
  watch: {
    firstName: (val) => {
      console.log("第一次没有执行");
      this.fullName = val + this.lastName;
    },
  },
});

初始化的时候,watch 是不会执行的。只要当 firstName 的值改变的时候才会执行监听计算。如果想再第一次被绑定的时候就执行,则需要这样修改:

var vm = new Vue({
  el: "#demo",
  data: {
    firstName: "Foo",
    lastName: "Bar",
    fullName: "Foo Bar",
  },
  watch: {
    firstName: (val) => {
      console.log("第一次执行了");
      this.fullName = val + this.lastName;
    },
    // 代表在 watch 里声明了 firstName 这个方法后,先去立即执行 handler 方法
    immediate: true,
  },
});

deep 属性

deep 属性的意思是深度监听,会在对象一层层往下遍历,在每一层上都加上监听器。默认为 false

<div id="app">
  <div>obj.a: {{ obj.a }}</div>
  <input type="text" v-model="obj.a" />
</div>
var vm = new Vue({
  el: "#app",
  data: {
    obj: {
      a: 1,
    },
  },
  watch: {
    obj: {
      handler(val) {
        console.log("obj,a changed");
      },
      immediate: true,
    },
  },
});

当我们在 input 输入框中输入数据改变 obj.a 的值时,我们发现在控制台没有打印出 obj.a changed。受现代 Javascript 的限制(以及废弃 Object.observe ), Vue 不能检测到对象属性的添加或删除。 由于 Vue 会在初始化实例时对属性执行 getter/setter 转化过程,所以属性必须在 data 对象上存在才能让 Vue 转换它,才能让它是响应式的。
默认情况下, 在 handler 方法中 只监听 obj 这个属性它的引用的变化,我们只有给 obj 赋值的时候它才会监听到,比如我们在 mounted 事件钩子函数中对 obj 进行重新赋值:

mounted() {
  this.obj = {
    a: '123
  }
}

这样 handler 就会执行了,且打印出了 obj.a changed

但是如果需要监听 obj 里的属性的值呢?这时候, deep 属性就派上用场了。我们只需要加上 deep: true,就能深度监听 obj 里属性的值。

watch: {
  obj: {
    handler(val) {
      console.log("obj.a changded");
    },
    immediate: true,
    deep: true
  }
}

使用 deep 属性会给每一层都加上监听器,性能开销可能就会非常大了。可以使用字符串的形式来优化:

watch: {
  'obj.a': {
    handler(val) {
      console.log("obj.a changde");
    },
    immediate: true
    // deep: true
  }
}

直到遇到 obj.a 属性,才会给该属性设置监听 d 函数,提高性能。

12、Vue中事件绑定的原理

13、Vuev-html回导致哪些问题

  • v-html 更新的元素是 innerHTML,内容按普通HTML插入,不会作为Vue模版进行编译。在网站上动态渲染 HTML 是非常危险的,因为容易导致XSS攻击。只在可信的内容上使用v-html,不用在用户提交的的内容上。
  • 在单文件组件里,scoped 的样式不会应用在 v-html 内部,因为那部分 HTML 没有被 Vue 的模板编译器处理。如果你希望针对 v-html 的内容设置带作用域的 CSS,你可以替换为 CSS Modules 或用一个额外的全局。

第二个问题的解决方案:
第一种解决方案,照样使用 scoped,但是我们可以使用深度选择器 (>>>),示例如下:

<style>
  .a >>> .b {
    /*
  ...
  */
  }
</style>

以上代码最终会被编译为:

.a[data-v-f3f3eg9] .b { /* ... */ }

如果你的 vue 项目使用 lesssass 的时候,>>> 可能会失效,我们可以用 /deep/ 来代替,代码如下:

.a {
  /deep/ .b {
    /*
    ...
    */
  }
}

第二种解决方案,单文件组件 style 标签可以使用多次,可以一个 style 标签带 scoped 属性针对当前组件,另外一个 style 标签针对全局生效,但是内部我们采用特殊的命名规则即可,例如 BEM 规则。

14、Vuev-ifv-show的区别

`v-if` 是真正的条件渲染,因为它会确保在切换的过程中,条件块中的事件监听器和子组件适当的被销毁和重建。
`v-if` 也是惰性的:如果在初始渲染条件为假,则直到第一次变为真时,才会开始渲染条件块。

`v-show` 不管初始条件是什么,元素总是回被渲染,并且只是简单基于 `CSS` 进行切换。

如果需要非常频繁的切换,用`v-show` 较好;如果在运行时条件不太可能改变,则使用`v-if`较好。

15、为什么v-forv-if不能连用

v-forv-if 不应该一起使用,必要情况下应该替换成 computed 属性。
原因:v-forv-if 优先,如果每一次都要遍历整个数组,将会影响速度。尤其是当之需要渲染一小部分的时候。

<ul>
  <li v-for="user in users" v-if="user.isActive" :key="user.id">
    {{ user.name }}
  </li>
</ul>

如上情况,即使 100 个user中只需要使用一个数据,也会循环整个数组。

正确的做法:

computed: {
  activeUsers: () => {
    return this.users.filter((user) => {
      return user.isActive;
    });
  };
}
<ul>
  <li v-for="user in activeUsers" :key="user.id">
    {{ user.name }}
  </li>
</ul>

16、v-model中的实现原理及如何自定义v-model

17、组件中的data为什么是一个函数

当一个组件被定义,data 必须声明返回为一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果 data 是一个纯粹的对象,则所引用的实例将共享引用同一个数据对象。通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新副本数据对象。

我们假设 data 是一个对象,因为组件是可以被复用的,注册了一个组件本质上就是创建了一个组件构造器的引用,而真正当我们使用组件的时候才会去将组件实例化。

const Component = function () {};

Component.prototype.data = {
  a: 1,
  b: 2,
};

// 使用组件
const componentA = new Component();
const componentB = new Component();

componentA.data.b = 3;
conponentB.data.b; // 3

当修改一个属性的时候, data 也会发生改变,这明显不是我们想要的结果。

const component = function () {};

Component.prototype.data = function () {
  return {
    a: 1,
    b: 2,
  };
};

const componentA = new Component();
const componentB = new Component();

componentA.data.b = 3;
componentB.data.b; // 2

data 是一个函数的时候,每一个实例的 data 属性都是独立的,不会相互影响。 JavaScript 本身的面向对象编程也是基于原型链和构造函数,应该会注意到原型链上添加一般都是一个函数方法而不是一个对象了。

18、Vue组件如何让通信(常见)

方法一、 props / $emit

1、父组件向子组件传值

我们提供一个例子,来说明父组件如何向子组件传值:在子组件 User.vue 中如何获取父组件 App.vue 中的数据:users: ["Henry", "Bucky", "Emily"]

// App.vue 父组件
<template>
  <div id="app">
    <users :users="users"></users> // 前者自定义名称便于子组件调用,后者要传递数据名
  </div>
</template>

<script>
import Users from "./components/Users";
export default {
  name: "App",
  data() {
    return {
      users: ["Henry", "Bucky", "Emily"]
    }
  },
  components: {
    "users": Users
  }
}
</script>
// 子组件 Users.vue
<template>
  <div class="users">
    <ul>
      <li v-for="user in users" >{{ user }}</li> // 遍历传递过来的值,然后呈现到页面
    </ul>
  </div>
</template>
<script>
export default {
  name: "Users",
  props: {
    users: {
      type: Array,
      required: true,
      dafault: () => []
    }
  }
}
</script>

父组件通过 props 向下传递数据给子组件。注:组件中的数据共有三种数据形式: datapropscomputed

2、子组件向父组件传值
// 子组件
<template>
  <header>
    <button @click="changeTitle" >{{title}}</button>
  </header>
</template>

<script>
export default {
  nanne: "app-header",
  data() {
    return {
      title: "Vue.js Demo"
    }
  },
  methods: {
    changeTitle() {
      this.$emit("titleChanged", "子向父组件传值"); // 自定义事件 传递值:子向父组件传值
    }
  }
}
</script>
// 父组件
<template>
  <div id="app">
    <app-header @titleChanged="updateTitle" ></app-header> // 与子组件 titleChanged 自定义事件保持一致
    // updateTitle($event) 接受传递过来的文字
    <h2>{{ title }}</h2>
  </div>
</template>

<script>
import Header from './components/header';
export default {
  name: 'App',
  data() {
    return {
      title: '传递的是一个值'
    };
  },
  components: {
    "app-header": Header
  },
  methods: {
    updateTitle(e) {
      this.title = e;
    }
  }
}
</script>

总之,子组件通过 event 给父组件发送消息,实际上就是子组件把自己的数据发送到父组件。

方法二、 $emit / $on

通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量的实现了任何组件间的通信。包括父子、兄弟、跨级。当我们的项目比较大时,可以选择更好的状态管理解决方案 vuex

1、具体实现方式
const GlobalBusEvent = new Vue();

GlobalBusEvent.$emit(事件名, 数据);
GlobalBusEvent.$on(事件名, (data) => {});
2、具体例子

假设兄弟组件有三个,分别是 A, B, C 组件, C 组件如何获取 A 或者 B 组件的数据。

<div id="itany">
  <my-b><my-a>
  <my-b><my-b>
  <my-c><my-c>
</div>

<template id ="a">
  <div>
    <h3>A组件: {{ name }}</h3>
    <button @click="send" >A组件 将数据发送给C组件</button>
  </div>
</template>
<template id ="b">
  <div>
    <h3>A组件: {{ age }}</h3>
    <button @click="send" >B组件 将数据发送给C组件</button>
  </div>
</template>
<template id="c">
  <div>
    <h3>C组件:{{ name }}, {{ age }}</h3>
  </div>
</template>

<script>
const Event = new Vue(); // 定义一个空的 Vue 实例

const A = {
  template: "#a",
  data() {
    return {
      name: "tom"
    };
  },
  methods: {
    send() {
      Event.$emit("data-a", this.name);
    }
  }
};

const B = {
  template: "#b",
  data() {
    return {
      age: 20
    };
  },
  methods: {
    send() {
      Event.$emit("data-b", this.ge);
    }
  }
};

const C = {
  template: "#c",
  data() {
    return {
      name: "",
      age: ""
    };
  },
  mounted() { // 在模版编译完成后执行
    Event.$on("data-a", name => {
      this.name = name; // 箭头函数内部不会产生l新的 this,这边如果不用 =>,this 指代 Event
    });

    Event.$on("data-b", age => {
      this.age = age;
    });
  }
};

const vm = new Vue({
  el: "#itany",
  components: {
    "my-a": A,
    "my-b": B,
    "my-c": C
  }ßß
});
</script>

$on 监听了自定义事件 data-adata-b,因为有时不确定何时会触发事件,一般会在 mountedcreated 钩子中来监听。

方法三、vuex

1.简要介绍 Vuex 原理

Vuex 实现了一个单项数据流,在全局拥有一个 State 存放数据,当组件要更改 State 中当数据时,必须通过 Mutation 进行, Mutation 同时提供了订阅者模式供外部插件调用获取 State 数据的更新。而当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走 Action,但 Action 也是无法直接修改 State 的,还是需要提供 Mutation 来修改 State 的数据。最后,根据 State 的变化,渲染到视图上。

2. 简要介绍各模块在流程中的功能
  • Vue ComponentsVue 组件HTML 页面上,负责收集用户操作等交互行为,执行 dispatch 方法触发对应 action 进行回应。

  • dispatch操作行为触发方法,是唯一能执行 action 的行为。

  • actions操作行为处理模块,由组件中的 $store.dispatch('action名称', detail) 来触发。然后由 commit() 来触发 mutation 的调用,间接更新 state。负责处理 Vue Components 接收到到所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台 API请求的操作就在这个模块中进行,包括触发其他 action 以及提交 mutation 的操作。该模块提供了 Promise 的封装,以支持 action 的链式触发。

  • commit状态改变提交操作方法,对 mutation 进行提交,是唯一能执行 mutation 的方法。

  • mutations状态改变操作方法,由 actions 中的 commit('mutation 名称') 来触发。是 Vuex 修改 state 的唯一推荐方法。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些 hook 暴露出来,以进行 state 的监控等。

  • state页面状态管理容器对象。集中存储 Vue componentsdata 对象的零散数据,全局唯一,以进行统一的状态管理。 页面显示所需的数据从该对象中进行读取,利用 Vue 的细粒度数据响应机制来进行高效的状态更新。

  • gettersstate 对象的读取方法Vue Components 通过该方法读取全局 state 对象。

3. VuexlocalStorage

vuexvue 的状态管理工具,存储的数据是响应式的。但是并不会保存起来,刷新之后就回到了初始状态,具体做法应该在 vuex 里数据改变的时候把数据拷贝一份保存到 localStorage 里面,刷新之后,如果 localStorage 里有保存的数据,取出来再替换 store 里的 state

let defaultCity = "上海";

try {
  if (!defaultCity) {
    defaultCity = JSON.parse(window.localStorage.getItem("defaultCity"));
  }
} catch (e) {
  export default new Vuex.Store({
    state: {
      city: defaultCity,
    },
    mutations: {
      changeCity(state, city) {
        state.city = city;
        try {
          window.localStorage.setItem(
            "defaultCity",
            JSON.stringify(state.city)
          );
        } catch (e) {}
      },
    },
  });
}

这里需要注意的是:由于 vuex 里,我们保存的状态都是数组,而 localStorage 只支持字符串,所以需要用 JSON 转换:

JSON.stringify(state.subscribeList); // array -> string
JSON.parse(window.localStorage.getItem("subscribeList"));

19、什么是作用域插槽

20、用vnode来描述一个数据结构

21、diff算法的时间复杂度

22、简述Vuediff算法原理

23、v-for中为什么要用key

key 的特殊 attribute 主要用在 Vue 的虚拟DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 keyVue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

24、描述组件渲染和更新过程

25、Vue中模版编译原理

26、Vue中常见性能优化

27、Vue中相同逻辑如何抽离

28、问什么要使用异步组件

29、谈谈你对keep-alive的了解

30、实现hash路由和history路由

31、Vue-Router中导航守卫有哪些

32、actionmutation的区别

33、简述Vuex工作原理

34、Vue3.0有哪些改进


 上一篇
总结 总结
1、diff 算法和虚拟 DOMdiff 算法特点: 1、同级比较,不会跨级别比较时间复杂度为 O(n) 2、在 diff 比较的过程中,循环从两边向中间靠拢 第一步: vue 的虚拟 DOM 首先会对新老 VNode 的开始位置和结束
2020-12-04
下一篇 
高频面试题 高频面试题
高频面试题[TOC] 一、HTML1、你都做过哪些兼容性问题?HTML 兼容性: h5 新标签只能兼容到 ie9,如果想要兼容 ie 低版本浏览器,需要引入 html5shiv.js 文件,其 cdn 写法如下: <script sr
2020-05-11
  目录