Vue3学习(对比Vue2)
前言
参考文档:vuejs官方中文文档
背景:最近换了新工作,要用Vue3进行开发项目。为了能顺利转正,趁闲余时间抓紧学习下。分享出来也希望能得到大佬们的指正。
什么是选项式API (Options API)和组合式API(Composition API)?
Vue2
使用的是Options API
,需要将内容固定写在特定位置或方法中。例如,响应式数据需要放在data
中,方法需要写在methods
中。
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
},
}
</script>
Vue3
主要使用Composition API
,同时兼容Options API
。它提供了更自由的使用方式,不再限制内容的位置。例如,响应式数据只需要调用ref
方法,方法也不再需要写在methods
中。
<script setup>
const count = ref(0)
function increment() {
count.value++
}
</script>
模板语法的差异
1、Vue3支持绑定多个动态的值。比如:
const objectOfAttrs = {
id: 'container',
class: 'wrapper'
}
// 使用方式威使用v-bind绑定对象
<div v-bind="objectOfAttrs"></div>
2、支持动态参数。写法如下(实践中有什么意义?)
<a v-on:[eventName]="doSomething"> ... </a>
<!-- 简写 -->
<a @[eventName]="doSomething">
3、v-modle修改了默认值。
Vue2
的v-model是属性value
和自定义事件input
的语法糖。具体语法如下:
<input v-model="searchText" />
<input
:value="searchText"
@input="searchText = $event.target.value"
/>
Vue3
的v-model
的默认属性改为modelValue
和默认自定义事件改为update:modelValue
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
4、v-model支持修饰符,可以操作返回的参数。例如:vant
的tabs
组件v-model:avtive
<van-tabs v-model:active="active"></van-tabs>
什么是Hooks?
Hooks
一词的英文含义是“钩子”,在编程中是指用钩子将外部特性勾住。在Vue3
中,比如ref
的功能就是返回一个响应式的数据并将参数设为默认值。简单来说,Hooks
就是一种加工数据的方法。
为什么要用Hooks?或者说Hooks对比之前的编码方式都有哪些优点和缺点?
- 相比之前的编码方式,使用Hooks可以使组件自定义常用方法更加便捷。在React 18之前,通常使用组件继承的方式编写常用自定义方法,而Vue2则使用mixin方式合并自定义方法,原生JavaScript中则常常将功能抽象为工具类的方法。
- 使用
Hooks
可以使代码更加整洁。通过将功能相同的代码集中在一块,我们可以更清晰地了解组件的逻辑和状态管理。 - Hooks采用函数式编程的方式,这使得在使用
TypeScript
等静态类型检查工具时更有利于代码的管理和类型检查。
而使用Hooks可能带来的一些潜在缺点是:
- 学习成本。对于面向对象编程熟悉的开发者,初步掌握函数式编程可能需要一定的时间和精力
setup 的使用方式有哪些?
在原生的JavaScript
中,我们要实现独立的组件需要使用函数返回的方式执行代码。为了在引用组件时使数据独立,选择了data
函数返回新对象的方式而不是直接使用data
对象的方式。所以在不借助外部工具的情况下,setup
的使用方式是调用setup
方法并在该方法中返回模板中所需的变量。这样的设计与React
调用render
方法渲染模板类似。示例代码如下:
export default {
setup() {
const count = ref(0)
function increment() {
// 在 JavaScript 中需要 .value
count.value++
}
return {
count,
increment
}
}
}
可以看到,在官方模板中,调用Hooks是不需要显式引用的。Vue
可以利用闭包原理,在外层定义ref
等Hooks方法后,通过调用setup
方法实现方法的引用。
由于Vue使用的是单文件组件 (SFC) ,可以对代码进行自定义操作,将部分繁杂的操作隐藏起来。使用方式是在<script>
标签中添加setup
标记。
<script setup>
// import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
- 你可以直接使用定义的数据,Vue3会返回当前函数作用域的闭包。
- 不需要引用官方的hook方法也可以使用,你可以理解为在代码块是在setup函数作用域中。
响应式
ref
ref()
接收参数,并返回一个包裹了.value
属性的ref对象。在JavaScript代码中,我们需要使用.value
的形式读取数据,在模板中可以直接使用(Vue3会自动解构ref对象)。
底层原理与Vue2的响应式原理类似,都是通过Object.defineProperty
来劫持对象的属性。不同的是,Vue2劫持的是data
方法返回的对象中的每个属性,而ref
只劫持了单个属性——value
。
对于对象或数组等深层嵌套数据类型,ref
会自动递归劫持该数据。
reative
reative()
接收一个普通对象作为参数,并返回一个对象的响应式代理对象。我们可以像使用普通对象一样使用该代理对象。
底层原理是通过Proxy
来劫持对象所有属性的访问和修改。由于代理需要与地址关联,所以作为参数的类型必须是对象。在JavaScript中,简单数据类型存储在内存栈中,而复杂数据类型存储在内存堆中。只有存储在内存堆中的数据才具有内存地址,因此只有复杂数据类型才能被成功代理。
在JavaScript中,声明变量时,变量的键值关系(也就是变量名与对应的值)是存在内存栈中的。当我们对变量进行直接赋值或解构赋值时,会改变变量名的引用,从而导致响应式对象的关联关系丢失,进而导致响应式功能失效。比如:
let state = reactive({ count: 0 });
state = { count: 0 }; // 这里直接赋值,变量state将指向一个新的内存地址
state.count++; // 页面不响应,因为现在state与原先的响应式对象失去了关联
上面的代码对应的关系变化如下:
变化顺序 | 变量名 | 变量值 | 内存地址 |
---|---|---|---|
1 | state | reactive({ count: 0 }) | 1 |
2 | state | { count:0 } | 2 |
3 | state | { count:1 } | 2 |
为了确保响应式功能正常运作,正确的做法如下:
- 单个属性通过修改对象属性的方式。比如:
state.count = 1
。 - 多个属性通过对象的合并方法。比如:
Object.assign(count:2,name:'la')
。 - 对于数组、Map、Set等集合类型数据,也应该使用特定的方法来修改内部数据,以确保响应式功能的正常运作。
综上所述,由于reactive()
在使用中有一些限制,Vue3官方文档推荐我们在声明响应式数据时,优先使用ref
,它能更好地满足大多数的需求,并且使用起来更加方便和直观。
Dom更新时机
如果将数据修改和更新页面放在同一个事件队列中,将增加页面的渲染次数。为了更好的性能,我们的目标是多次修改仅渲染一次。
JavaScript的事件轮询机制是宏任务-微任务-页面渲染
,在页面渲染完后会执行钩子函数nextTick
来告知一次事件循环结束。
因此,实现方案就显而易见了:在第一次事件循环时执行代码修改虚拟DOM,在nextTick
中对比虚拟DOM和真实DOM,并修改不同部分,在第二次事件循环中进行页面渲染。
Vue中也有钩子函数nextTick
,表示修改状态后、页面更新前。根据上面的原理,我们知道只需要在页面渲染前回调nextTick
方法就可以实现优化渲染。
生命周期
对比Vue2
的生命周期,对卸载组件的回调函数名称进行了修改,由destory
改为unMount
。
在Vue3中加入了Hooks
的用法,如果多次调用同一声明周期会有什么效果呢?尝试运行下面的代码:
onMounted(() => {
console.log(4);
setTimeout(() => {
console.log(3)
}, 0)
})
onMounted(() => {
console.log(2)
})
onMounted(() => {
console.log(1)
})
// 输出结果 4,2,1,3
从输出结果可以看出,声明多个声明周期会按照从上往下的顺序执行。
如果给你设计,应该如何实现这个功能?
- 在当前作用域创建一个先进先出的桶作为容器,保证从上往下的执行顺序。
- 在调用方法的位置将函数添加到桶中。
- 在需要的地方遍历并调用桶中的方法。
我们可以使用以下代码简单实现上述思路:
const onMountedBucket = []
function onMounted (callback) {
onMounredBucket.push(callback)
}
nextTick(() => {
onMountedBucket.forEach(callback => callback())
})
这种设计模式解决了什么问题?有什么优点和缺点
解决了将某一类方法集合在某一处执行的需求。它的优点是可以保证在不同的作用域下,代码仍能按照预期顺序执行,而不需要将这些代码写在同一个作用域内。
假设应用到性能优化当中,能否优化性能?比如我们要实现react
的setState
方法,将多次修改数据操作集合成一次修改数据操作。然而,这种设计模式也带来了一个缺点,即数据更新不及时的问题。在react
的setState
方法中,为了解决这个问题,引入了callback
参数,允许开发者在数据更新后通过回调函数获取到更新后的state
。我们尝试着实现这个方案:
- 初级目标是将多次修改数据操作集合成一次修改数据操作。操作的时机要求是一次代码执行完后,根据我们对事件循环的认识,选择微任务或开启新一轮事件循环的时机。从执行顺序的角度来看,微任务的方案会更加适合,因此我们优先考虑实现微任务的方案。
- 实现完初级目标后,我们将尝试加入回调函数来获取更新后的
state
。
下面就是我的实现方案了:
const state = {};
let provStateMap = {};// 为了方便数据合并操作。设计一个类似state的数据结构
function setState(provState) {
Object.assign(provStateMap, provState);
// 首次执行添加一次合并回调钩子
if (Object.keys(provStateMap).length === 0) {
Promise.resolve().then(() => {
if (Object.keys(provStateMap).length > 0) {
Object.assign(state, provStateMap);
provStateMap = {}; // 执行完毕后清空数据,方便下次操作
}
});
}
}
// 初级目标已经实现了,下面实现回调函数获取更新后的state
let callbakBucket = [] // 设计一个桶来装回调函数
function setStateWithCallback(provState,callback){
setState(provState)
// 首次执行添加一次合并回调钩子
if(setStateBucket.length === 0){
Promise.resolve().then(() => {
if(callbakBucket.length > 0)
callbakBucket.forEach(callback => callback())
callbakBucket = []
})
}
}
当我们想要实现新一轮事件循环时,发现只需要将 Promise.resolve().then
修改成setTimeout
就可以实现了。
计算属性和侦听器
computed
const surname = 'li'
const lastName = 'hua'
const getName = computed(() => surName+lastName)
watch
const count = ref(0)
watch(count,
(newCount) =>{console.log(newCount)},
{deep:true,immediate: true}
)
watchEffect
自动读取函数中使用到的响应式数据并在数据更新时调用方法。
const count = ref(0)
watchEffect(() => {
console.log(newCount)
})
底层实现中,watch
和 watchEffect
都是基于响应式原理实现的。其大致思路如下:
- 劫持响应式数据的访问,并将回调函数添加到监听队列中。
- 当响应式数据发生变化时,触发监听队列中的回调函数。
一种简化的实现思路如下:
const Bucket = [];
const addCallback = (callback) => { return callback; }
const reactiveObject = new Proxy({}, {
get: (target) => {
Bucket.push(addCallback());
},
set: (target) => {
Bucket.forEach(callback => callback());
}
});
watch
通过第一个参数找到对应的响应式数据,然后将第二个参数的回调函数添加到监听队列中。
watchEffect
则在初始化时执行一次函数,触发函数内部对响应式数据的访问,然后将回调函数添加到监听队列中。
computed
可以看作是高级版的 watchEffect
。通过声明变量,执行函数,并调用相应属性的 get
方法,将回调函数添加到监听队列中。以上述代码为例进行解释:
const surname = 'li';
const lastName = 'hua';
const getName = computed(() => surName + lastName);
// 等同于
let name = "";
watchEffect(() => {
name = surname + lastName;
});
computed
将一个计算属性的依赖关系和更新函数封装在一起,当依赖数据发生变化时,它会自动计算新值,并触发更新函数。这样可以更加高效地处理依赖关系和数据更新。
组件通信
props
在 Vue3 中,通过 defineProps()
方法返回组件的 props 对象,其中保存了父组件传递给子组件的属性。
defineProps
接受一个对象参数,用于定义哪些属性可以被读取。官方设计此方法的原因是为了在开发者使用错误的属性时,能够在浏览器中发出警告,并通过对参数的读取实现在模板中不需要写明 props 的效果。
withDefaults
提供了为 props 设置基于类型声明的默认值的能力。
具体使用方法如下:
// 定义允许使用的属性列表,以字符串数组形式传递
const props = defineProps(['foo']);
// 定义允许使用的属性以及其数据类型,以对象形式传递
const props2 = defineProps({
title1: String,
title2: [String, Number],
title3: {
required: true,
type: String,
default: '标题'
},
title4: {
type: Object,
default: () => ({}),
validator: (value) => { console.log(value) }
}
});
// 使用 TypeScript 的情况
const props = defineProps<{
title?: string;
likes?: number;
}>();
// 使用 withDefaults 的情况
export interface Props {
msg?: string;
labels?: string[];
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
});
emit
通过 defineEmits()
方法返回 Vue3
中的 $emit
对象,其设计与 props
类似。使用方式如下:
// 定义允许使用的自定义方法列表,以字符串数组形式传递
const emit = defineEmits(['inFocus', 'submit']);
emit('inFocus'); // 调用自定义方法
// 使用 TypeScript 的情况,e 表示事件名称,第二个参数是传给外部的值,void 代表方法没有返回值
const emit = defineEmits<{
(e: 'change', id: number): void;
(e: 'update', value: string): void;
}>();