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有什么区别
- 性能优化:
- Vue3引入静态树与静态属性提升,在编译时识别不会改变的Dom与属性,将静态节点移动到render函数外部进行缓存,减少渲染开销
- Diff算法,Vue3使用的经过优化的单向遍历,跳过不需要更新的节点,Vue2是双向指针遍历
- Vue3支持Fragment,允许组件返回多个根节点,无需额外的包装
- 事件监听优化,Vue3事件监听器是懒加载的,触发时才绑定;Vue2是挂载立即绑定
- 更好的TS支持,模块化设计按需引入
- Vue3是Composition API,根据逻辑功能组织代码,可读性和复用性更高;Vue2是Options API(data、computed、methods、watch),想复用要用mixin函数
- 响应式系统:Vue3使用Proxy和Reflect实现响应性,即reactive方法,可监听属性的增删改和数组的变化;Vue2则依赖Object.defineProperty,只能监听指定对象的指定属性的getter和setter行为。
- 生命周期钩子:
- 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:事件总线,可用于任意组件之间传值,需要使用数据的订阅者组件通过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因为添加了更多的计算操作,反而效率会低。