从Vue2迁移Vue3的全攻略
从Vue3 alpha
(2019.12.21)发布开始,开始关注Vue3
,并通过@vue/composition-api
包在 Vue2 的练习项目使用,到后来的Vue3 beta
(2020.4.17)发布,直接使用Vue3 beta
版本再次尝试,再到后来的Vue3 rc
(2020.7.18),以及最后终于在2020.9.18
发布了正式版,这一路学着不断变更的 api,经常过一段时间发现之前的 api 变了,直到Vue3
的生态稳定之后又开始尝试,发现真香。
其实,最好的学习文章应该是官方文档,不得不说VUE
的文档是真的不错,在 Vue3 官网上也有Vue3 迁移指南。所以,如果需要认真的了解全部的 Vue3 内容建议去官网学习,此文章只会针对性的说明一些我在实际开发中遇到的变更。
组合式 API
Vue3 重大更新就是组合式API(Composition Api)
了,接下来就一一说明一下。
生命周期
选项式 | Hook inside setup |
---|---|
beforeCreate | setup |
created | setup |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
activated | onActivated |
deactivated | onDeactivated |
setup
一个组件选项,在创建组件之前执行,一旦 props 被解析,并作为组合式 API 的入口点
setup
函数有两个参数: props
和ctx
,其中 ctx 是一个对象({attrs,slots,emit}
).
- attrs: 绑定在组件中的所有
显示指定
的props
,这里需要特别注意的是在Vue3
中如果需要将属性绑定到组件根节点需要声明props
,对应Vue2
的this.$attrs
- slots: 插槽列表,对应
Vue2
中的this.$slots
- emit: 触发事件,对应
Vue2
中的this.$emit
,若组件内需要触发事件,需要在emtis
选项中配置或者在使用defineEmit
时指定。
在setup
函数中定义的变量或函数,如果需要在template
中使用的话需要在setup
函数中返回,不返回的无法在template
中使用。这样写会有一些繁琐,所以Vue3
提出了一个新想法: script setup
。在script
标签上标注为setup
则不需要返回定义的变量和函数就可以在template
中使用。
Count.vue
<template>
<div>{{ count }}</div>
<div>
<button @click="$emit('sub')">-1</button>
<button @click="$emit('add')">+1</button>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'Count',
props: {
count: {
type: Number,
default: 0
}
},
emits: ['sub', 'add']
});
</script>
App.vue
<template>
<count :count="count" @add="onAdd" @sub="onSub" />
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import Count from './components/Count.vue';
export default defineComponent({
name: 'App',
components: { Count },
setup() {
const count = ref(0);
const onAdd = () => count.value++;
const onSub = () => count.value--;
return {
count,
onAdd,
onSub
};
}
});
</script>
如果使用script setup
,则是下面的代码
Count.vue
<template>
<div>{{ count }}</div>
<div>
<button @click="$emit('sub')">-1</button>
<button @click="$emit('add')">+1</button>
</div>
</template>
<script setup lang="ts">
import { defineComponent, defineEmit, defineProps } from 'vue';
defineComponent({
name: 'Count'
});
defineProps({
count: {
type: Number,
default: 0
}
});
defineEmit(['sub', 'add']);
</script>
App.vue
<template>
<count :count="count" @add="onAdd" @sub="onSub" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import Count from './components/Count.vue';
const count = ref(0);
const onAdd = () => count.value++;
const onSub = () => count.value--;
</script>
注意:
script setup
目前(Vue: ^3.0.5)
是RFC
状态
ref
将一个基本类型的数据转换成响应式且可变的 ref 对象, 通过.value
属性修改和获取值.
<template>
<div>{{ count }}</div>
<button @click="onAdd">+1</button>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const count = ref(0);
const onAdd = () => count.value++;
</script>
在script
中,需要使用.value
,但是在template
中不再需要,Vue 会自动解开,如果觉得script
中的.value
不顺眼,其实还有一个语法糖ref:
,通过此语法糖可以不使用.value
直接设置和获取值.
<template>
<div>{{ count }}</div>
<button @click="onAdd">+1</button>
</template>
<script setup lang="ts">
ref: count = 1;
const onAdd = () => count++;
</script>
注意:
ref:
目前(Vue: ^3.0.5)
是RFC
状态,且只能在SFC(单文件)
中使用
ref 的配套方法
unref
如果参数是一个ref
则返回内部值,如果不是则返回参数本身.即: isRef(val) ? val.value : val
的语法糖
toRef
将一个响应式的对象属性转为响应式的.
<template>
<div>{{ count }}</div>
<button @click="onAdd">+1</button>
</template>
<script setup lang="ts">
import { reactive, toRef } from 'vue';
const state = reactive({
count: 0
});
const count = toRef(state, 'count');
const onAdd = () => count.value++;
</script>
toRefs
将一个响应式的对象,转换成一个普通对象,并且将对象中的属性转换为ref
对象.一般的,当想对一个reactive
包装的响应式对象进行解构时,解构之后的变量将会失去响应式,此时可通过toRefs
将对象转换然后再解构
<template>
<div>{{ count }}</div>
<button @click="onAdd">+1</button>
</template>
<script setup lang="ts">
import { reactive, toRefs } from 'vue';
const state = reactive({
count: 0,
text: 'hello'
});
const { count } = toRefs(state);
const onAdd = () => count.value++;
</script>
isRef
检查值是否为一个ref
对象.
customRef
创建一个自定义的 ref
,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收 track
和 trigger
函数作为参数,并且应该返回一个带有 get
和 set
的对象
shallowRef
创建一个跟踪自身 .value 变化的 ref,但不会使其值也变成响应式的。即:改变.value
是响应式的,但是.value
本身不是响应式的
triggerRef
手动执行与 shallowRef
关联的任何副作用。
reactive
返回对象的响应式副本。
<template>
<div>{{ state.count }}</div>
<button @click="onAdd">+1</button>
</template>
<script setup lang="ts">
import { reactive } from 'vue';
const state = reactive({
count: 0,
text: 'hello',
user: {
nickname: 'codebook'
}
});
const onAdd = () => state.count++;
</script>
注意:
reactive
包装的是深度响应式,即:state.user.nickname
改变之后也会触发渲染。如果不想深度响应式可使用shallowReactive
readonly
接受一个对象 (响应式或纯对象) 或 ref
并返回原始对象的只读代理。只读代理是深层的:任何被访问的嵌套属性也是只读的。
<template>
<div>{{ state.count }}</div>
<button @click="onAdd">+1</button>
</template>
<script setup lang="ts">
import { reactive, readonly, watchEffect } from 'vue';
const state = reactive({
count: 0,
text: 'hello'
});
const readonlyState = readonly(state);
watchEffect(() => {
console.log(readonlyState.count);
});
// 触发watchEffect
state.count++;
// 点击不会触发watchEffect,且会出现警告
const onAdd = () => readonlyState.count++;
</script>
isProxy
检查对象是否是由 reactive
或 readonly
创建的 proxy
。
isReactive
检查对象是否是由 reactive
创建的响应式代理。
isReadonly
检查对象是否是由 readonly
创建的只读代理。
toRaw
返回 reactive
或 readonly
代理的原始对象。
markRaw
标记一个对象,使其永远不会转换为 proxy
。返回对象本身。
shallowReactive
创建一个响应式代理,它跟踪其自身 property
的响应性,但不执行嵌套对象的深层响应式转换 (暴露原始值)。
shallowReadonly
创建一个 proxy
,使其自身的 property
为只读,但不执行嵌套对象的深度只读转换 (暴露原始值)。
computed
如果传入的是一个函数,则此函数为getter
函数,如果传入的是个对象,则get
属性函数为getter
,set
属性函数为setter
<template>
<div>{{ msg }}</div>
<div>{{ UpperMsg }}</div>
<button @click="onChangeMsg">改变Msg</button>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
const msg = ref('codebook');
const UpperMsg = computed(() => msg.value.toUpperCase());
const onChangeMsg = () => {
msg.value = 'renzp94';
};
</script>
watchEffect
在响应式地跟踪其依赖项时立即运行一个函数,并在更改依赖项时重新运行它。其返回一个停止函数
<template>
<div>{{ msg }}</div>
<div>{{ UpperMsg }}</div>
<button @click="onChangeMsg">改变Msg</button>
</template>
<script setup lang="ts">
import { computed, ref, watchEffect } from 'vue';
const msg = ref('codebook');
const UpperMsg = computed(() => msg.value.toUpperCase());
const onChangeMsg = () => {
msg.value = 'renzp94';
};
const stop = watchEffect(() => {
console.log(msg.value);
});
onMounted(stop);
</script>
watch
和watchEffect
API 相同,创建后不会立即执行,需要指定监听的数据源
<template>
<div>{{ msg }}</div>
<button @click="onChangeMsg">改变Msg</button>
<div>{{ count }}</div>
<button @click="onAdd">+1</button>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
const msg = ref('codebook');
const onChangeMsg = () => {
msg.value = 'renzp94';
};
const count = ref(0);
const onAdd = () => count.value++;
// 监听单个数据
watch(
() => count.value,
(val) => {
console.log(val);
}
);
// 监听多个数据
watch([count, msg], (vals) => {
console.log(vals);
});
</script>
新增特性
teleport
将一个元素传送到指定位置
<template>
<teleport to="body">
<div>父元素是body</div>
</teleport>
</template>
片段
支持template
中写多个根节点,不再需要指定一个根节点了。
script setup 语法糖
可以在script
标签上指定属性为setup
,则在script
中书写的变量和函数无需导出即可在template
中使用,极大的精简了代码书写,非常 nice 的一个想法。可以看到本文的代码例子都是使用script setup
语法糖。
在style
中使用JS
变量
可以在style
标签中通过v-bind
使用在setup
中导出的变量,而且是响应式的。
<template>
<div class="logo">CodeBook</div>
<button @click="onLargen">变大</button>
<button @click="onLessen">变小</button>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const fontSize = ref('14px');
const onLargen = () => {
fontSize.value = `${parseInt(fontSize.value) + 2}px`;
};
const onLessen = () => {
fontSize.value = `${parseInt(fontSize.value) - 2}px`;
};
</script>
<style scoped>
.logo {
font-size: v-bind(fontSize);
font-weight: 700;
}
</style>
变更
v-model
v-model
用于自定义组件的时候,prop
由value
改为modelValue
,event
由input
改为update:modelValue
,用在原生标签则没有变化。移除了.sync
修饰符,通过v-model:
代替。
AInput.vue
<template>
<div>{{ title }}</div>
<button @click="onUpper">title大写</button>
<div>
<input type="text" :value="modelValue" @input="onInput" />
</div>
</template>
<script setup lang="ts">
import { defineEmit, defineProps } from 'vue';
const props = defineProps({
modelValue: [String, Number],
title: String
});
const emit = defineEmit(['update:modelValue', 'update:title']);
const onInput = (e: Event): void => {
emit('update:modelValue', (e.target as HTMLInputElement).value);
};
const onUpper = () => {
emit('update:title', props.title?.toUpperCase());
};
</script>
App.vue
<template>
<p>
{{ inputValue }}
</p>
<p>
<span>自定义Input</span>
<a-input v-model:title="inputTitle" v-model="inputValue" />
</p>
<p>
<span>原生Input</span>
<input type="text" v-model="inputValue" />
</p>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import AInput from './components/AInput.vue';
const inputValue = ref('');
const inputTitle = ref('codebook');
</script>
自定义组件 ref
自定义组件指定 ref 时,无法直接绑定到内部根元素上,可以通过expose
共享的解决方案解决此问题。
AInput.vue
<template>
<input ref="input" type="text" :value="modelValue" @input="onInput" />
</template>
<script setup lang="ts">
import { defineEmit, defineProps, ref, useContext } from 'vue';
defineProps({
modelValue: [String, Number]
});
const emit = defineEmit(['update:modelValue']);
const onInput = (e: Event): void => {
emit('update:modelValue', (e.target as HTMLInputElement).value);
};
const input = ref<HTMLInputElement | null>(null);
const { expose } = useContext();
const autoFocus = () => {
input.value?.focus();
};
expose({ autoFocus });
</script>
App.vue
<template>
{{ inputValue }}
<a-input ref="input" v-model="inputValue" />
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import AInput from './components/AInput.vue';
const inputValue = ref('');
const input = ref<any>(null);
onMounted(() => {
input.value?.autoFocus();
});
</script>
小改变
v-if
比v-for
的优先级更高。<template v-for />
现在不需要指定到真实节点了,只需指定到template
即可。异步组件
通过defineAsyncComponent
来创建。- 组件中的
prop
的default
函数不能访问this
data
需要始终声明为函数mixin
被设置为浅合并