文章目录
- 一、为什么 Vue 组件的 data 必须是函数?
- 二、Vue2中的过滤器了解吗?过滤器的应用场景有哪些?
- v1. 过滤器的工作原理
- 2. 过滤器的应用场景
- 3. 过滤器的进阶特性
- 级联调用(串联)
- 接收参数
- 4. 为什么 Vue 3 废弃了过滤器?
- 三、Vue.mixin(混入)深度解析
- 1. 核心应用场景
- 2. 核心原理:选项合并策略
- 3. 代码示例
- ① 定义混入 (`logMixin.js`)
- ② 组件引用
- 4. 为什么 Vue 3 弱化了 Mixin?
- 四、Vue 面试题解析:深挖 Vue.observable
- 1. 什么是 Vue.observable?
- 2. 核心应用场景:轻量级状态管理
- A. 创建共享 Store (`store.js`)
- 3. Vue.observable vs. Vue 3 Reactive
- 4. 面试避坑指南 (关键点)
- A. 底层局限性
- B. 引用一致性 (In-place Mutation)
- C. 组件内的配合使用
- 5. 状态管理方案对比
- 6. 总结语
- 五、Vue 2 源码解析:new Vue() 过程中发生了什么?
- 1. 初始化入口:`_init` 方法
- 2. 核心初始化阶段
- A. 合并配置 (Merge Options)
- B. 初始化核心子系统
- C. 触发生命周期:`beforeCreate`
- D. 状态初始化 (State Init)
- E. 触发生命周期:`created`
- 3. 挂载阶段:`$mount`
- 总结:初始化全流程表
一、为什么 Vue 组件的 data 必须是函数?
- 数据隔离:组件是可复用的。如果
data是对象,所有实例将共享同一个内存地址的数据。 - 闭包原理:通过函数返回对象,利用闭包特性,每次实例化时都会调用函数,生成独立的数据副本。
- 避免副作用:防止一个组件实例修改数据后,影响到其他完全不相关的实例。
constMyComponent={data(){return{count:0}}}// 每次实例化时,Vue 都会执行 data()constinstanceA=MyComponent.data();constinstanceB=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()returnvalue.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 中一种非常灵活的分发复用组件逻辑的方式。它允许你定义一套通用的选项(如data、methods、生命周期钩子等),然后将其“混入”到一个或多个组件中。
1. 核心应用场景
当多个组件之间存在重复的逻辑代码时,mixin是 Vue 2 中的首选工具:
- ① 通用的工具方法:如多个页面都需要实现“点击复制”、“图片预览”或“格式化金额”的功能。
- ② 公共的生命周期逻辑:如埋点统计(页面打开上报)、监听全局滚动事件、或在
destroyed钩子中清理定时器。 - ③ 表单校验逻辑:抽离重复的表单处理逻辑,如
validate、resetForm等。 - ④ 权限控制:在
created钩子中统一判断用户权限,实现跳转或提示拦截。
2. 核心原理:选项合并策略
Vue.mixin的本质是“合并(Merge)”。当调用混入时,Vue 会将混入对象与组件自身的选项按以下规则合并:
具体的合并规则:
| 选项类型 | 合并策略 | 冲突处理 |
|---|---|---|
数据对象 (data) | 内部进行递归合并。 | 键名冲突时,以组件自身数据为准(组件优先)。 |
| 生命周期钩子 | 全部保留,并合并为一个数组。 | 混入对象钩子先执行,组件自身钩子后执行。 |
对象选项 (methods等) | 合并为同一个对象。 | 键名冲突时,以组件自身为准。 |
| 全局混入 | Vue.mixin({ ... }) | 影响之后创建的所有实例,包括第三方组件,需慎用。 |
3. 代码示例
① 定义混入 (logMixin.js)
exportconstlogMixin={data(){return{name:'mixin'}},created(){console.log('Mixin: 组件已创建');},methods:{hello(){console.log('来自 Mixin 的问候');}}}② 组件引用
import{logMixin}from'./logMixin'exportdefault{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)
importVuefrom'vue';// 1. 创建响应式状态exportconststore=Vue.observable({count:0,user:{name:'Gemini'}});// 2. 定义修改状态的方法(类似 Mutations)exportconstmutations={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';exportdefault{computed:{// 像访问本地 data 一样访问共享状态sharedCount(){returnstore.count;}},methods:{increment(){mutations.addCount();}}};</script>3. Vue.observable vs. Vue 3 Reactive
Vue.observable不仅仅是一个 API,更是理解 Vue 3 响应式设计的关键:
- 传承关系:它实际上是 Vue 3 中
reactiveAPI 的前身。Vue 3 将这种“对象响应式化”的能力进一步标准化,成为了组合式 API(Composition API)的核心。 - 设计思想的转变:
- Vue 2 早期:响应式系统高度耦合在
new Vue()实例中。 - Vue.observable:证明了“状态”可以独立于“UI 渲染”存在。这种解耦的思想,直接启发了 Vue 3 的功能组织方式。
- Vue 2 早期:响应式系统高度耦合在
4. 面试避坑指南 (关键点)
在面试中若能提到以下细节,可以显著体现你对 Vue 源码的理解深度:
A. 底层局限性
尽管它使对象响应式化,但由于 Vue 2 依然基于Object.defineProperty:
- 无法监测:直接给对象新增属性、删除属性,或者通过索引修改数组元素(如
arr[0] = 1)仍然无法触发视图更新。 - 解决方法:在这些场景下,依然需要配合
Vue.set()或splice()等变异方法使用。
B. 引用一致性 (In-place Mutation)
这是一个非常微妙的点:
Vue.observable会直接修改传入的原对象,并返回该对象。- 验证代码:
这意味着原对象已经被“注入”了 getter 和 setter。constobj={count:0};constobs=Vue.observable(obj);console.log(obj===obs);// true
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 源码简略实现functionVue(options){if(process.env.NODE_ENV!=='production'&&!(thisinstanceofVue)){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)还未开始初始化。因此在该钩子中无法访问
data、methods等。
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进入挂载流程:
- 编译模板 (Compile):
- 将
template字符串编译成render函数。 - 如果是运行时版本(Runtime-only),此步骤已在构建时由
vue-loader完成。
- 将
- 触发生命周期:
beforeMount:- 模板已编译完成,虚拟 DOM 已创建,但尚未替换页面上的真实 DOM。
- 创建渲染 Watcher (Mount):
- Vue 会实例化一个渲染 Watcher,它会持续监听数据变化并执行
updateComponent。 - _render: 执行渲染函数,生成虚拟 DOM(VNode)。
- _update: 执行Patch 算法(Diff),比对新旧 VNode,并将差异应用到真实 DOM 上。
- Vue 会实例化一个渲染 Watcher,它会持续监听数据变化并执行
- 触发生命周期:
mounted:- 真实 DOM 已经插入页面,可以通过
this.$el访问真实的节点。
- 真实 DOM 已经插入页面,可以通过
总结:初始化全流程表
| 阶段 | 关键操作 | 对应的生命周期 |
|---|---|---|
| 1. 初始化准备 | 合并 Options、初始化生命周期/事件 | - |
| 2. 注入前 | 此时实例已创建,但数据未挂载 | beforeCreate |
| 3. 状态注入 | 核心:初始化 Props/Data/Computed (响应式处理) | - |
| 4. 状态就绪 | 数据已可访问,异步请求通常在此发起 | created |
| 5. 编译挂载 | 模板编译、寻找挂载点 | - |
| 6. 渲染前 | VNode 已经生成 | beforeMount |
| 7. DOM 生成 | 执行 Render & Patch,生成真实 DOM 树 | - |
| 8. 挂载完成 | DOM 节点已进入页面,可进行 DOM 操作 | mounted |