Vue面试问题不完全整理

22 阅读3分钟

MVVM模式

即Model-View-ViewModel模式,模型-视图-视图模型。
模型指后端的数据,视图指看到的页面(DOM),但是二者无法直接通信。视图模型使用双向数据绑定,观察到数据的变化对视图内容进行更新;监听到视图的变化通知数据发生改变。就这样将视图和模型连接起来。vue.js就是视图模型层的实现者。

Object.defineProperty和Proxy

Vue实现响应式更新的底层原理就是Object.defineProperty(Vue2)和Proxy(Vue3)。
Vue2使用的Object.defineProperty是es5的方法,用于修改对象的某个属性的特征(或创建新属性),并对属性的读写操作进行监听拦截(get和set),并返回此对象。
三个参数分别是要监听的对象,要监听的属性,要做的修改。

const obj = {};
Object.defineProperty(obj, 'name', {
  get() {
    console.log('读取 name 属性');
    return this._name;
  },
  set(value) {
    console.log('设置 name 属性:', value);
    this._name = value;
  },
  enumerable: true,   // 是否可枚举
  configurable: true  // 是否可删除或重新配置
});

obj.name = 'Alice';  // 输出: "设置 name 属性: Alice"
console.log(obj.name); // 输出: "读取 name 属性" → "Alice"

但是它只能实现对单个属性的拦截,如果需要对多个属性进行拦截就需要遍历操作,如果存在嵌套对象的情况还需要进行递归深层监听,有性能问题;而且因为只能代理对象上已经定义的属性,所以对象属性的添加或删除和数组的push和pop都无法被劫持。


而Vue3则使用了es6的新特性Proxy。Proxy意为代理,new一个Proxy对某个对象进行代理,可以很高效地对整个对象的所有属性进行监听拦截,包括数组和新增删除的属性,并返回一个新对象。而且Proxy有很多的拦截方法,不止有get和set 。

const target = { name: 'Alice' };

const proxy = new Proxy(target, {
  // 拦截属性读取
  get(target, key, receiver) {
    console.log(`读取属性: ${key}`);
    // 使用 Reflect.get 实现默认读取行为
    return Reflect.get(target, key, receiver);
  },

  // 拦截属性设置
  set(target, key, value, receiver) {
    console.log(`设置属性: ${key} = ${value}`);
    // 使用 Reflect.set 实现默认设置行为,并返回操作结果(布尔值)
    return Reflect.set(target, key, value, receiver);
  }
});

proxy.name = 'Bob'; // 输出: "设置属性: name = Bob"
console.log(proxy.name); // 输出: "读取属性: name"   "Bob"

Vue2和Vue3有什么区别

  1. 性能优化
    • Vue3引入静态树与静态属性提升,在编译时识别不会改变的Dom与属性,将静态节点移动到render函数外部进行缓存,减少渲染开销
    • Diff算法,Vue3使用的经过优化的单向遍历,跳过不需要更新的节点,Vue2是双向指针遍历
    • Vue3支持Fragment,允许组件返回多个根节点,无需额外的包装
    • 事件监听优化,Vue3事件监听器是懒加载的,触发时才绑定;Vue2是挂载立即绑定
    • 更好的TS支持,模块化设计按需引入
  2. Vue3是Composition API,根据逻辑功能组织代码,可读性和复用性更高;Vue2是Options API(data、computed、methods、watch),想复用要用mixin函数
  3. 响应式系统:Vue3使用Proxy和Reflect实现响应性,即reactive方法,可监听属性的增删改和数组的变化;Vue2则依赖Object.defineProperty,只能监听指定对象的指定属性的getter和setter行为。
  4. 生命周期钩子:
    • Vue3的setup函数代替了Vue2的beforeCreate、created
    • beforeMounte,Mounted => onBeforeMount(组件挂载前render调用),onMounted(组件挂载时)
    • beforeDestroy,destroyed => onBeforeUnmount(组件卸载前),onUnmounted(组件卸载后)
    • updated、activated什么的也是在前面加个on

computed、watch、watchEffect区别

  • computed:计算属性,有缓存无异步注重计算后return返回的结果,适用于多个数据影响一个数据
  • watch:监听,指明要监听的数据源和回调,可以访问改变前和改变后的值,没缓存有异步,适用于一个数据影响多个数据
  • watchEffect:副作用函数,可以自动监听数据源作为依赖,回调中用到哪个数据就监听哪个数据,注重的是回调函数的过程,也就是函数体

v-if和v-for优先级

在 Vue2 中 v-for 的优先级更高,但是在 Vue3 中优先级改变了。v-if 的优先级更高。

v-for为什么设置key

帮助 Vue 高效跟踪节点身份,在数据变化时复用元素,减少 DOM 操作。否则会采用就地复用策略,影响性能。

组件间通信方式

  • props:父传子,父组件中使用子组件的时候添加属性,子组件中使用props接收属性
  • emit:子传父,子组件中声明自定义事件$emit(事件,数据),父组件中使用子组件的地方添加事件获取子组件传来的值
  • v-model:父子之间双向绑定,本质是一个名为 modelValue 的 prop+一个名为 update:modelValue 的事件
  • ref:父组件使用子组件的时候设置ref,拿到子组件暴露的数据。
  • provide+inject:依赖注入,用于嵌套层级比较深的后代组件拿到祖先组件的数据
  • vuex/pinia:全局状态管理工具,用于复杂关系的组件传输数据
  • evenbus:事件总线,可用于任意组件之间传值,需要使用数据的订阅者组件通过on注册事件(监听)到调度中心,传出数据的发布者组件通过on注册事件(监听)到调度中心,传出数据的发布者组件通过emit发布事件(触发)到调度中心
// 创建一个中央时间总线类  
class Bus {  
  constructor() {  
    this.callbacks = {};   // 存放事件的名字  
  }  
  $on(name, fn) {  
    this.callbacks[name] = this.callbacks[name] || [];  
    this.callbacks[name].push(fn);  
  }  
  $emit(name, args) {  
    if (this.callbacks[name]) {  
      this.callbacks[name].forEach((cb) => cb(args));  
    }  
  }  
}  
  
// main.js  
Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上  
// 另一种方式  
Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能  

// 组件1
this.$bus.$emit('foo')  
// 组件2
this.$bus.$on('foo', this.handle)

nextTick

vue更新视图是异步进行的,数据发生变化时会开启一个异步更新队列,vue会等队列中所有数据变化完成之后统一更新视图。如果想要实时获得最新的dom,可以在修改数据后手动调用nextTick。参数是回调函数和执行函数上下文,返回值是一个promise对象(可以用async/await)。在 DOM 更新后执行回调,确保操作基于最新 DOM 状态。

虚拟DOM

虚拟dom是一个能描述dom结构及其属性信息的js对象(VNode),本质上是js和真实dom之间的映射。dom操作太昂贵了,当dom操作比较频繁时,框架会把前后的虚拟dom树进行对比(diff),找出需要更新的地方,把补丁一次性打到需要更新的真实dom树上,完成视图更新。可以避免重复的渲染计算,提高性能。且虚拟dom抽象渲染过程,可以实现跨平台。但当操作dom的数量比较少的时候,虚拟dom因为添加了更多的计算操作,反而效率会低。

未完待续