Vue2 面试题 (2023-09-22更新)

基础

Vue2.0 兼容 ie 哪个版本?

不支持 ie8,部分兼容 ie9,完全兼容 ie10
因为 vue 的响应式原理是基于 es5 的 Object.defineProperty 这个方法不支持 ie8 及以下

解释下 MVVM 模式?

MVVM 是 Model-View-ViewModel 的缩写

model 数据层:处理业务逻辑和数据
view 视图层:UI 视图,负责数据的展示
viewModel 负责监听 model 中数据的改变并控制视图的更新,处理用户操作

Vue 的优点?缺点?

优点:

  • 数据驱动 (自动计算属性和追踪依赖的表达式)
  • 组件化 (可以复用,解耦的组件来构建页面)
  • 轻量(代码量小,不依赖其他库,大小只有几十 kb)
  • 渐进式
  • 虚拟 DOM
  • 单页面路由
  • 数据与视图分离

缺点:

  • 单页面不利于 seo
  • 不支持 ie8 及以下
  • 首屏加载时间长

Vue 的两个核心?

  • 数据驱动
  • 组件化

数据

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

解释:
当组件中的 data 使用对象时,因为对象是引用类型,所有组件实例的 data 都会指向同一个内存地址。在其中一个组件中修改数据,其他组件的数据也会发生变化
而使用返回对象的函数,每次返回一个新的对象,函数返回的对象内存地址并不相同。则不会存在这个问题

下面用代码举例说明:
我们模仿一个组件构造函数,定义 data 属性,采用对象的形式

function Component() {}

Component.prototype.data = {
  count: 0
}

创建两个组件实例:

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

修改componentA组件data属性的值,componentB中的值也会发生变化

console.log(componentB.data.count) // 0
componentA.data.count = 1
console.log(componentB.data.count) // 1

通过代码我们能看出:vue 组件就是一个 vue 实例,通过构造函数来创建。每个实例都会继承原型上的方法属性。data就是 vue 原型上的一个属性,当是对象的时候,每个实例的 data 就会指向同一个内存地址,每个组件之间的数据就会相互影响

如果我们采用函数的形式,则不会出现这种问题(函数返回的对象内存地址并不相同)

function Component() {
  this.data = this.data()
}
Component.prototype.data = function () {
  return {
    count: 0
  }
}

修改componentA组件data属性的值,componentB中的值不会受影响

console.log(componentB.data.count) // 0
componentA.data.count = 1
console.log(componentB.data.count) // 0

动态给 data 添加一个属性会发生什么?怎么解决?

现象: 数据会更新,但视图不会更新
原因: 动态添加的属性没有通过 Object.defineProperty 设置成响应式数据

data 中哪些对象操作无法监听?为什么?怎么解决?

  • 对象属性的添加、删除

原因: 因为 vue 的响应式原理是通过 Object.defineProperty 来实现的。只能劫持到对属性的获取或修改,无法追踪新增和删除属性。

解决方法:

  • 属性添加可以使用 $set
this.$set(this.obj, 'key', newValue)
  • 属性删除可以使用 $delete
this.$delete(obj, key)

data 中哪些数组操作无法监听?为什么?怎么解决?

  • 利用索引直接设置一个数组项时:this.arr[inx] = 1
  • 直接修改数组的长度:this.arr.length = 10

原因:Vue 没有对数组的属性进行劫持

解决办法:

  • this.arr.splice(inx, 1, 新数据)
  • this.$set(this.arr, inx, 新数据)

怎么重置数据?

  1. 重置整个 data
Object.assign(this.$data, this.$options.data())
// this.$data 用于获取Vue实例的data选项
// this.$options 用于获取Vue实例的初始化选项
  1. 重置单个对象数据(比如重置 form 表单数据)
Object.assign(this.formData, this.$options.data().formData)

不需要响应式的数据应该怎么处理?

  1. 使用 Object.freeze
data() {
  return {
    list: Object.freeze([{count: 1}])
  }
}
  1. 在 data 外定义
data(){
  this.list = [{count: 1}]
  return {}
}

watch 和 computed 的区别?

  • watch 适合一个数据影响多个数据。当需要在数据变化时:执行异步操作或开销较大的操作时(搜索)
  • computed 适合一个数据受多个数据影响。是基于他的响应式依赖进行缓存的,只有相关响应式依赖属性发生变化才会重新计算(计算价格)

watch 怎么初始化的时候被立即调用?

在选项参数中添加 immediate: true 将立即以表达式的当前值触发回调

watch: {
  count: {
    handler(newVal, oldVal) {
      // ...
    },
    immediate: true
  }
}

watch 怎么深度监听对象变化?

在选项参数中添加 deep: true

watch: {
  count: {
    handler(newVal, oldVal){
      // ...
    },
    deep: true
  }
}

computed 中的属性名和 data 中的属性名可以相同么?

不能同名,因为 computed 属性、data 属性、props 属性都会被挂载 vm 实例上

if (key in vm.$data) {
  warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
  warn(`The computed property "${key}" is already defined as a prop.`, vm)
}

data 属性名和 methods 的属性名可以相同么?

不能同名,有限制,会报错

let data = vm.$options.data
const keys = Object.keys(data)
const methods = vm.$options.methods
let i = keys.length
while (i--) {
  const key = keys[i]
  if (process.env.NODE_ENV !== 'production') {
    if (methods && hasOwn(methods, key)) {
      warn(`Method "${key}" has already been defined as a data property.`, vm)
    }
  }
}

指令

列举常用的指令有哪些?

v-forv-ifv-showv-bind(:)v-on(@)v-modelv-elsev-else-ifv-htmlv-textv-once

列举表单修饰符和事件修饰符

事件修饰符:

  • .stop 阻止事件冒泡
  • .prevent 阻止默认事件(a 标签、浏览器鼠标事件、form 表单提交)
  • .capture 事件默认是冒泡,capture 作用是捕获
  • .self 只有绑定点击事件的本身才会触发事件
  • .once 监听一个自定义事件,只会触发一次,一旦触发后,监听器就会被移除
  • .native 加在自定义组件的事件上,保证事件能执行(解决给组件绑定自定义事件无效)
  • .sync父组件传值加上这个修饰符,子组件可直接修改传过来的值

表单修饰符:

  • .lazy 改变输入框的值时 value 不会改变,当失去焦点的时候。v-model 绑定的值 value 才会改变
  • .trim 去掉 v-model 绑定的值首尾空格
  • .number 将值转换成数字(先输入字符串和先输入数字,是两种情况,先输入数字的话,只取前面数字部分,先输入字母的话,number 修饰符无效)

v-if 和 v-show 的区别?

  • 相同点:都可以控制元素的显示和隐藏

  • 不同点

    • v-if 是通过创建和销毁元素来达到显示隐藏
    • v-show 是通过cssdisplay属性来达到显示隐藏
  • 性能消耗
    v-if 有更高的切换消耗,v-show有更高的初始化渲染消耗,频繁显示隐藏用v-show

v-if 和 v-for 的优先级是什么?

两者作用于同一标签时,v-for 的优先级比 v-if 高。
尽量不要把 v-ifv-for 使用同一个元素上,会带来性能方面的浪费(每次渲染都会先循环再判断)。可以使用 computed 进行过滤

v-for 遍历,是按照什么顺序遍历的?如果保证顺序?

按照 Object.keys() 的顺序遍历
转成数组保证顺序

v-model 的实现原理?

v-model 是分别利用了 v-bind 绑定 value 的值,v-on 绑定 input 事件,通过$event 进行赋值

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

v-once 有什么用?

只渲染元素和组件一次。下次渲染,会被视为静态内容并跳过

内置组件

谈谈你对 keep-alive 的了解?

含义:keep-alive是一个抽象组件,他自身不会渲染成一个 DOM 元素,也不会出现在父子组件中
作用:使用 keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁他们
属性介绍:includeexcludemax

  • include:缓存白名单,包含的组件会被缓存
  • exclude:缓存黑名单,包含的组件不会被缓存
  • max:最多可以缓存多少组件实例,一旦达到这个数字,在新实例被创建前,已缓存组件中最久没有被访问的实例会被销毁掉

keep-alive的基本用法:

<keep-alive>
  <component :is="view"></component>
</keep-alive>

使用includesexclude

<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>

<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>

<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>

keep-alive 的原理?

keep-alive 是一个组件,组件有三个属性:includeexcludemax
在其内部维护了一个 key 数组和一个缓存对象:key数组记录目前缓存的组件key值,如果组件没有指定key值,会自动生成一个唯一的key值。缓存对象 cache ,会以key值为键,vnode 为值,用于缓存组件对应的虚拟 DOM
mounted中会监听includeexclude属性,进行组件的缓存处理,如果发生变化会动态的添加和删除属性
keep-alive的渲染函数中,会判断当前渲染的vnode是否有对应的缓存,如果有会从缓存中读取对应的组件实例,如果没有就会把他缓存
当缓存的数量超过max设置的数值时,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉

为什么 keep-alive,不会渲染成真正的 DOM 节点

vue 底层中有一个 abstract 属性,会根据该属性决定是否忽略某个组件
keep-alive 中设置了 abstract:true 表示该组件为抽象组件,不会被渲染 DOM,跳过该组件实例

跟 keep-alive 相关的生命周期有哪些?

  • activated keep-alive组件激活时调用
  • deactivated keep-alive组件停用时调用

以上钩子服务端渲染期间不被调用

组件通信

组件通信都有什么方式?

  • props$emit
  • $parent$children$refs
  • providereject
  • $attrs$listeners
  • envetBus
  • vuex

父子组件通信?

props 和 $emit

  • 父传子:父组件在子组件标签上绑定数据,子组件设置 props 来接收
  • 子传父:子组件通过 $emit 触发自定义事件,第二个参数为传递的数据,父组件绑定自定义事件来监听传过来的数据

prop 验证的 type 类型有哪几种?

StringNumberBooleanArrayObjectDateFunctionSymbol

子组件怎么直接修改父组件传过来的数据?

父组件在传值的时候加上.sync,子组件在自定义事件加上update:

// 父组件
<children :count.sync="count"/>

// 子组件
this.$emit('update:count')

依赖注入怎么使用?解决了什么问题?

provide 和 inject

作用:解决了嵌套组件的通信问题

使用:

  • 在祖先组件定义 provide 属性,返回向下传递的值
  • 在后代组件中通过 inject 接收传过来的值

注意:传递的值需要是对象才能实现响应式,如果是基本类型就无法实现响应式

// 父组件
export default {
  provide() {
    return {
      count: 1
    }
  }
}

// 子组件
export default {
  inject: ['count']
}

生命周期

Vue 生命周期有哪些?

  • beforeCreate Vue 实例创建前:数据初始化还没有完成,像 data、props 等属性还无法访问
  • created Vue 实例创建后:完成了数据观测(data、props、computed 等),属性和方法(method)的运算。完成了数据代理
  • beforeMount 实例挂载前:生成了 Watcher,调用 render 函数,生成虚拟 DOM,但是还没转换成真实 DOM
  • mounted 实例挂载后:真实 DOM 挂载完毕
  • beforeUpdate 数据更新前调用:新的虚拟 DOM 生成,但是还没有对比打补丁
  • updated 数据更新后调用:新旧虚拟 DOM 对比打补丁,进行真实 DOM 的更新
  • beforeDestory 实例销毁前调用:此时实例仍然完全可用,可以访问数据
  • destoryed 实例销毁之后调用:此时实例的所有东西都会解绑,所有的事件监听器都会被移除,所有的子实例也会被销毁

父子组件 渲染 生命周期顺序

beforeCreate → 父created → 父beforeMount → 子beforeCreate → 子created → 子beforeMount → 子mounted → 父mounted

父子组件 更新 生命周期顺序

beforeUpdate → 子beforeUpdate → 子updated → 父updated

父子组件 销毁 生命周期顺序

beforeDestory → 子beforeDestory → 子destoryed → 父destoryed

在 created 和 mounted,这两个生命周期中请求数据有什么区别?

created中,页面视图未出现,如果请求信息过多,会阻塞渲染,页面会长时间处于白屏状态
mounted中,页面已经渲染完成,则不会有这种情况

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

beforeCreate、 created、 beforeMount、 mounted

DOM 渲染是在那个生命周期完成的?

在 mounted 生命周期

内置 API

$nextTick 有什么作用?

$nextTick中的回调延迟到下次 DOM 更新循环结束之后执行。
Vue 采用的是异步更新策略,同一事件循环内,多次修改,会统一进行一次视图更新,所以数据一更新,视图还未更新,所以拿到的还是上一次的旧数据

$nextTick 的实现原理?

$nextTick 主要使用异步队列来控制
Vue 数据响应过程:数据更新 → 通知 Watcher → 更新 DOM,其中的数据更新 DOM 是个微任务
$nextTick 就是将回调函数添加到了更新 DOMmicroTask 队列中,确保代码在 DOM 更新后执行

根据浏览器的渲染机制,渲染线程是在微任务执行完成之后执行的,渲染线程还没运行,怎么获取到 dom 的呢?

渲染线程只是把 DOM 树渲染成 UI 而已。
Vue 更新 DOM 之后,在 DOM 树里,新的 DOM 已经存在,就可以拿到新的 DOM 了

属性

key 的作用?

给元素添加了标识,优化复用对比策略,提高渲染性能
Vue 为了高效渲染元素,默认会复用已有元素而不是重新渲染。为元素添加 key,当列表数据发生变化,会根据 key 找到已渲染的元素进行复用,对顺序不匹配的元素进行位置的调整
如果不使用 key,默认采用“就地更新”策略,如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素

CSS

为什么 style 上加 scoped 能实现样式私有化?

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

dom 上的 data-v-xxxxx 是做什么的?(和上面问题一样,只是问法不一样)

这是在 Vue 中,写 css 时使用 scoped 标记产生的,保证了各个组件中的 css 不相互影响,给每个组件都做了唯一标记

深度选择器

  • >>>

只能作用于 css,使用了预处理语言的 css 不生效

<style>
  >>> .el-input {
    width: 100px;
  }
</style>
  • /deep/ (只能在 vue2 中使用)

只能作用于 less 或者 sass

<style lang="less">
  /deep/ .el-input {
    width: 100px;
  }
</style>
  • ::v-deep (目前 vue2 和 vue3 都可使用)

只能作用于 less 或者 sass

<style lang="less">
  ::v-deep .el-input {
    width: 100px;
  }
</style>
  • :v-deep() (vue3 中使用)
<style lang="less">
  :v-deep(.el-input) {
    width: 100px;
  }
</style>

怎么动态绑定 class?

  • 对象语法
<div :class="{'active': flag}"></div>

<script>
  data(){
    return {
      flag: true
    }
  }
</script>
  • 数组语法(三元表达式)
<div :class="[flag ? 'active' : 'default']"></div>

<script>
  data(){
    return {
      flag: true
    }
  }
</script>
  • 对象和计算属性直接返回
<div :class="createClass"></div>

<script>
  data(){
    return {
      flag: true
    }
  },
  computed: {
    createClass(){
      return {
        active: this.flag
      }
    }
  }
</script>

原理

配置 proxy 为什么能解决跨域问题?

跨域问题是存在于浏览器端,服务器与服务器之间的请求不存在跨域问题
配置 proxy 相当于在本地创建了虚拟服务器,使用虚拟服务器去代理请求

Vue 数据响应式原理?

Vue2 采用数据劫持结合发布订阅的方式,通过 Object.defineProperty 劫持属性,在数据变化时,发布消息给订阅者,触发相应的监听回调

Vue 实例初始化的时候,会对数据进行递归遍历,通过 Object.defineProperty 对数据属性进行代理。
这样做的作用是:

  1. 当渲染视图的时候,会先编译 Vue 中的 template 模板,解析模板指令,将模板中的变量替换成数据,这样就会触发 getter 进行依赖收集,依赖收集是指将订阅者 Watcher 存放到订阅器 Dep 里面
  2. 当数据发生变化时,会触发 setter,setter 会通知订阅器 Dep,订阅器会循环遍历之前收集到的 Watcher,告诉他们自己的值改变了,需要重新渲染视图,这时候 Watcher 就会调用自身的 update 来更新视图

进阶

为什么 Vue 中 data 更新后,不能立即获取到 DOM?

受异步更新策略影响,Vue 在修改数据后,视图不会立即更新,而是等同一事件循环的所有数据变更完成之后,在统一进行视图更新

讲一下 Vue 实例挂载过程?

说说 Vue 模板的编译过程?

Vue 模板编译(Vue template compilation)是指将 template 模板转换为可执行的 JavaScript 代码的过程
Vue 模板编译分为以下几个阶段:

  1. 解析代码(Parsing):这个阶段主要是解析器(Parser)解析模板中的 HTML 标签、文本内容、指令等,生成抽象语法树(AST Tree)
  2. 优化阶段:遍历 AST 树,找出其中的静态节点,打上标记,(作用是:diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch(对比打补丁)的性能)
  3. 生成代码(Code Generation):将 AST Tree 转化为可执行的 JavaScript 代码。代码生成器遍历 AST Tree 生成对应的渲染字符串和渲染函数,这个渲染函数主要是在实例挂载和 DOM 更新时被调用
  4. 运行时(RunTime):当 Vue 实例被创建并挂载到 DOM 上时,渲染函数会被调用,生成对应 DOM 结构。当数据发生变化时,渲染函数会重新执行,更新 DOM,在此过程中,Vue 会使用虚拟 DOM 技术来减少实际的 DOM 操作次数,提高性能

watch、methods 中的属性和方法,可以使用箭头函数定义么?

不可以,this 会是 undefined
所有的生命周期钩子,内置方法,自动绑定 this 上下文到实例中,因此可以访问数据,对属性和方法的运算。
箭头函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向 Vue 实例

复用性、扩展、组合

组件和插件的区别?

1. 编写方式的不同

  • 组件编写:最常见的就是 vue 单文件的这种格式,每个.vue 文件都可以看成是一个组件
  • 插件编写:会暴露一个 install 方法,这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象

2. 注册方式的不同

  • 组件注册:分为全局注册和局部注册,通过 Vue.component(component)
  • 插件注册:通过 Vue.use(plugin)

3. 使用场景

  • 组件 Component:解耦页面,拆分和封装页面公共的业务模块
  • 插件 Plugin:扩展 Vue 的功能

谈谈你对自定义指令的理解?

指令本质是装饰器,是 Vue 对 HTML 元素的扩展,增加自定义功能

自定义指令五个生命周期:

  • bind 只调用一次,指定一个绑定到元素时调用
  • inserted 被绑定元素插入父节点时调用
  • update 被绑定元素所在的模块更新时调用
  • componentUpdated 被绑定元素所在模块完成一次更新时调用
  • unbind 只调用一次,指令与元素解绑时调用

混入(mixins)

  1. 局部混入
const user = {
  data() {
    return {
      userName: 'Jane'
    }
  }
}

export default {
  mixins: [user]
}
  1. 全局混入
    2.1. 新建 mixin.js

    import Vue from 'vue'
    
    Vue.mixin({
      data() {
        return {
          userName: 'Sasa'
        }
      }
    })
    

    2.2 在 main.js 中引用

    import '@/mixin/mixin.js'
    

混入(mixins)的作用?有什么需要注意的地方?

作用:用来抽离公共的数据和方法,提高代码复用性

需要注意如下:

  • 同名函数合并为数组,都会执行,先执行 mixin 中的同名函数
  • 同名数组会进行替换,组件内的数据会将 mixin 的进行覆盖
  • 应该尽量只在页面的父组件(包含所有页面的组件)中使用混合。尽量不通过使用Vue.mixin()全局设置,通过这种方式,会为所有组件导入它,混入的方法会被执行多次

mixin 可以定义公用的变量和方法,mixin 中的数据和方法是独立的,每个组件中的 mixin 实例都是不一样的,不存在相互影响

其他

属性书写顺序

export default {
  name: '',

  mixins: [],

  components: {},

  props: {},

  data() {},

  computed: {},

  watch: {},

  created() {},

  mounted() {},

  destroyed() {},

  methods: {}
}

Vue 和 jquery 的区别?

  • jquery 是直接操作 DOM。Vue 数据和视图分离,不直接操作 DOM,只需要操作数据即可
  • jquery 操作的是真实 DOM,操作行为也很频繁。Vue 利用虚拟 DOM 技术,提高了更新 DOM 的性能
  • Vue 集成的一些库,比如 Vuex、Vue-Router 等,可以提高开发效率

assets 和 static 的区别?

相同点:两者都存放静态资源文件的
不同点:assets 存放的文件,在打包的时候,会进行打包压缩体积。
statis 存放的文件不会进行打包压缩,而是存入指定的目录,因为跳过了这一流程,在打包时会提高一定效率,但因此也会占用服务器更大空间

name 选项有什么作用?

  • 递归组件。调用自身时使用
  • iscomponent 时使用
  • keep-alive 中,includeexclude 使用

怎么强制刷新组件?

  • this.$forceUpdate()
  • 组件加上 key,变化 key 的值

组件在什么时候会被销毁

  • 没有使用keep-alive,进行路由切换
  • v-if = "fasle"
  • this.$destory()

Vue 组件里写的原生 addEventListener 事件,需要手动销毁么?为什么?需要注意什么?

需要在 beforeDestory() 中销毁,不然会造成多次绑定和内存泄露
注意事项:

  • 执行函数必须使用外部函数

    window.addEventListener('resize', this.onResize)
    
  • 执行函数不能是匿名函数

    window.addEventListener('resize', () => {
      this.onResize
    })
    
  • 执行函数不能改变 this 指向

    window.addEventListener('resize', this.onResize(this))
    
  • addEventListener 和 removeEventListener 第三个参数必须一致

    window.addEventListener('resize', this.onResize, true)
    
    window.removeEventListener('resize', this.onResize, true)
    

在 Vue 事件中是如何使用 event 对象的?

@click="onClick($event)" 事件参数中传入 event 对象

在 Vue 事件中传入$event,使用 $event.target 和 $event.currentTarget 有什么区别?

  • $event.currentTarget 指向事件所绑定的元素
  • $event.target 指向事件触发的元素

什么是虚拟 DOM?

虚拟 DOM 是对真实 DOM 的抽象,用 JavaScript 对象作为一个容器,用对象的属性来描述节点,最终通过一系列操作来映射到真实 DOM

为什么需要虚拟 DOM?

操作真实 DOM 是很慢的,哪怕一个最简单的 div 也会包含很多属性

操作 DOM 的代价是昂贵的,频繁操作还是会出现页面卡顿,影响用户的体验

举例
比如用 jquery,操作的都是真实 DOM
当在一次操作时,需要更新 10 个 DOM 节点,浏览器没那么智能,收到第一个更新 DOM 请求后,会马上执行流程,最终会执行 10 次
而通过 VNode,同样更新 10 个 DOM 节点,虚拟 DOM 不会立即操作 DOM,而是通过 diff算法比较需要更新的地方,将这 10 次更新的内容保存到本地的一个 js 对象中,最终将这个 js 对象一次性更新到 DOM 树上,避免了大量的无用计算





posted @ 2019-06-26 09:27  时光凉忆  阅读(761)  评论(0编辑  收藏  举报