一、为什么 Vue 组件的 data 必须是函数?

  1. 数据隔离:组件是可复用的。如果 data 是对象,所有实例将共享同一个内存地址的数据。
  2. 闭包原理:通过函数返回对象,利用闭包特性,每次实例化时都会调用函数,生成独立的数据副本。
  3. 避免副作用:防止一个组件实例修改数据后,影响到其他完全不相关的实例。
const MyComponent = {
  data() {
    return {
      count: 0
    }
  }
}

// 每次实例化时,Vue 都会执行 data()
const instanceA = MyComponent.data();
const instanceB = MyComponent.data();

instanceA.count++;
console.log(instanceB.count); // 依然是 0,互不干扰

Vue 底层机制:Vue 在初始化组件时,如果发现 data 不是函数,会在控制台抛出警告,并停止后续的响应式处理。

二、Vue2中的过滤器了解吗?过滤器的应用场景有哪些?

在 Vue 2 中,过滤器 (Filters) 是一个非常实用的功能,主要用于在不改变原始数据的情况下,对文本进行 格式化处理。

不过需要提前说明的是:过滤器在 Vue 3 中已被正式废弃。Vue 官方建议使用计算属性(Computed)或全局方法来替代它。

v1. 过滤器的工作原理

过滤器本质上是一个 纯函数。它接收一个值作为输入,处理后返回一个新的值。它通常用在两个地方:双花括号插值 和 v-bind 表达式。

局部过滤器

filters: {
  capitalize: function (value) {
    if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}

全局过滤器

Vue.filter('currency', function (value) {
  return '$' + value.toFixed(2)
})

2. 过滤器的应用场景

过滤器最适合处理那些简单的、纯文本的格式转换,且这种转换逻辑在多个地方都会复用。

  • 货币格式化

    • 这是最经典的场景。将数字 1234.5 转换为 $ 1,234.50。

    • 用法:{{ price | currency }}

  • 时间戳格式化

    • 将后端返回的 Unix 时间戳(如 1712793600)格式化为可读的日期(如 2024-04-11)。

    • 用法:{{ timestamp | formatDate(‘YYYY-MM-DD’) }}

3. 过滤器的进阶特性

级联调用(串联)

过滤器可以像传送带一样,一个接一个地处理数据:

  • {{ message | filterA | filterB }}

  • 这里 message 的结果会传给 filterA,filterA 的处理结果再传给 filterB。

接收参数

  • 过滤器可以接收额外的参数:

  • {{ message | filterA(‘arg1’, arg2) }}

  • 注意:第一个参数始终是管道符 | 前面的表达式的值。

4. 为什么 Vue 3 废弃了过滤器?

Vue 3 追求更简洁的架构和更好的 TypeScript 支持,废弃过滤器的主要原因包括:

  • 功能重叠:过滤器能做到的,计算属性 (Computed) 和 普通方法 (Methods) 都能做到,且后者更符合 JavaScript 的编程直觉。

  • 强制性上下文缺失:过滤器内部拿不到 this(实例上下文),这限制了它访问组件内部数据的能力。

    • Vue 官方对过滤器的定位是 “纯粹的文本转换工具”。
    • 如果过滤器能访问 this,开发者就可能在过滤器里调用 this.doSomething() 修改组件状态,或者根据组件内复杂的动态状态来返回结果。这会打破过滤器的“纯粹性”,让模板的渲染逻辑变得难以预测。
  • TS 类型支持差:过滤器的参数类型推导非常麻烦。

三、Vue.mixin(混入)深度解析

Vue.mixin 是 Vue 2 中一种非常灵活的分发复用组件逻辑的方式。它允许你定义一套通用的选项(如 datamethods、生命周期钩子等),然后将其“混入”到一个或多个组件中。


1. 核心应用场景

当多个组件之间存在重复的逻辑代码时,mixin 是 Vue 2 中的首选工具:

  • ① 通用的工具方法:如多个页面都需要实现“点击复制”、“图片预览”或“格式化金额”的功能。
  • ② 公共的生命周期逻辑:如埋点统计(页面打开上报)、监听全局滚动事件、或在 destroyed 钩子中清理定时器。
  • ③ 表单校验逻辑:抽离重复的表单处理逻辑,如 validateresetForm 等。
  • ④ 权限控制:在 created 钩子中统一判断用户权限,实现跳转或提示拦截。

2. 核心原理:选项合并策略

Vue.mixin 的本质是 “合并(Merge)”。当调用混入时,Vue 会将混入对象与组件自身的选项按以下规则合并:

具体的合并规则:

选项类型 合并策略 冲突处理
数据对象 (data) 内部进行递归合并。 键名冲突时,以组件自身数据为准(组件优先)。
生命周期钩子 全部保留,并合并为一个数组。 混入对象钩子先执行,组件自身钩子后执行。
对象选项 (methods 等) 合并为同一个对象。 键名冲突时,以组件自身为准。
全局混入 Vue.mixin({ ... }) 影响之后创建的所有实例,包括第三方组件,需慎用。

3. 代码示例

① 定义混入 (logMixin.js)

export const logMixin = {
  data() {
    return { name: 'mixin' }
  },
  created() {
    console.log('Mixin: 组件已创建');
  },
  methods: {
    hello() {
      console.log('来自 Mixin 的问候');
    }
  }
}

② 组件引用

import { logMixin } from './logMixin'

export default {
  mixins: [logMixin], // 局部混入
  created() {
    console.log('Component: 自身钩子');
  }
}

/**
 * 控制台输出顺序:
 * 1. "Mixin: 组件已创建"
 * 2. "Component: 自身钩子"
 */

4. 为什么 Vue 3 弱化了 Mixin?

虽然 Mixin 解决了代码复用问题,但其设计缺陷在大型项目中非常明显,因此 Vue 3 推荐使用 Composition API (组合式 API) 彻底取代它。

Mixin 的三大致命缺点:

  • 命名冲突:多个混入或组件自身存在同名变量/方法时,会发生覆盖,导致难以察觉的 Bug。

  • 来源不明:在模板中调用 this.doSomething() 时,无法直观判断该方法来自哪个 Mixin,增加了维护成本。

  • 隐式依赖:多个 Mixin 之间可能存在逻辑依赖,导致代码耦合严重,难以独立重构。

四、Vue 面试题解析:深挖 Vue.observable

在 Vue 2.6+ 版本中,官方暴露了 Vue.observable 这个 API。它是响应式系统的核心能力外溢,允许开发者在不创建组件实例的情况下,构建响应式状态。


1. 什么是 Vue.observable?

Vue.observable(object) 用于将一个普通的 JavaScript 对象转换成响应式对象

  • 底层原理:它直接调用了 Vue 内部的 observe 方法。在 Vue 2 中,这意味着它使用 Object.defineProperty 递归地将对象的属性转化为 getter/setter,从而实现依赖收集与触发更新。
  • 返回值:返回的对象已经是“响应式”的了。当这个对象的属性在组件模板或计算属性中被使用时,Vue 会自动追踪它。

2. 核心应用场景:轻量级状态管理

在没有 Vue.observable 之前,跨组件共享状态通常依赖 Vuex(太重)或 Event Bus(逻辑乱)。Vue.observable 提供了一种极其简洁的跨组件通信方式。

A. 创建共享 Store (store.js)

import Vue from 'vue';

// 1. 创建响应式状态
export const store = Vue.observable({
  count: 0,
  user: { name: 'Gemini' }
});

// 2. 定义修改状态的方法(类似 Mutations)
export const mutations = {
  addCount() {
    store.count++;
  },
  setUserName(name) {
    store.user.name = name;
  }
};
<template>
  <div>
    <p>当前计数:{{ sharedCount }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

<script>
import { store, mutations } from './store.js';

export default {
  computed: {
    // 像访问本地 data 一样访问共享状态
    sharedCount() {
      return store.count;
    }
  },
  methods: {
    increment() {
      mutations.addCount();
    }
  }
};
</script>

3. Vue.observable vs. Vue 3 Reactive

Vue.observable 不仅仅是一个 API,更是理解 Vue 3 响应式设计的关键:

  • 传承关系:它实际上是 Vue 3 中 reactive API 的前身。Vue 3 将这种“对象响应式化”的能力进一步标准化,成为了组合式 API(Composition API)的核心。
  • 设计思想的转变
    • Vue 2 早期:响应式系统高度耦合在 new Vue() 实例中。
    • Vue.observable:证明了“状态”可以独立于“UI 渲染”存在。这种解耦的思想,直接启发了 Vue 3 的功能组织方式。

4. 面试避坑指南 (关键点)

在面试中若能提到以下细节,可以显著体现你对 Vue 源码的理解深度:

A. 底层局限性

尽管它使对象响应式化,但由于 Vue 2 依然基于 Object.defineProperty

  • 无法监测:直接给对象新增属性、删除属性,或者通过索引修改数组元素(如 arr[0] = 1)仍然无法触发视图更新。
  • 解决方法:在这些场景下,依然需要配合 Vue.set()splice() 等变异方法使用。

B. 引用一致性 (In-place Mutation)

这是一个非常微妙的点:

  • Vue.observable直接修改传入的原对象,并返回该对象。
  • 验证代码:
    const obj = { count: 0 };
    const obs = Vue.observable(obj);
    console.log(obj === obs); // true
    
    这意味着原对象已经被“注入”了 getter 和 setter。

C. 组件内的配合使用

在组件中使用外部的 observable 对象时,必须通过 computed 返回该对象的属性。

  • 原因:只有在计算属性或渲染函数中访问属性,Vue 的 Watcher 才能正确收集依赖。直接在 methods 中读取而不经过计算属性,可能会导致视图不更新。

5. 状态管理方案对比

维度 Vue.observable Vuex
复杂度 极轻量,无额外配置。 较重,需定义 State, Mutation, Action。
调试支持 弱。不支持 Vue Devtools 状态快照。 强。支持时间旅行(Time Travel)和状态追踪。
严谨性 弱。任何地方都能修改状态,难以维护。 强。严格要求通过 Mutation 修改,流程可预测。
适用场景 逻辑简单的全局配置、跨组件的小型 Store。 大型单页应用(SPA)、业务逻辑复杂的金融/电商项目。

6. 总结语

Vue.observable 是处理中小型项目组件库内部状态共享的神器。它规避了 Vuex 繁琐的样板代码,同时比 Event Bus 更加直观和易于数据追踪。

五、Vue 2 源码解析:new Vue() 过程中发生了什么?

执行 new Vue() 是整个框架生命的起点。这个过程本质上是将你传入的配置对象(options)转化为一个具备响应式能力、可挂载、可观察的实体的初始化过程


1. 初始化入口:_init 方法

当你调用 new Vue(options) 时,实际上执行的是 Vue 构造函数内部定义的 _init 方法。

// Vue 源码简略实现
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

2. 核心初始化阶段

_init 内部,Vue 按照严格的顺序执行了一系列初始化操作:

A. 合并配置 (Merge Options)

Vue 会将用户传入的 options 与全局的 Vue.options(如全局组件、指令、mixin)进行合并,生成最终的 vm.$options

B. 初始化核心子系统

  • initLifecycle: 初始化组件父子关系。设置 $parent$root$children 以及一些状态标识(如 _isMounted)。
  • initEvents: 初始化事件系统。处理父组件在当前组件上注册的事件(v-on)。

C. 触发生命周期:beforeCreate

注意:此时数据监测(Data Observation)和状态(State)还未开始初始化。因此在该钩子中无法访问 datamethods 等。

D. 状态初始化 (State Init)

这是 Vue 2 最核心的部分,按顺序初始化以下内容:

  • initInjections: 初始化 inject
  • initState:
    • initProps: 遍历 props,通过 defineReactive 设为响应式。
    • initMethods: 将 方法 挂载到实例上,并绑定 this
    • initData: 核心步骤。调用 observe(data),递归地使用 Object.defineProperty 将数据属性转化为 getter/setter。
    • initComputed: 创建计算属性的专用 Watcher
    • initWatch: 遍历 watch 配置,创建用户定义的 Watcher
  • initProvide: 初始化 provide

E. 触发生命周期:created

此时,数据响应式已经完成。你可以访问 data、修改属性,但 DOM 还没有生成,$el 属性目前不可见。


3. 挂载阶段:$mount

如果配置中提供了 el 选项,Vue 会自动调用 $mount 进入挂载流程:

  1. 编译模板 (Compile)
    • template 字符串编译成 render 函数。
    • 如果是运行时版本(Runtime-only),此步骤已在构建时由 vue-loader 完成。
  2. 触发生命周期:beforeMount
    • 模板已编译完成,虚拟 DOM 已创建,但尚未替换页面上的真实 DOM。
  3. 创建渲染 Watcher (Mount)
    • Vue 会实例化一个渲染 Watcher,它会持续监听数据变化并执行 updateComponent
    • _render: 执行渲染函数,生成虚拟 DOM(VNode)。
    • _update: 执行 Patch 算法(Diff),比对新旧 VNode,并将差异应用到真实 DOM 上。
  4. 触发生命周期:mounted
    • 真实 DOM 已经插入页面,可以通过 this.$el 访问真实的节点。

总结:初始化全流程表

阶段 关键操作 对应的生命周期
1. 初始化准备 合并 Options、初始化生命周期/事件 -
2. 注入前 此时实例已创建,但数据未挂载 beforeCreate
3. 状态注入 核心:初始化 Props/Data/Computed (响应式处理) -
4. 状态就绪 数据已可访问,异步请求通常在此发起 created
5. 编译挂载 模板编译、寻找挂载点 -
6. 渲染前 VNode 已经生成 beforeMount
7. DOM 生成 执行 Render & Patch,生成真实 DOM 树 -
8. 挂载完成 DOM 节点已进入页面,可进行 DOM 操作 mounted

更多推荐