Vue基本原理
vue2.x
-
数据劫持
-
创建实例的时候,遍历data对象的每个属性
-
使用Object.defineProperty将这些属性转成 getter/setter。
-
访问属性时,会收集依赖(哪个组件或者watch使用了这个属性)
-
修改时,触发setter,通知依赖更新组件。
-
-
依赖追踪
-
Vue 内部使用 Dep(依赖收集器) 和 Watcher(观察者)。
-
每个组件实例对应一个 watcher,用来监听组件渲染过程中用到的属性。
-
当属性更新时,会触发 watcher 的更新方法,重新渲染组件。
-
-
渲染流程
-
创建组件 → 初始化 data。
-
遍历 data → 为每个属性添加 getter/setter。
-
渲染组件 → 在渲染过程中访问属性 → getter 收集 watcher 依赖。
-
更新数据 → setter 被触发 → Dep 通知对应 watcher → watcher 重新渲染组件。
-
缺点:对数组和对象的新增属性无法检测(Vue 2 通过 Vue.set/$set 解决)。对象属性必须在初始化时存在才能被响应式追踪。
Vue 3.x
-
Vue 3 使用Proxy替代了Object.defineProperty
-
Proxy 可以直接监听对象的读写操作,包括新增、删除属性。
-
解决了 Vue 2 中无法检测新增属性和数组索引更新的问题。
-
-
响应式系统核心是 Reactive Effect:
-
访问属性 → track(依赖收集)。
-
修改属性 → trigger(触发更新)。
-
-
结构上:
-
reactive():将对象转为响应式对象。
-
ref():为基本类型(Number、String 等)创建响应式对象。
-
effect():类似 watcher,包裹渲染或计算逻辑,依赖追踪和触发更新。
-
Vue 双向数据绑定原理
Vue 的双向数据绑定(Data Binding)核心是 数据劫持 + 发布-订阅模式(Observer + Dep + Watcher),实现数据与视图的自动同步。
核心概念
Observer(观察者)
负责监听数据对象(data)。使用 Object.defineProperty 将每个属性转为 getter/setter。当属性被访问时收集依赖(Dep)。当属性被修改时通知依赖(Watcher)。
Compile(模板解析器)
解析模板中的指令(如 v-model、v-bind)。将模板中的变量替换为实际数据。为每个绑定节点注册更新函数(绑定 DOM 更新)。
Watcher(订阅者)
负责将 Observer 和 Compile 连接起来。在初始化时:将自己添加到属性的依赖列表(Dep)中。提供 update() 方法,在数据变化时更新对应的视图。当 Dep 通知变化时,Watcher 调用 update 并触发 Compile 中绑定的回调。
双向绑定工作流程
Vue 在初始化时通过数据劫持给 data 中的属性添加 getter 和 setter,在模板编译阶段为使用到这些数据的 DOM 节点创建 Watcher 并注册更新回调;当数据发生变化时,setter 通知 Watcher 执行回调从而更新视图,而当用户在表单元素中输入内容时,v-model 通过事件监听把视图变化同步到数据中,最终形成数据变化自动更新视图、视图变化又能反向修改数据的双向绑定效果。
使用 Object.defineProperty() 来进行数据劫持有什么缺点?
Object.defineProperty 无法监听对象属性的新增和数组索引变化,且初始化成本高,而 Proxy 能完整拦截所有数据操作,因此 Vue 3 用 Proxy 取代了它。
MVVM、MVC、MVP的区别
MVC、MVP 和 MVVM 实际上都是为了解决应用程序中界面(UI)和逻辑(Logic)耦合过重的问题,通过分离关注点来提高代码的可维护性。
最早是 MVC。在 MVC 中,View 可以直接访问 Model,导致两者耦合度较高。用户交互由 Controller 处理,Model 变更后通知 View 更新。
后来演化出了 MVP。MVP 的核心是将 View 做成'哑视图'(Passive View),它完全不了解 Model。所有的逻辑和交互都由中间的 Presenter 层来协调。这样实现了 View 和 Model 的完全解耦,方便做单元测试。
而 MVVM 是目前前端最主流的模式,尤其是在 Vue 和 React 等框架兴起后。 它的核心在于 ViewModel 和 双向数据绑定(Data Binding)。
在 MVVM 中,ViewModel 充当了 View 和 Model 之间的自动化桥梁。开发者不需要手动去操作 DOM(比如像以前用 jQuery 那样),只需要关注数据的变化。
-
当 Model 数据变了,ViewModel 会自动更新视图;
-
当视图上的表单数据变了,ViewModel 也会自动同步到 Model。
这种'数据驱动视图'的模式,极大地提高了前端开发的效率。
所以,总结一下它们的区别:
-
MVC 是单向通信为主,View 和 Model 存在耦合。
-
MVP 通过 Presenter 实现了 View 和 Model 的完全解耦,手动控制更新。
-
MVVM 则是引入了自动化的绑定机制,解决了 MVP 中繁琐的视图更新代码,让我们从繁琐的 DOM 操作中解放出来。"
Computed 和 Watch 的区别
Computed 和 Watch 的核心区别在于:Computed 侧重于'数据的派生'并具有缓存性,而 Watch 侧重于'副作用'(Side Effects)的处理。
-
缓存机制(Cache): Computed: 支持缓存。只有当其依赖的响应式数据发生变化时,才会重新计算。如果数据没变,它会直接返回之前的计算结果,性能更好。
Watch: 不支持缓存。只要监听的数据变化,回调函数就会立即执行。
-
异步支持(Asynchronous): Computed: 不支持异步。因为它必须同步返回一个值用于模板渲染,如果在内部进行异步操作(如
setTimeout或axios),它无法获取异步结果作为返回值。Watch: 支持异步。这是 Watch 最大的优势,可以在回调中执行异步操作(比如请求 API、设置定时器)。
-
数据流向(多对一 vs 一对多): Computed: 通常是 "多对一"。比如
Fullname依赖于Firstname和Lastname,多个数据的变化影响一个属性。Watch: 通常是 "一对多"。一个数据变化,可能触发一系列操作(比如修改其他数据、打印日志、发请求)。
在实际开发中:如果我需要根据现有的数据(data/props)计算出一个新值来渲染视图,比如购物车总价计算、列表过滤。如果我需要在数据变化时执行'副作用',比如搜索框输入后调用搜索接口、或者数据变化后操作 DOM。
Computed 和 Methods 的区别
Computed 和 Methods 在使用上最大的区别在于:Computed 是具有响应式缓存的,而 Methods 是没有缓存的。
Computed 依赖于响应式数据。只有当它依赖的属性(比如 message)发生改变时,它才会重新求值。如果依赖没变,无论页面重渲染多少次,它都直接返回缓存的结果,不会重复执行函数。Methods 则是单纯的函数调用。只要触发了页面的重渲染(比如页面上另一个不相关的 timer 更新了),模板中调用的 Method 就会重新执行一次
在功能上还有一个明显的区别: Computed 通常是不接受参数的(因为它像是一个属性),而 Methods 可以接受参数。所以如果需要根据传入参数动态计算结果,通常只能用 Methods。
slot是什么?有什么作用?原理是什么?
Slot(插槽)是 Vue 的内容分发机制。简单来说,就是子组件挖了一个坑(占位符),父组件填什么内容进去,由父组件决定。
-
默认插槽:父组件没指名道姓,内容就直接塞这里。
-
具名插槽:有名字的坑。父组件通过v-slot:header这种方式,把内容精确地放到子组件对应的 <slot name="header">里。
-
作用域插槽(重点):这是最灵活的。数据在子组件,但样式由父组件定。子组件通过 props 的形式把数据传出来,父组件拿到数据后决定怎么渲染。
父组件的内容是在哪里编译的?
编译作用域
"Vue 有个重要原则:父级模板里的所有内容都是在父级作用域中编译的;子级模板里的所有内容都是在子级作用域中编译的。"
实现原理的本质
普通插槽(默认/具名): 在父组件编译阶段,插槽内容已经被编译成了 VNode(虚拟节点)。当子组件渲染时,它通过 this.$slots 拿到这些 VNode,直接把它们替换到对应的 <slot> 标签位置。
本质:父组件把生成好的"砖块"给子组件,子组件只负责砌上去。
作用域插槽(Scoped Slots):
原理完全不同。作用域插槽在父组件编译时,不会立即生成 VNode,而是被编译成一个函数。 当子组件渲染时,它会调用这个函数,并把子组件内部的数据作为参数传进去。这个函数执行后,才生成最终的 VNode。
本质:父组件给子组件一个"模具"(回调函数),子组件把"材料"(数据)倒进去,生成"砖块"。
如何保存页面的当前的状态
第一层:利用路由元信息 (Vue Router)
对于简单的参数传递,我们可以使用 Vue Router 的params或query。
原理: 把状态放到 URL 上(query)或者路由内部对象里(params)。
Vue 写法: this.$router.push({ name: 'PageB', params: { data: xxx } })。
缺点: 页面刷新后,params 传值可能会丢失(取决于配置),且不适合传递大量复杂数据。
第二层:利用本地存储 (Web Storage)
对于需要持久化(刷新页面也不丢)的数据,使用 localStorage 或 sessionStorage。
需要在组件即将被销毁的生命周期中在 LocalStorage / SessionStorage 中把当前组件的 state 通过 JSON.stringify() 储存下来就可以了。
优化方案: 现在的项目通常会配合 Pinia 的持久化插件 (pinia-plugin-persistedstate) 来自动完成这个过程,不需要手动写 setItem/getItem。
第三层:全局状态管理 (Vuex / Pinia)
如果组件之间关系不紧密,或者状态需要在多个页面共享。
方案: 将数据存入 Store (Pinia/Vuex)。
优点: 响应式、组件间解耦。
缺点: 页面刷新会丢失(除非配合上面的持久化插件)。
第四层:组件缓存 (keep-alive)
这是 Vue 区别于 React 的一个显著优势(React 需要手动实现类似机制)。
核心原理 keep-alive 是 Vue 的内置组件,它会在内存中缓存组件的实例 (VNode),而不是销毁它们。当组件在 keep-alive 内切换时,不会触发 created、mounted 和 destroyed 钩子。只会触发 activated (激活) 和 deactivated (停用) 钩子。
keep-alive 内部使用了 LRU (Least Recently Used) 算法。我们可以通过 max 属性限制缓存组件的最大数量。当超过限制时,最近最久未使用的组件实例会被销毁。
常见的事件修饰符及其作用
Vue 的事件修饰符主要是为了处理 DOM 事件的细节,让我们在写 methods 时能更专注于业务逻辑,而不需要去写大量的 event.stopPropagation() 或 preventDefault()。
- .stop:等同于 JavaScript 中的 event.stopPropagation() ,防止事件冒泡;
- .prevent :等同于 JavaScript 中的 event.preventDefault() ,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);
- .capture :与事件冒泡的方向相反,事件捕获由外到内;
- .self :只会触发自己范围内的事件,不包含子元素;
- .once :只会触发一次。
- .passvie。它告诉浏览器'我不会阻止默认行为',这样浏览器就不用等待 JS 执行完再去滚动,能明显提升滚动的流畅度。
- .native:在 Vue 2 中如果想给组件根元素绑定原生事件需要加 .native,但在 Vue 3 中这个修饰符被移除了,Vue 3 能自动识别原生事件。
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的值。
v-if和v-show的区别
v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display样式属性控制显隐;
-
编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换;
-
编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且DOM元素保留;
-
性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;
-
使用场景:v-if适合运营条件不大可能改变;v-show适合频繁切换。