从Vue2迁移Vue3的全攻略

作者:renzp94
时间:2021-06-05 08:03:16

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函数有两个参数: propsctx,其中 ctx 是一个对象({attrs,slots,emit}).

  • attrs: 绑定在组件中的所有显示指定props,这里需要特别注意的是在Vue3中如果需要将属性绑定到组件根节点需要声明props,对应Vue2this.$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,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收 tracktrigger 函数作为参数,并且应该返回一个带有 getset 的对象

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

检查对象是否是由 reactivereadonly 创建的 proxy

isReactive

检查对象是否是由 reactive 创建的响应式代理。

isReadonly

检查对象是否是由 readonly 创建的只读代理。

toRaw

返回 reactivereadonly 代理的原始对象。

markRaw

标记一个对象,使其永远不会转换为 proxy。返回对象本身。

shallowReactive

创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (暴露原始值)。

shallowReadonly

创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值)。

computed

如果传入的是一个函数,则此函数为getter函数,如果传入的是个对象,则get属性函数为getterset属性函数为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

watchEffectAPI 相同,创建后不会立即执行,需要指定监听的数据源

<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用于自定义组件的时候,propvalue改为modelValueeventinput改为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-ifv-for的优先级更高。
  • <template v-for />现在不需要指定到真实节点了,只需指定到template即可。
  • 异步组件通过defineAsyncComponent来创建。
  • 组件中的propdefault函数不能访问this
  • data需要始终声明为函数
  • mixin被设置为浅合并