高频前端面试题汇总之Vue篇 (上)

1,149 阅读26分钟

一、Vue 基础

1. Vue的基本原理

当一个Vue实例创建时,Vue会遍历data中的属性,用 Object.defineProperty将它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件都有相应的 watcher 实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而更新组件。 0_tB3MJCzh_cB6i3mS-1.png

2. 响应式原理

3. 双向数据绑定的原理

4. 使用 Object.defineProperty() 来进行数据劫持有什么缺点?

检测不到对象属性的添加和删除、数组 API 方法无法监听到,所以在 Vue2 中,增加了 set 、 delete 方法,并且对数组 api 方法进行重写。

5. MVVM、MVC、MVP的区别

MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化开发效率。

(1)MVC

其中 View 视图层,Model 负责业务逻辑、存储数据。当 Model 层发生改变的时候它会通知 View 层更新。当用户与页面产生交互的时候,Controller 通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。

image.png

(2)MVVM

  • Model 存储数据和业务逻辑
  • View 视图层
  • ViewModel 桥梁,监听Model中数据的改变并且控制视图的更新

Model和View并无直接关联,通过ViewModel进行联系,当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。

image.png

(3)MVP

MVP 模式与 MVC 唯一不同的在于 Presenter 和 Controller。在 MVC 模式中 View 层和 Model 层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,MVP 模式实现对 View 层和 Model 层的解耦。MVC 中的Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,MVP 模式中,Presenter 可以控制 Model 和 View,以此来实现 View 和 Model 的同步更新。

6. Computed 和 Watch 的区别

1)是否支持缓存
Computed支持缓存,只有依赖的数据发生了变化,才会重新计算
Watch不支持缓存,只要数据一变,它就会触发相应的操作

(2)是否支持异步
Computed不支持异步监听
Watch支持异步监听

(3)运用场景
当需要进行数值计算,并且依赖于其它数据时使用 computed
当需要执行异步或开销较大的操作时,应该使用 watch

7. slot 是什么?原理?

详细介绍参考:juejin.cn/post/715502…

原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。

8. 过滤器的作用,如何实现一个过滤器

过滤器是用来过滤数据的,在Vue中使用 filters 来过滤数据, filters 不会修改数据,而是过滤数据,改变用户看到的输出。

使用场景: 需要格式化数据的情况,比如需要处理时间、价格等数据

过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。过滤器用在插值表达式 {{ }}v-bind 表达式 中,然后放在操作符“ | ”后面进行指示。

例如,在显示金额,给商品价格添加单位:

<li>商品价格:{{item.price | filterPrice}}</li>

 filters: {
    filterPrice (price) {
      return price ? ('¥' + price) : '--'
    }
  }

9. 谈谈你对Vue的理解

Vue 是一个渐进式的JavaScript框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型。它有两个核心功能:数据驱动、组件化 。同时可以轻松引入 Vue 插件或其他的第三方库进行开发。

数据驱动:也就是 MVVM 模型, Model 表示数据模型层, view 表示视图层, ViewModel 是 View 和 Model 层的桥梁,数据绑定到 viewModel 层并自动渲染到页面中,视图变化通知 viewModel 层更新数据。

组件化:把一个页面分成多个模块,每个模块都可以看做是一个组件,或者把一些公共的模块抽离出来,方便复用,提高可维护性,降低整个系统的耦合度。

Vue 跟传统开发的区别:Vue 所有的界面事件,都是只去操作数据的,不直接操作DOM,而Jquery或者原生的 JS 是操作DOM的。

10. 常见的事件修饰符及其作用

  • .stop:阻止事件冒泡(等同于JS 中的 event.stopPropagation())
  • .prevent :阻止事件的默认行为,等同于 JS 中的 event.preventDefault()
  • .capture :使事件捕获由外到内
  • .self :只会触发自己范围内的事件,不包含子元素
  • .once :只会触发一次

写法如下:

image.png

11. Vue2中怎么使用TS语法?

(1)引入Typescript包

(2)vue.config.js中配置

(3)Vue组件的编写(装饰器写法)

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
@Component
export default class Test extends Vue {

};
</script>

12. 什么是函数式组件?

函数式组件,我们可以理解为没有内部状态,没有生命周期钩子函数,没有 this ,不需要实例化,结构比较简单,代码结构更清晰,所以渲染性能要好于普通组件。

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

  • 手段:v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display样式属性控制显隐
  • 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换
  • 编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show是在任何条件下,无论首次条件是否为真,都被编译
  • 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗
  • 使用场景:v-if适合运营条件不大可能改变;v-show适合频繁切换

14. v-model 是如何实现的,语法糖实际是什么?

v-model 本质上是一个 value 和 input 语法糖(Vue2),会对用户的输入做一些特殊处理以达到更新数据,而所谓的处理其实就是给使用的元素默认绑定属性和事件。

v-bind 绑定value属性的值
v-on 绑定input事件监听到函数中,函数会获取最新的值赋值到绑定的属性中

以 input 表单元素为例:

<input v-model='something'>

// 相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">

在自定义组件中:自己写 model 属性,里面放上 prop  和 event

<!-- 子组件 -->
<template>
  <div>
    <input
      type="text"
      :value="title"
      @input="$emit('change', $event.target.value)"
    />
  </div>
</template>

<script>
export default {
  model: {
    prop: "title", // 将默认的 prop 名 value 改为 title
    event: "change", // 将默认的事件名 input 改为 change
  },
  props: {
    title: String, // 注意 template 代码中也要修改为 title
  },
};
</script>

<!-- 父组件 -->
<my-input v-model="msg"></my-input>
// 等同于
<my-input :title="msg" @change="msg = $event"></my-input>

15. v-model 在Vue2 和 Vue3中有什么区别?

关于 Vue2 中的 v-model 可以看上一题,这里讨论Vue3 中的 v-model:

(1)修改默认 prop 名和事件名 当用在自定义组件上时, v-model 默认绑定的 prop 名从 value 变为 modelValue ,而事件名也从默认的 input 改为 update:modelValue 。

<!-- 父组件 -->
<my-input v-model="msg"></my-input>
// 等同于
<my-input :modelValue="msg" @update:modelValue="msg = $event"></my-input>

(2)废除 model 选项

Vue3 中移除了 model 选项,这样就不可以在组件内修改默认 prop 名了。现在有一种更简单的方式,就是直接在 v-model 后面传递要修改的 prop 名。

// 子组件

<template>
  <div>
    <input
      type="text"
      :value="title"
      @input="$emit('update:title', $event.target.value)"
    />
  </div>
</template>

<script>
export default {
  // 此时这里不需要 model 选项来修改了
  props: {
    title: String, // 修改为 title,注意 template 中也要修改
  },
};
</script>
<!-- 父组件 -->
// 要修改默认 prop 名,只需在 v-model 后面接上 :propName,例如修改为 title
<my-input v-model:title="msg"></my-input>
// 等同于
<my-input :title="msg" @update:title="msg = $event"></my-input>

(3)使用多个 v-model

例如有一个表单子组件,用户输入的多个数据都需要更新到父组件中显示:

<!--  子组件  -->

<template>
  <div class="form">
    <label for="name">姓名</label>
    <input id="name" type="text" :value="name" @input="$emit('update:name',$event.target.value)">
    
    <label for="address">地址</label>
    <input id="address" type="text" :value="address" @input="$emit('update:address',$event.target.value)">
  </div>
</template>

<script>
export default {
  props:{
    name: String,
    address: String
  }
}
</script>
<!-- 父组件 -->
<child-component v-model:name="name" v-model:address="address"></child-component>
    
// 将用户输入数据更新到父组件中显示
<p>{{name}}</p>
<p>{{address}}</p>

16. data为什么是一个函数而不是对象

JavaScript中的对象是引用类型的数据,当多个实例引用同一个对象时,只要一个实例对这个对象进行操作,其他实例中的数据也会发生变化。

而在Vue中,更多的是想要复用组件,那就需要每个组件都有自己的数据,这样组件之间才不会相互干扰。所以组件的数据不能写成对象的形式,而是要写成函数的形式,这样当每次复用组件的时候,就会返回一个新的data,也就是说每个组件都有自己的私有数据空间,它们各自维护自己的数据,不会干扰其他组件的正常运行。

17. MVVM 的优缺点?

优点:

  • 分离 View 层和 Model 层,降低代码耦合
  • 利⽤双向绑定,数据更新后视图⾃动更新,避免⼿动操作DOM

缺点:

  • 数据绑定使得⼀个位置的Bug被快速传递到别的位置,要定位原始出问题的地⽅就很难了。
  • ⼀个大的模块中model也会很⼤,花费更多的内存
  • 对于大型的图形应⽤程序,视图状态较多,ViewModel的构建和维护的成本都会⽐较⾼。

18. $nextTick 的原理及作用

请前往这篇文章:juejin.cn/post/715755…

19. Vue 中给 data 中的对象属性添加一个属性时会发生什么?如何解决?

<template> 
   <div>
      <ul>
         <li v-for="value in obj" :key="value"> {{value}} </li> 
      </ul> 
      <button @click="addObjB">添加 obj.b</button> 
   </div>
</template>

<script>
    export default { 
       data () { 
          return { 
              obj: { 
                  a: 'obj.a' 
              } 
          } 
       },
       methods: { 
          addObjB () { 
              this.obj.b = 'obj.b' 
              console.log(this.obj) 
          } 
      }
   }
</script>

点击 button 会发现,obj.b 已经成功添加,但是视图并未刷新。这是因为在Vue实例创建时,obj.b并未声明,因此就没有被Vue转换为响应式的属性,自然就不会触发视图的更新,这时就需要使用Vue的全局 api $set():

addObjB () (
   this.$set(this.obj, 'b', 'obj.b')
   console.log(this.obj)
}

$set() 方法相当于手动的去把一个属性变成响应式,此时视图也会跟着改变了。

20. Vue中封装的数组方法有哪些,其如何实现页面更新

在Vue中,对响应式处理利用的是Object.defineProperty对数据进行拦截,而这个方法并不能监听到数组内部变化,所以需要对数组的 API 进行重写。

image.png

那Vue是如何实现让这些数组方法实现元素的实时更新的呢,下面是Vue中对这些方法的封装:

// 缓存数组原型
const arrayProto = Array.prototype;
// 实现 arrayMethods.__proto__ === Array.prototype
export const arrayMethods = Object.create(arrayProto);
// 需要进行功能拓展的方法
const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse"
];

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function(method) {
  // 缓存原生数组方法
  const original = arrayProto[method];
  def(arrayMethods, method, function mutator(...args) {
    // 执行并缓存原生数组功能
    const result = original.apply(this, args);
    // 响应式处理
    const ob = this.__ob__;
    let inserted;
    switch (method) {
    // push、unshift会新增索引,所以要手动observer
      case "push":
      case "unshift":
        inserted = args;
        break;
      // splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。
      case "splice":
        inserted = args.slice(2);
        break;
    }
    // 
    if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听
    // notify change
    ob.dep.notify();// 通知依赖更新
    // 返回原生数组方法的执行结果
    return result;
  });
});

21. Vue 单页应用与多页应用的区别

概念:

  • SPA单页面应用,指只有一个主页面的应用,只需要在一开始加载一次js、css等相关资源即可。所有内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。
  • MPA多页面应用,指有多个独立页面的应用,每个页面必须重复加载js、css等相关资源。多页应用跳转,需要整页资源刷新。

区别: 775316ebb4c727f7c8771cc2c06e06dd.jpg

22. Vue 初始化页面闪动问题

在Vue初始化之前,由于div是不归Vue管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于{{message}}的字样,虽然一般情况下这个时间很短暂,但是还是有必要解决这个问题。在 CSS里加上以下代码:

[v-cloak] {    
    display: none;
}

23. Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?

不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环tick中,Vue 刷新队列并执行实际(已去重的)工作。

24. template 和 jsx 有什么区别?

template 和 jsx 的都是 render 函数的一种表现形式,不同的是:JSX具有更高的灵活性,在复杂的组件中,更具有优势,而 template 在代码结构上更符合视图与逻辑分离的习惯,更简单、更直观、更好维护。

在 webpack 中,使用 vue-loader 编译.vue文件,内部依赖的 vue-template-compiler 模块将template预编译成 render 函数。在添加了jsx的语法糖解析器 babel-plugin-transform-vue-jsx 之后,就可以直接手写render函数。

25. 描述下 Vue 自定义指令

在有的情况下,我们仍然需要手动操作 DOM 元素,这时候就会用到自定义指令。自定义指令是用来操作DOM的,尽管Vue推崇数据驱动视图的理念,但并非所有情况都适合数据驱动。自定义指令就是一种有效的补充和扩展,不仅可用于定义任何的DOM操作,并且是可复用的。

  • 1.全局定义:Vue.directive("focus",{})
  • 2.局部定义:directives:{focus:{}}
  • 3.钩子函数

image.png

  • 4.钩子函数参数

image.png

26. 子组件可以直接改变父组件的数据吗?

子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。Vue提倡单向数据流,即父级 props 的更新会流向子组件,但是反过来则不行。这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解,导致数据流混乱。

只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。

27. React 和 Vue 的异同

image.png

28. Vue 的优点

1.轻量级框架  2.简单易学  3.双向数据绑定  4.组件化  5.视图、数据分离  6.虚拟DOM

29. assets 和 static 的区别

相同点:存放静态资源文件

不同点: 
1.assets:走打包压缩流程,压缩后的文件会放置在static文件中跟着index.html一同上传至服务器
2.static:不走打包压缩等流程,而是直接进入打包好的目录,直接上传至服务器(打包效率高但体积大)

建议:
1.将项目中template需要的样式文件、js 文件等都可以放置在assets中
2.项目引入的第三方资源文件如iconfoont.css 等放置在static中(第三方文件已经被处理过了)

30. delete 和 Vue.delete 删除数组的区别

delete:删除一个数组中的元素,该元素会成为空值,数组长度不变
Vue.delete:会直接删除一个数组元素,长度会减少

31. 什么是 mixin ?

混入(mixin)提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能
一个混入对象可以包含任意组件选项(data、methods、mounted...)
当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项
当组件和混入对象含有同名选项时进行合并
数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先

详细请参考:juejin.cn/post/720729…

32. Vue 模版编译原理

Vue 中的模板 template 无法被浏览器解析并渲染,因为这不属于浏览器的标准,所以需要将 template 转化成一个 JavaScript 函数,这样浏览器就可以执行这一个函数并渲染出对应的 HTML 元素,这一个转化的过程,就称为模板编译。

模板编译又分三个阶段,解析parse,优化optimize,生成generate,最终生成可执行函数render。

  • 解析阶段:使用大量的正则表达式对 template 字符串进行解析,将标签、指令、属性等转化为抽象语法树 AST
  • 优化阶段:遍历 AST,找到其中的一些静态节点并进行标记,方便diff 比较时直接跳过这些静态节点,优化 runtime 的性能
  • 生成阶段:将最终的 AST 转化为 render 函数字符串

33. Vue 动画怎么实现的?

当 Vue 中,显示隐藏,创建移除,一个元素或者一个组件的时候,可以通过 transition实现动画。

  • 进入(显示,创建)
    • v-enter-from 进入前
    • v-enter-active 进入中
    • v-enter-to 进入后
  • 离开(隐藏,移除)
    • v-leave-from 进入前
    • v-leave-active 进入中
    • v-leave-to 进入后

两个步骤:

  1. 给要加动画的盒子,包裹一个 transition 标签
  2. 在动画类名中写样式

34. 对SSR的理解

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

SSR 的优势:1.更好的SEO  2.首屏加载速度更快

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

35. 对 SPA 单页面的理解,它的优缺点分别是什么?

SPA仅在页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换。

优点:
1.用户体验好、快,对服务器压力小
2.避免了不必要的跳转和重复渲染

缺点:
1.初次加载耗时多
2.不能使用浏览器的前进后退功能
3.SEO 上其有着天然的弱势

36. v-for 和 v-if 为什么不能连用

在 Vue2 中,v-for 的优先级比 v-if的优先级要高,而在 Vue3 中则相反 image.png

37. 说说 Vue 的内置指令

image.png

38. 组件中的name属性有什么用?

  1. 项目使用keep-alive时,可搭配组件name进行缓存过滤
  2. DOM做递归组件时需要调用自身 name
  3. Vue-devtools调试工具里显示的组见名称是由Vue中组件name决定的
  4. 动态切换组件

39. is这个特性你用过吗?有什么作用?

(1)解决html模板的限制

比如ul里面嵌套li的写法是html语法的固定写法,如果想在ul里面嵌套自己的组件,但是html在渲染dom的时候,组件对ul来说并不是有效的dom。

解决办法:

<ul>
  <li is='my-component'></li>
</ul>

(2)动态组件(组件切换)

componentName 可以是在本页面已经注册的局部组件名和全局组件名, 也可以是一个组件的选项对象。当控制 componentName改变时就可以动态切换选择组件。

<component :is="componentName"></component>

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

1.工厂模式 - 传入参数即可创建实例

2.单例模式 - 整个程序有且仅有一个实例

3.发布-订阅模式 (vue 事件机制)

4.观察者模式 (响应式数据原理)

5.装饰模式: (@装饰器的用法)

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

对象层级不要过深,否则性能就会差

不需要响应式的数据不要放到 data 中(可以用 Object.freeze() 冻结数据)

v-if 和 v-show 区分使用场景

computed 和 watch 区分使用场景

v-for 遍历必须加 key,key 最好是 id 值,且避免同时使用 v-if

大数据列表和表格性能优化-虚拟列表/虚拟表格

防止内部泄漏,组件销毁后把全局变量和事件销毁

图片懒加载

路由懒加载

第三方插件的按需引入

适当采用 keep-alive 缓存组件

防抖、节流运用

服务端渲染 SSR or 预渲染

42. 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;
}

43. Vue.set 方法原理

了解 Vue 响应式原理的同学都知道在以下的情况修改数据 Vue 是不会触发视图更新:在实例创建之后添加新的属性到实例上、删除属性

Vue.set 或者说是 $set 原理如下:因为响应式数据,我们给对象和数组本身都增加了__ob__属性,代表的是 Observer 实例。当给对象新增不存在的属性,首先会把新的属性进行响应式跟踪,然后会触发对象__ob__的 dep 收集到的 watcher 去更新

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;
}

44. 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) {
      this._init(options); //调用Vue初始化方法
    };
    Sub.cid = cid++;
    Sub.prototype = Object.create(this.prototype); // 子类原型指向父类
    Sub.prototype.constructor = Sub; //constructor指向自己
    Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的options
    return Sub;
  };
}

45. v-model 的修饰符有哪些

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

46. 谈谈你对 keep-alive 的了解?

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

  • 一般结合路由和动态组件一起使用,用于缓存组件;
  • 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
  • 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。

47. Vue中组件和插件有什么区别?

1. 组件是什么?
把一个页面分成多个模块,每个模块都可以看做是一个组件,或者把一些公共的模块抽离出来,方便复用
提高可维护性,降低整个系统的耦合度。

2. 插件是什么?
插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码,对Vue功能的增强和补充

3. 区别
(1)编写形式:   .vue文件(html、css、js代码)       ->      暴露一个install方法
(2)注册形式: 全局注册与局部注册(Vue.component)     ->       Vue.use()

48. scoped 样式隔离的原理

在当前组件的 .vue 文件中,如果 style 标签加了 scoped 属性,那么在组件渲染为 DOM 时,会对每个组件中的 DOM 元素添加格式为:data-v-[hash:8] 的属性,然后该组件的所有选择器也会添加上对应的[data-v-[hash:8]]属性选择器来只对自身组件产生影响,以此来实现样式隔离。

49. Vue2中如何解除数据绑定

解决方法:使用 JSON 对对象进行深拷贝

let formdata= JSON.parse(JSON.stringify(this.formValidate))

其实是通过 JSON 之间的解析,创建的临时变量,不会随 this.formValidate 的改变而改变。

50. 对组件化和模块化的理解

(1)模块化:是从代码逻辑角度进行划分的,保证每个模块的职能单一;比如登录页的登录功能,就是一个模块,注册功能又算一个模块。

(2)组件化:是从UI界面的角度划分的;页面上的每个独立的区域,都可以视为一个组件,前端组件化开发,便于UI组件的复用,减少编码量。

其实组件化是 UI 界面层面,模块化是代码逻辑层面。

(3)为什么要使用组件化和模块化?

1.开发和调试效率高:随着代码结构越发复杂,要修改某一个功能,可能要把所有相同的地方都修改一遍,浪费时间和人力;使用组件化,每个相同的功能结构都调用同一个组件,只需要修改这个组件。

2.可维护性强:便于后期代码查找和维护

3.避免阻断:模块化是可以独立运行的,如果一个模块产生了 bug,不会影响其他模块的调用。

4.版本管理更容易,如果由多人协作开发,可以避免代码覆盖和冲突。

51. Vue 的兼容性

Vue 支持所有兼容 ECMAScript5 的浏览器,因 IE8 不支持 ECMAScript5 特性,故 IE8 及其以下浏览器均不支持 Vue。

Vue 不支持 IE8 的官方解释:
当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用Object.defineProperty 把这些属性全部转为 getter/setter。 Object.defineProperty 是 ES5 中一个特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器的原因。

52. 发布者模式 / 订阅者模式

在软件架构中,发布订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。

这里很明显了,区别就在于,不同于观察者和被观察者,发布者和订阅者是互相不知道对方的存在的,发布者只需要把消息发送到订阅器里面,订阅者只管接受自己需要订阅的内容。

53. v-on 常用的修饰符

  • .stop - 调用 event.stopPropagation(),阻止默认事件
  • .prevent - 调用 event.preventDefault(),阻止默认行为
  • .native - 监听组件根元素的原生事件

54. v-if、v-show、v-html 的原理

v-if会调用 addIfCondition 方法,生成 vnode 的时候会忽略对应节点,render 的时候就不会渲染

v-show会生成 vnode,render 的时候也会渲染成真实节点,只是在 render 过程中会在节点的属性中修改show 属性值,也就是常说的 display

v-html会先移除节点下的所有节点,调用 html 方法,通过 addProp 添加 innerHTML 属性,归根结底还是设置 innerHTML 为 v-html 的值

55. v-model和.sync的对比

v-model 与.sync 的共同点:都是语法糖 ,都可以实现父子组件中的数据的双向通信

v-model与.sync的不同点:

v-model:

1.父组件:v-model="" 子组件:@(input,value)

2.一个组件只能绑定一个v-model

3.v-model针对更多的是最终操作结果,是双向绑定的结果,是value,是一种change操作

.sync:

1.父组件 :my-prop-name.sync 子组件 @update:my-prop-name 的模式来替代事件触发,实现父子组件间的双向绑定。

2.一个组件可以多个属性用.sync修饰符,可以同时双向绑定多个“prop”

3..sync 针对更多的是各种各样的状态,是状态的互相传递,是status

56. mixin 和 mixins 区别

mixin 是全局引用,mixin 是局部引用

全局引用:

image.png

image.png

局部引用:

image.png

57. 讲一下组件的命名规范

  • 给组件命名有两种方式(在Vue.Component/components时),一种是使用链式命名"my-component",一种是使用大驼峰命名"MyComponent",
  • 因为要遵循W3C规范中的自定义组件名 (字母全小写且必须包含一个连字符),避免和当前以及未来的 HTML 元素相冲突

58. Vue中如何扩展一个组件

  1. 常见的组件扩展方法有:mixins,slots,extends等
  2. 混入mixins是分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。
  3. 插槽主要用于vue组件中的内容分发,也可以用于组件扩展。如果要精确分发到不同位置可以使用具名插槽,如果要使用子组件中的数据可以使用作用域插槽。
  4. 组件选项中还有一个不太常用的选项extends,也可以起到扩展组件的目的

59. 如果让你从零开始写一个Vue路由,说说你的思路

一个SPA应用的路由需要解决的问题是页面跳转内容改变同时不刷新,同时路由还需要以插件形式存在,所以:

  1. 首先我会定义一个createRouter函数,返回路由器实例,实例内部做几件事:

    • 保存用户传入的配置项
    • 监听hash或者popstate事件
    • 回调里根据path匹配对应路由
  2. 将router定义成一个Vue插件,即实现install方法,内部做两件事:

    • 实现两个全局组件:router-link和router-view,分别实现页面跳转和内容显示
    • 定义两个全局变量:route和route和route和router,组件内可以访问当前路由和路由器实例

60. 说说在Vue中踩过的坑,是怎么解决的?

1. 给对象添加属性或者数组通过下标修改值的时候,直接通过给data里面的对象添加属性然后赋值,新添加的属性不是响应式的。

【解决办法】通过Vue.set(对象,属性,值)这种方式就可以达到,对象新添加的属性是响应式的。

2. 在created操作dom的时候,是报错的,获取不到dom,这个时候Vue实例没有挂载

【解决办法】使用 Vue.nextTick , 或者在 mounted 钩子里获取 dom

3. 父组件调用子组件的方法,发送请求,修改子组件数据 ,子组件的视图没有更新。由于Vue的DOM操作是异步的,修改数据的时候子组件的DOM还没生成,this.$refs获取不到。

【解决办法】通过:Vue.nextTick() , 在nextTick里面去发送请求修改数据。

61. Vue2对比原生js有什么优缺点?

优点:1. 使用虚拟dom,避免手动操作Dom 2. 视图、数据、结构分离 3. 实现数据的双向绑定 4. 组件化、模块化开发 5. 提供了各种指令,功能更加强大 6. MVVM模型,数据驱动视图

缺点:Vue是单页面页面,对于搜索引擎不友好,影响SEO

62. v-for 遍历对象

大多数人在使用 v-for 的时候 ,是用来遍历数组的,那么 v-for 是否能够用来遍历对象呢 ?

(1)获取一个值 如果只是获取一个值,那么获取得到的是 value 。 image.png

运行结果: image.png

(2)获取两个值 获取 key 和 value ,格式:(value,key)

image.png

运行结果:

image.png

(3)获取三个值 获取 key ,value ,index 格式: (value,key,index)

image.png

运行结果:

image.png

63. Vue为什么要进行异步渲染

简单来说就是为了提升性能,因为不采用异步更新,在每次更新数据都会对当前组件进行重新渲染,为了性能考虑,Vue会在本轮数据更新后,再去异步更新视图。

64. 父组件调用子组件的方法

1.子组件使用ref,父组件直接调用

<child ref="mychild"></child>  
this.$refs.mychild.say("嘿嘿嘿")

2.子组件注册监听事件,父组件调用$emit触发

// 父组件
<child ref="mychild"></child>  
this.$refs.mychild.$emit('childMethod','嘿嘿嘿')  
  
//子组件 
this.$on('childMethod', (res) => {  
    console.log('触发监听事件监听成功') 
})

65. 子组件调用父组件的方法

1、子组件中通过 this.$parent.event 来调用父组件的方法

// 父组件
<template>
  <div>
    <child></child>
  </div>
</template>
<script>
  import child from './components/childcomponent';
  export default {
    components: {
      child
    },
    methods: {
      fatherMethod() {
        console.log('父组件方法');
      }
    }
  };
</script>

// 子组件
<template>
  <div>
    <button @click="childMethod()">点击按钮</button>
  </div>
</template>
<script>
  export default {
    methods: {
      childMethod() {
        this.$parent.fatherMethod();
      }
    }
  };
</script>

2、子组件用 $emit 向父组件触发一个事件,父组件监听这个事件

// 父组件
<template>
  <div>
    <child @fatherMethod="fatherMethod"></child>
  </div>
</template>
<script>
  import child from './components/childcomponent'
  export default {
    components: {
      child
    },
    methods: {
      fatherMethod() {
        console.log('父组件方法');
      }
    }
  };
</script>

// 子组件
<template>
  <div>
    <button @click="childMethod()">点击按钮</button>
  </div>
</template>
<script>
  export default {
    methods: {
      childMethod() {
        this.$emit('fatherMethod');
      }
    }
  };
</script>

3、父组件把方法作为属性传入子组件中,在子组件里直接调用这个方法

// 父组件
<template>
  <div>
    <child :fatherMethod="fatherMethod"></child>
  </div>
</template>
<script>
  import child from './components/childcomponent';
  export default {
    components: {
      child
    },
    methods: {
      fatherMethod() {
        console.log('父组件方法');
      }
    }
  };
</script>

// 子组件
<template>
  <div>
    <button @click="childMethod()">点击按钮</button>
  </div>
</template>
<script>
  export default {
    props: {
      fatherMethod: {
        type: Function,
        default: null
      }
    },
    methods: {
      childMethod() {
          this.fatherMethod();
        }
      }
    }
  };
</script>

66. Vue 2.7

Vue2.7 支持你的项目在不升级 Vue3 的情况下使用 Vue3 的特性,例如Composition ApisetuprefonMount等,也能摆脱Vue实例( this )的束缚;与此同时,Vue2.7 也是 Vue2.X 的最终次要版本,在这个版本之后,Vue2将进入 LTS(长期支持)。对于一些老项目来说,当升级 Vue3 成本过大而你又垂涎 Vue3 新的 api 和代码组织方式时,那Vue2.7 无疑是最佳选择,这可以让还在使用 Vue2 的同学更好的学习并过渡到 Vue3。

67. 封装一个弹窗组件(全局调用 this.$message)

68. Vue 事件绑定原理

Vue 的事件绑定分为两种,一种是直接绑定在 dom 元素上的原生事件,采用的是 addEventListener 实现;另一种是绑定在组件上的事件,通过 Vue 自带的on实现,需要子元素 $emit触发,如果组件事件加了.native修饰符的则等价于原生事件。

原生的事件绑定:Vue 在创建 dom 时会调用 createEle,默认调用 invokeCreateHooks,针对事件会调用updateDOMListeners,其中就有 add 方法,核心使用 addEventListener 绑在 dom 上。

组件的事件绑定:组件实例化 -> 获取到父给子绑定的自定义事件 -> 调用 updateListeners(传入 add 方法,核心使用 $on)

image.png

二、生命周期

1. 说一下Vue的生命周期

Vue 实例有⼀个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom、渲染、更新、卸载 等⼀系列过程,称这是Vue的生命周期。

  1. beforeCreate(创建前) :数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是不能访问到 data、methods 上的方法和数据。
  2. created(创建后) :实例创建完成,data、methods 等都配置完成,但是此时渲染的节点还未挂载到 DOM,所以不能访问到 $el 属性。
  3. beforeMount(挂载前) :在挂载开始之前被调用。此时已生成 html,但还没有挂载到页面上。
  4. mounted(挂载后) :在 el 被挂载到实例上之后调用。实例已完成以下的配置:用编译好的html内容替换el属性指向的DOM对象,html 已挂载到页面上。
  5. beforeUpdate(更新前) :响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染。
  6. updated(更新后) :在由于数据更改导致的虚拟DOM重新渲染之后调用。此时 DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。
  7. beforeDestroy(销毁前) :实例销毁之前调用,此时实例仍然完全可用。
  8. destroyed(销毁后) :实例销毁后调用,调用后 Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

2. Vue 子组件和父组件执行顺序

加载渲染过程:

  1. 父组件 beforeCreate
  2. 父组件 created
  3. 父组件 beforeMount
  4. 子组件 beforeCreate
  5. 子组件 created
  6. 子组件 beforeMount
  7. 子组件 mounted
  8. 父组件 mounted

更新过程:

  1. 父组件 beforeUpdate
  2. 子组件 beforeUpdate
  3. 子组件 updated
  4. 父组件 updated

销毁过程:

  1. 父组件 beforeDestroy
  2. 子组件 beforeDestroy
  3. 子组件 destroyed
  4. 父组件 destoryed

3. created和mounted的区别

两者都可以访问实例上的 data 和 props,区别如下:

created: 此时渲染的节点还未挂载到 DOM,所以不能访问到$el属性
mounted: 挂载完毕,可以访问到$el属性

4. 一般在哪个生命周期请求异步数据

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

推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面加载时间,用户体验更好;
  • SSR不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性。

5. keep-alive 中的生命周期有哪些

keep-alive是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染DOM。

如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。

当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件、触发 activated钩子函数。

6. 父组件可以监听到子组件的生命周期吗?

比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:

//  Parent.vue
<Child @hook:mounted="doSomething" ></Child>

doSomething() {
   console.log('父组件监听到 mounted 钩子函数 ...');
},
    
//  Child.vue
mounted(){
   console.log('子组件触发 mounted 钩子函数 ...');
},    
    
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...     

7. 第一次加载页面会触发哪几个钩子函数?

  • beforeCreate
  • created
  • beforeMount
  • mounted

三、组件通信

(1) props  /   $emit

父组件向子组件传值:

// 父组件
<template>
    <div id="father">
        <son :msg="msgData" :fn="myFunction"></son>
    </div>
</template>

<script>
import son from "./son.vue";
export default {
    name: father,
    data() {
        msgData: "父组件数据";
    },
    methods: {
        myFunction() {
            console.log("vue");
        }
    },
    components: {
        son
    }
};
</script>
// 子组件
<template>
    <div id="son">
        <p>{{msg}}</p>
        <button @click="fn">按钮</button>
    </div>
</template>
<script>
export default {
    name: "son",
    props: ["msg", "fn"]
};
</script>

子组件向父组件传值:

// 父组件
<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) {
      this.$emit('onEmitIndex', index) // 触发父组件的方法,并传递参数index
    }
  }
}
</script>

(2)eventBus事件总线($emit / $on

创建事件中心管理组件之间的通信:

// event-bus.js

import Vue from 'vue'
export const EventBus = new Vue()

在 firstCom 组件中发送事件:

<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>

在 secondCom 组件中接收事件:

<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)依赖注入(provide / inject)

该方法用于父子(子孙)组件之间的通信。 provide / inject 是 Vue 提供的两个钩子,和 data 、 methods 是同级的。

  • provide 钩子用来发送数据或方法
  • inject 钩子用来接收数据或方法

在父组件中:

provide() { 
    return {     
        num: this.num  
    };
}

在子组件中:

inject: ['num']

还可以这样写,这样写就可以访问父组件中的所有属性:

provide() {
 return {
    app: this
  };
}
data() {
 return {
    num: 1
  };
}

inject: ['app']
console.log(this.app.num)

注意: 依赖注入所提供的属性是非响应式

(4)ref / $refs

这种方式可以实现父子组件之间的通信。

子组件:

export default {
  data () {
    return {
      name: 'JavaScript'
    }
  },
  methods: {
    sayHello () {
      console.log('hello')
    }
  }
}

在父组件中:

<template>
  <child ref="child"></component-a>
</template>
<script>
  import child from './child.vue'
  export default {
    components: { child },
    mounted () {
      console.log(this.$refs.child.name);  // JavaScript
      this.$refs.child.sayHello();  // hello
    }
  }
</script>

(5)parent / children

在子组件中:

<template>
  <div>
    <span>{{message}}</span>
    <p>获取父组件的值为:  {{parentVal}}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Vue'
    }
  },
  computed:{
    parentVal(){
      return this.$parent.msg;
    }
  }
}
</script>

在父组件中:

// 父组件中
<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: 'Welcome'
    }
  },
  methods: {
    change() {
      // 获取到子组件
      this.$children[0].message = 'JavaScript'
    }
  }
}
</script>

需要注意:

  • 通过 parent 访问到的是上一级父组件的实例,可以使用 $root 来访问根组件
  • 在组件中使用 $children 拿到的是所有的子组件的实例,它是一个无序数组
  • children 的值是数组,而 $parent 是个对象

(6)attrs / listeners

考虑一种场景,如果A是B组件的父组件,B是C组件的父组件。如果想要组件A给组件C传递数据,这种隔代的数据,该使用哪种方式呢?

如果是用 props/$emit 来一级一级的传递,确实可以完成,但是比较复杂;如果使用事件总线,在多人开发或者项目较大的时候,维护起来很麻烦;如果使用Vuex,的确也可以,但是如果仅仅是传递数据,那可能就有点浪费了。

针对上述情况,Vue引入了 attrs/attrs / listeners ,实现组件之间的跨代通信。

先来看一下 inheritAttrs ,它的默认值true,继承所有的父组件属性除 props 之外的所有属性; inheritAttrs:false 只继承class属性 。

  • attrs :继承所有的父组件属性(除了prop传递的属性、class 和 style ),一般用在子组件的子元素上
  • listeners :该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。

A组件( APP.vue ):

<template>
    <div id="app">
        //此处监听了两个事件,可以在B组件或者C组件中直接触发 
        <child1 :p-child1="child1" :p-child2="child2" @test1="onTest1" @test2="onTest2"></child1>
    </div>
</template>
<script>
import Child1 from './Child1.vue';
export default {
    components: { Child1 },
    methods: {
        onTest1() {
            console.log('test1 running');
        },
        onTest2() {
            console.log('test2 running');
        }
    }
};
</script>

B组件( Child1.vue ):

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

C 组件 (Child2.vue):

<template>
    <div class="child-2">
        <p>props: {{pChild2}}</p>
        <p>$attrs: {{$attrs}}</p>
    </div>
</template>
<script>
export default {
    props: ['pChild2'],
    inheritAttrs: false,
    mounted() {
        this.$emit('test2');// 触发APP.vue中的test2方法
    }
};
</script>

(7)Vuex