Vue2 VS Vue3
Vue2
响应式缺陷
无法监听到对象属性的动态添加和删除
在 data 中声明一个对象:
data() {
return {
obj: {
a: 1
}
}
}页面上通过 for...in... 循环该对象,能看到数据 a。为对象 obj 添加属性 b:
mounted() {
this.obj.b = 2
console.log(b)
}控制台能输出 b,但页面只有 a 没有 b,属性 b 没有响应式。
无法监听到数组下标和 length 长度的变化
Vue2内部重写了部分数组 API 使其保持响应式:array.pop()array.push()array.shift()array.unshift()array.sort()arry.reverse()array.splice()
通过数组索引修改数据不会响应式修改:
fn() {
this.arr[0] = 1
}直接修改数组长度也不会生效:
fn() {
this.arr.length = 0
}原因
利用 defineReactive 方法,通过 Object.defineProperty 对属性进行劫持,数组则通过重写方法进行劫持。每个属性值拥有自己的 dep 属性,用来存取所依赖的 watch,数据改变时触发相应的 watch 更新数据。
Vue2的响应式存在两个缺陷:1. 对象新增属性、删除属性界面不会更新;2. 通过数组下标修改数组内容界面不会更新。
原因:
- Vue无法检测
property的添加或移除。由于Vue在初始化实例时对property执行getter/setter转化,所以property必须在data对象上存在才能让Vue将其转换为响应式。- 通过数组下标修改数组不会触发响应式。尤雨溪在 GitHub 上说明过,由于数组长度可能很大,通过索引修改数据的方式会造成很大的性能消耗,因此不对索引方法作额外处理。
数组响应式
对象中增加或删除属性时,数据响应式不生效,因为Vue2使用 Object.defineProperty 方法进行数据劫持。 添加元素时应用 $set 添加属性,用 $remove 删除属性。
数组也是对象,但使用数组 API 操作数组(添加或删除元素)时视图有更新。原因:
push、pop 等方法不会触发 getter/setter,因为它们是 Array 原型上的方法,不在 Array 本身上面。
push、pop、shift、unshift、splice、sort、reverse 操作数组时能响应式。实现思路:使用拦截器覆盖 Array.prototype 上的方法,在执行原型方法之外做数据响应式。
- 将数组的原型存到对象
arrayMethods中 - 找到 Array 上能改变数组自身的 7 个方法:
push、pop、shift、unshift、splice、sort、reverse - 将这 7 个方法进行响应式处理
- 处理完成后,用它们覆盖
arrayMethods中对应的方法 - 将需要响应式处理的数组 arr 的
__proto__指向arrayMethods,如果浏览器不支持访问__proto__,则直接将响应式处理后的 7 个方法添加到数组 arr 上 - 如果要将数组完全实现响应式,需要遍历该数组,将数组中的数组使用该方法进行响应式处理,将对象使用 walk 方法进行响应式处理
定义拦截器
// 获取Array的原型
const arrayProto = Array.prototype;
// 创建一个新对象,该新对象的原型指向Array的原型。
export const arrayMethods = Object.create(arrayProto);
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(mentod => {
// 缓存原始方法
const original = arrayProto[method];
// 对新原型对象上的方法,做数据绑定
Object.defineProperty(arrayMethods, method, {
value: function mutator(...args) {
// 返回原始方法
return original.apply(this, args);
},
enumerable: false,
writable: true,
configurable: true
})
})将拦截器挂载到数组上面
import { arrayMethods } from './array' // 处理好的Array原型对象
// __proto__是否可用
const hasProto = '__proto__' in {}
// 所有属性名,不论是否可枚举(与Object.keys的区别)
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
export class Observe {
// 将value转为响应式
constructor(value) {
this.value = value
if (Array.isArray(value)) {
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
} else {
this.walk(value) // Object的响应式处理,在其他文章中
}
}
}
/**
* __proto__可用的处理方式
* 将target对象的原型对象替换为src
*/
function protoAugment(target, src, keys) {
target.__proto__ = src
}
/**
* __proto__不可用的处理方式
* 将src上面的所有属性都copy到target
*/
function copyAugment(target, src, keys) {
for (let i = 0, len = keys.length; i < len; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
// Object.defineProperty()的封装
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true,
})
}收集依赖
收集依赖:
function defineReactive(data, key, val) {
let childOb = observe(val)
let dep = new Dep() // 存储依赖
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend()
if (childOb) childOb.dep.depend() // 收集
return val
},
set: function (newVal) {
if (val === newVal) return
dep.notify()
val = newVal
},
})
}
// 返回val的响应式对象
function observe(val, asRootData) {
if (!isObject(value)) return
let ob
// 避免重复侦测
if (hasOwn(value, '__ob__') && value.__ob__ instanceof observer) {
ob = value.__ob__
} else {
ob = new Observe(value)
}
return ob
}Vue3
响应式原理
Vue3响应式原理主要通过Proxy 和 Reflect 实现。Proxy 是 ES6 的新语法,直接代理对象,无需像 Object.defineProperty 那样重新代理,性能更佳。
Reflect 可以对源对象的属性进行操作。
let person = {
name: '张三',
age: 18,
}
const p = new Proxy(person, {
//有人读取p的某个属性时调用
get(target, prop, receiver) {
console.log(target, prop)
//return target[p]
return Reflect.get(target, prop)
},
//有人修改、增加p的某个属性时调用
set(target, p, value, receiver) {
console.log(`有人修改了p身上的${p},我要去更新界面了`)
//target[p] = value
Reflect.set(target, p, value)
},
//有人删除p的某个属性时调用
deleteProperty(target, p) {
console.log(`有人删除了p身上的${p},我要去更新界面了`)
//return delete target[p]
return Reflect.deleteProperty(target, p)
},
})
console.log((p.age = 23))
console.log(person)hook
Vue3 中的hook 通常称为 Composition API,是 Vue 框架的重要特性。本质是组件内部使用的函数,能在不影响组件逻辑的情况下增强和扩展组件功能。Hook 的主要作用是允许在组件之间重用状态逻辑。例如,处理异步请求和管理请求状态的功能,可能在多个组件中都需要。在Vue2.x中,需要使用 mixins 或 HOC(高阶组件)来抽象和重用这些逻辑,但容易导致命名冲突和逻辑混乱。
使用Vue3的 Composition API,可以避免上述问题。通过调用 useFetch 这样的自定义 hook,在任何组件中重用异步请求逻辑:
import { reactive, onMounted } from 'vue'
function useFetch(url) {
const state = reactive({
data: null,
loading: true,
error: null,
})
const fetchData = async () => {
try {
const response = await fetch(url)
const data = await response.json()
state.data = data
state.loading = false
} catch (error) {
state.error = error
state.loading = false
}
}
onMounted(fetchData)
return state
}
export default useFetch在其他组件中使用:
import useFetch from './useFetch'
export default {
setup() {
const posts = useFetch('/api/posts')
return {
posts,
}
},
}useFetch 就是一个自定义 hook(Composition API),可在各组件间重用。
Vue3 中的 hook 和 mixin 的对比
Vue3的hook(复用性函数)和 mixin(混入)都是Vue中实现逻辑复用和代码组织的机制。以下是区别:复用性:
mixin允许多个Vue组件共享JavaScript功能,但mixin内的生命周期函数不易理解,容易导致命名冲突。Vue3的hook以函数形式提取可复用内容,可解决命名冲突。逻辑相关性:
mixin混入的方法、生命周期函数中的逻辑可能散落在整块代码中,不便管理与维护;Vue3的hook更容易形成独立、按功能集中管理的代码。类型支持:通过
mixin混入的属性或方法,在类型系统中很难得到良好支持。Vue3通过Composition API的hook,因其通过函数返回值主动暴露,在TypeScript环境下有更好的类型推导支持。逻辑组织:
mixin无法将大组件拆分为更小的函数单元,Vue3的hook可以轻松实现。
| Hooks | Mixins | |
|---|---|---|
| 定义 | hook 是通过 Composition API 引入的新特性,类似于 React 的 hook。 | mixin 是一种对 Vue 组件进行扩展的方式。 |
| 功能 | 组织和重用逻辑。在组件中创建和重用复杂的逻辑代码,使组件逻辑更清晰和可维护。 | 将组件的代码封装到可复用的模块。常用于抽离公用代码片段,实现复用,使组件逻辑更清晰和可维护。 |
| 使用 | 使用 setup 方法组织和复用各类逻辑。 | 使用 mixin 属性加载公用代码片段。 |
| 组织代码效果 | 使用 Hooks 可以让组件的逻辑函数按功能组织,使组件的逻辑结构更清晰。 | 使用 Mixins 可以将组件的各个生命周期的相关函数统一放在一起,但可能使组件的逻辑函数分散在各个生命周期中。 |
| 冲突问题 | Hooks 允许命名冲突的功能,从而避免各种命名冲突。 | Mixins 可能导致函数名冲突。如果两个 mixin 包含相同函数,后一个 mixin 的函数会覆盖先前的函数。 |
| 难以追踪的来源 | Hooks 使用的是函数,如果不加注释,可能不太容易找到其来源。 | 在 Mixin 中,可以在每个使用了公用代码片段的地方用注释表明代码来源,有助于更好地追踪和维护代码。 |
| Debug 困难度 | Hooks 有更好的 Stack Trace,可以提供更优秀的 debug 体验。 | 对 mixins 的支持可能在运行时错误的情况下无法找到出错的代码,导致调试困难。 |
示例对比:
使用 mixin:
//定义一个mixin
let myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
},
},
}
//在组件中使用mixin
var Component = Vue.extend({
mixins: [myMixin],
})这里 "hello" 方法被添加到组件的 methods 属性中,组件的 created 生命周期钩子被调用后,也调用了 mixin 中的 created。
使用 hook:
//定义一个hook
function useHello() {
const hello = () => {
console.log('hello from hook!')
}
onMounted(hello)
return {
hello,
}
}
//在组件中使用hook
const Component = {
setup() {
const { hello } = useHello()
return {
hello,
}
},
}这里用 onMounted 函数代替了 created 生命周期钩子,"hello" 函数从 hook 中解构出来。Vue3的 hook 将逻辑保持在独立函数中,使组件代码保持清晰。
NOTE
Vue3可继续使用mixin。但推荐使用 Composition API 来组合和重用逻辑,使逻辑组合和重用更方便、灵活,可读性和可维护性更好。根据Vue3官方文档,Mixin 在Vue3中已被标记为不推荐,将来可能被 Composition API 完全取代。Vue2 与 Vue3 区别
改成组合式 API 没有
this生命周期没有
create,setup等同于create,卸载destroyed改成unmounted- Vue3中
v-if优先级高于v-for 根实例创建从
new app变为createApp方法全局注册(如
mixin、全局组件、use)改成用App实例调用,而不是 Vue 类调用新增传送门组件
teleporttemplate模版可不包在根div里- Vue3新增了静态节点,在对比更新时,如果节点是静态的,会跳过对比,提升性能html
<div>{{ a }}</div> <div>123</div> <div :class="b">cc</div>jsexport function render(_ctx, _cache, $props, $setup, $data, $options) { return ( _openBlock(), _createElementBlock(_Fragment, null, [ _createElementVNode( 'div', null, toDisplayString(_ctx.a), 1 /* TEXT */, ), _createElementVNode('div', null, '123'), _createElementVNode('div', { class: _ctx.b }, 'cc', 2 /* CLASS */), ]) ) }模板字符串这种文本动态会有标记「1」,类名这种属性动态有标记「2」,静态节点没有标记。
mixin 与 hook
在Vue2中,公共方法可以使用 mixin 混入。
app.mixin({
data() {
return {
name: 'mixin',
}
},
methods: {
mixinMethod() {
console.log('mixinMethod')
},
},
mounted() {},
})mixin 是选项式 API,在Vue3中推荐使用 Composition API,mixin 不适用,更推荐使用 hook。
import { ref, onMounted } from 'vue'
export function useHello() {
const name = ref('hook')
const hello = () => {
console.log('hello from hook!')
}
onMounted(hello)
return {
name,
hello,
}
}根节点
- Vue2只能有一个根节点,多个根节点会报错
- Vue3允许拥有多个根节点
生命周期
| Vue2 | Vue3 | 生命周期含义 |
|---|---|---|
| beforeCreate | setup() | 开始创建组件之前,创建的是 data 和 method |
| created | setup() | 同上 |
| beforeMount | onBeforeMount | 组件挂载到节点上之前执行的函数。 |
| mounted | onMounted | 组件挂载完成后执行的函数 |
| beforeUpdate | onBeforeUpdate | 组件更新之前执行的函数。 |
| updated | onUpdated | 组件更新完成之后执行的函数。 |
| beforeDestroy | onBeforeUnmount | 组件挂载到节点上之前执行的函数。 |
| destroyed | onUnmounted | 组件卸载之前执行的函数。 |
| activated | onActivated | 组件卸载完成后执行的函数 |
| deactivated | onDeactivated | 在组件切换中老组件消失的时候执行 |
v-if 与 v-for 的优先级
- 在Vue2中,
v-for优先级高于v-if,每循环一次就判断一次,造成性能消耗 - 在Vue3中,
v-if优先级高于v-for,当判断不生效、不渲染该 DOM 节点时,该节点的v-for失效,不生成循环 DOM 节点
TIP
无论Vue2还是Vue3,都不建议在同一 DOM 节点中同时使用 v-if 和 v-for。
如需实现该效果,可根据业务做不同处理:
- 使用
filter过滤出需要的数组数据,再通过v-for循环遍历 v-for遍历后在内部设置template标签包裹内容,在template上使用v-if判断
API
在Vue2中,使用选项式 API,优点是初学者简单易懂,缺点是相关模块分散,不利于大型项目维护:
vue<template> <div>......</div> </template> <script> export default { // 数据 data() { return {} }, mounted() {}, // 方法 methods: {}, computed: {}, } </script>在Vue3中,使用组合式 API:
vue<script setup> // 数据和方法都写这里,更简洁 </script>
改为组合式 API 后,没有 this。
Diff 算法
Vue2:
diff算法就是进行虚拟节点对比,并返回一个patch对象,用来存储两个节点不同的地方,最后用patch记录的消息去局部更新Dom。
diff算法会比较每一个vnode,而对于一些不参与更新的元素,进行比较是有点消耗性能的。Vue3:
diff算法在初始化的时候会给每个虚拟节点添加一个patchFlags,patchFlags就是优化的标识。
只会比较patchFlags发生变化的vnode,进行更新视图,对于没有变化的元素做静态标记,在渲染的时候直接复用。父子组件
| Vue2 | Vue3 | |
|---|---|---|
| prop | 子组件 this.$prop 接收数据 | 子组件 defineProps 接收数据 |
| emit | 子组件 this.$emit() 设置自定义事件 父组件在子组件设置相应的方法使用 | 子组件先需要 const emit = defineEmits() 设置方法 然后再通过 emit() 自定义事件通知父组件 父组件使用方法和 Vue2 一样 |
| ref | 子组件使用关键字 ref = xxxx 设置组件实例名称 通过 this.$refs.xxxx 获取子组件实例 | 子组件使用关键字 ref = xxx 设置组件实例名称 声明该实例 const xxx = ref() 现在可以使用该组件实例内的方法 注意:如果想使用子组件的方法变量,需要子组件通过 defineExpose 方法事先导出 |
数据绑定原理
| vue2 | vue3 |
|---|---|
| 利用 ES5 的一个 API Object.defineProperty() 对数据进行劫持,结合发布者订阅者模式的方式来实现的。 | 使用了 ES6 的 Proxy API 对数据代理。 |
Proxy 替换 Object.defineProperty,优势:- 可直接监听数组类型的数据变化
- 性能提升
- 监听的目标为对象本身,不需要像
Object.defineProperty一样遍历每个属性 - 可直接实现对象属性的新增/删除
组件 v-model
在Vue2中,组件使用 v-model 实际是为组件动态绑定 value,监听 change 或 input 事件。
在Vue3中,组件使用 v-model 实际是为组件动态绑定 modelValue,监听 update:modelValue 事件。
Vite VS Webpack
参考 Vite VS Webpack。