本文是Vue3学习记录,适合Vue3初学者,大牛可以跳过。
目前完成部分,持续更新。完整代码见 代码仓库
Vue3相比Vue2的优势
-
更快 Proxy实现响应式;diff算法优化,添加PatchFlag;hoistStatic静态提升;cacheHandler事件监听缓存;SSR优化
-
更小 移除一些不常用的API;tree-shaking,仅打包需要的
-
更好 组合式API,提高代码组织、逻辑复用、可读性、维护性;TypeScript支持;Fragments、Teleport、Suspense等
选项式和组合式API
Vue2 是选项API(Options API),选项包括data、props、methods、computed、watch、生命周期钩子等,一个逻辑的代码分散在各处,导致代码的可读性变差。
Vue3 组合式API(Composition API)则很好地解决了这个问题,可将同一逻辑的内容写到一起,增强了代码的可读性、内聚性。
生命周期
setup函数
-
在组件中使用组合式 API 的入口
-
只会在组件初始化的时候执行一次
-
在beforeCreate之前执行
生命周期
- Vue3中可以使用Vue2的生命周期,但beforeDestroy改为beforeUnmount,destroyed改为 unmounted
- Vue3大部分生命周期名称是在Vue2的前面加 + “on”
vue2 | vue3 |
---|---|
beforeCreate | 不需要 |
created | 不需要 |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
OptionsDemo.vue:
<template>
<div>
<h2>{{ msg }}</h2>
</div>
</template>
<script>
export default {
props: ["type"],
data(props) {
return {
msg: `${props.type} API`,
};
},
beforeCreate() {
console.log(this.type, "beforeCreate");
},
created() {
console.log(this.type, "created");
},
beforeMount() {
console.log(this.type, "beforeMount");
},
mounted() {
console.log(this.type, "mounted");
// 1s后触发组件更新
setTimeout(() => {
this.msg += " 生命周期";
}, 1000);
},
beforeUpdate() {
console.log(this.type, "beforeUpdate");
},
updated() {
console.log(this.type, "updated");
},
// beforeDestroy 改名
beforeUnmount() {
console.log(this.type, "beforeUnmount");
},
// destroyed 改名
unmounted() {
console.log(this.type, "unmounted");
},
};
</script>
<style lang="scss" scoped>
</style>
CompositionDemo.vue:
<template>
<div>
<h2>{{ msg }}</h2>
</div>
</template>
<script>
import {
ref,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
} from "vue";
export default {
props: ["type"],
setup(props) {
const type = props.type
const msg = ref(`${type} API`);
// beforeCreate和created之前执行
console.log(type, "setup");
onBeforeMount(() => {
console.log(type, "onBeforeMount");
});
onMounted(() => {
console.log(type, "onMounted");
// 1s后触发组件更新
setTimeout(() => {
msg.value += " 生命周期";
}, 1000);
});
onBeforeUpdate(() => {
console.log(type, "onBeforeUpdate");
});
onUpdated(() => {
console.log(type, "onUpdated");
});
onBeforeUnmount(() => {
console.log(type, "onBeforeUnmount");
});
onUnmounted(() => {
console.log(type, "onUnmounted");
});
return {
msg,
type,
};
},
beforeCreate() {
console.log("composition beforeCreate");
},
created() {
console.log("composition created");
},
};
</script>
<style lang="scss" scoped>
</style>
上面两个的父组件IndexDemo:
<template>
<div>
<OptionsDemo type='options' />
<CompositionDemo type='composition' />
</div>
</template>
<script>
import OptionsDemo from "./OptionsDemo.vue";
import CompositionDemo from "./CompositionDemo.vue";
export default {
name: "Api-style",
components: {
OptionsDemo,
CompositionDemo,
},
};
</script>
<style lang="scss" scoped>
</style>
实例化
Vue2是new Vue({}),Vue3是createApp()
其他
- Vue3移除了filter
- setup中使用getCurrentInstance获取this, 使用getCurrentInstance().appContext.config.globalProperties获取全局属性
- 获取全局app上的属性通过app.config.globalProperties
- 很多接口由Vue.XXX变成了app.XXX,比如注册组件 Vue.component变成app.component,自定义指令Vue.directive变成app.directive
响应式
ref
- 生成值类型的响应式数据
- 除了在模板和reactive中使用外,其他地方都要用.value读取、修改值
RefDemo.vue
<template>
<div>
<h2>{{ msg }}</h2>
count: {{ count }} <button @click="countClickHandler">+1</button>
</div>
</template>
<script>
// import { ref } from "vue";
// export default {
// setup() {
// const msg = ref("Ref Demo");
// const count = ref(0);
// const countClickHandler = () => {
// count.value++;
// };
// return {
// msg,
// count,
// countClickHandler,
// };
// },
// };
//
</script>
<script setup>
import { ref } from "vue";
const msg = ref("Ref Demo");
const count = ref(0);
const countClickHandler = () => {
count.value++;
};
</script>
<style lang="scss" scoped>
</style>
reactive
- 生成对象、数组类型的响应式数据
- 解构后的值,不具有响应式
ReactiveDemo.vue:
<template>
<div>
<h2>{{ msg }}</h2>
<p>
{{ user.name }} {{ user.age }} {{ user.gender === "m" ? "男" : "女" }}
</p>
</div>
</template>
<script setup>
import { ref, reactive } from "vue";
const msg = ref("Reactive Demo");
const ageRef = ref(20);
const user = reactive({
name: "jim",
gender: "m",
age: ageRef,
});
setTimeout(() => {
console.log("age++");
ageRef.value++;
}, 2000);
</script>
<style lang="scss" scoped>
</style>
toRef
- 针对一个响应式对象的prop,创建一个ref,具有响应式
- 该ref和响应式对象的prop保持引用关系
ToRefDemo.vue:
<template>
<div>
<h2>{{ msg }}</h2>
<p>
{{ user.name }} {{ user.age }} {{ user.gender === "m" ? "男" : "女" }}
</p>
<p>age+1:{{ AgePlusOne }}</p>
</div>
</template>
<script setup>
import { ref, reactive, toRef, computed, watch, watchEffect } from "vue";
const msg = ref("ToRef Demo");
const user = reactive({
name: "jim",
gender: "m",
age: 20,
});
const age = toRef(user, "age");
setTimeout(() => {
console.log("age++");
age.value++;
}, 2000);
const AgePlusOne = computed(() => {
return user.age + 1;
});
watch(() => user.age, (val, oldVal) => {
console.log("watch age triggered.", val, oldVal);
});
watchEffect(() => {
console.log("watchEffect age triggered.", user.age);
});
</script>
<style lang="scss" scoped>
</style>
toRefs
- 针对响应式对象的每个prop都创建对应ref,具有响应式
- 保持引用关系
ToRefsDemo.vue:
<template>
<div>
<h2>{{ msg }}</h2>
<p>{{ name }} {{ age }} {{ gender === "m" ? "男" : "女" }}</p>
</div>
</template>
<script setup>
import { ref, reactive, toRefs } from "vue";
const msg = ref("ToRefs Demo");
const user = reactive({
name: "jim",
gender: "m",
age: 20,
});
setTimeout(() => {
console.log("age++");
user.age++;
user.name += '又老了一岁'
}, 2000);
// 注意:直接解构响应式对象得到的属性不是响应式的,以下写法2s后age++页面不更新
// const { name, age, gender } = user;
const { name, age, gender } = toRefs(user);
</script>
<style lang="scss" scoped>
</style>
v-model参数
ChildComp.vue:
<template>
<input :value="name" @input="emit('update:name', $event.target.value)" />
<input
type="number"
min="0"
max="200"
:value="age"
@input="emit('update:age', $event.target.value)"
/>
</template>
<script setup>
import { defineProps, defineEmits, toRefs } from "vue";
const props = defineProps({
name: {
type: String,
default: "",
},
age: {
type: [Number, String],
default: 20,
},
});
const emit = defineEmits({
"update:name": null,
// age校验
"update:age": (val) => {
if (Number(val) >= 0 && Number(val) <= 200) {
return true;
}
setTimeout(() => {
alert('age check failed!!!');
}, 200);
return false;
},
});
const { name, age } = toRefs(props);
</script>
<style lang="scss" scoped>
</style>
上面组件的父组件IndexDemo.vue:
<template>
<h2>{{ msg }}</h2>
<p>name: {{ name }} age: {{ age }} <button @click="resetAge">修改age</button></p>
<!-- 两种写法等价 -->
<ChildComp v-model:name="name" v-model:age="age"></ChildComp>
<ChildComp
v-model:name="name"
:age="age"
@update:age="age = $event"
></ChildComp>
</template>
<script setup>
import { reactive, toRefs, ref } from "vue";
import ChildComp from "./ChildComp.vue";
const user = reactive({
name: "chang bai",
age: 20,
});
const msg = ref("v-model参数");
const { name, age } = toRefs(user);
const resetAge = () => {
age.value = 20;
};
</script>
<script>
export default {
components: {
ChildComp,
},
};
</script>
接口相关
Fragment
Vue2模板必须有个根节点,Vue3不强求,模板可以有多个根节点(或称他们为兄弟节点,没有父节点)。
FragmentDemo.vue:
<template>
<h2>{{ msg }}</h2>
<p>name: {{ name }} age: {{ age }}</p>
</template>
<script setup>
import { ref, reactive, toRefs } from "vue";
const msg = ref("Fragment Demo");
const user = reactive({
name: "chang bai",
age: 20,
});
const { name, age } = toRefs(user);
</script>
<style lang="scss" scoped>
</style>
Teleport
传送门,可以将当前组件内部分DOM节点移动到指定位置,比如body,Dialog就是这种。
TeleportDemo.vue:
<template>
<h2>{{ msg }}</h2>
<teleport to="body" v-if="isOpen">
<div class="mask" @click="toggleOpen">
<div class="center" @click.stop="">
<div class="header">这是header</div>
<div class="main">点击mask关闭</div>
<div class="footer">这是footer</div>
</div>
</div>
</teleport>
<p>
isOpen: {{ isOpen }}
<button @click="toggleOpen">{{ isOpen ? "关闭" : "打开" }}</button>
</p>
</template>
<script setup>
import { ref, reactive, toRefs } from "vue";
const msg = ref("Teleport Demo");
const isOpen = ref(false);
const toggleOpen = () => {
isOpen.value = !isOpen.value;
};
</script>
<style scoped>
.mask {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
position: fixed;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
.center {
width: 300px;
height: 200px;
border-radius: 10px;
background: #fff;
display: flex;
flex-direction: column;
align-items: center;
}
.header,
.footer {
padding: 20px;
width: 100%;
box-sizing: border-box;
text-align: center;
}
.main {
flex: 1;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>
Suspense
异步组件,配合defineAsyncComponent使用,可以在加载完成前渲染自定义内容,比如loading。
FetchDataDemo.vue:
<template>
<div>{{ mockData }}</div>
</template>
<script>
export default {
async setup(props) {
const fn = async () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("获取数据成功!");
}, 2000);
});
};
const mockData = await fn()
return {
mockData,
};
},
};
</script>
<style lang="scss" scoped>
</style>
上面组件的父组件SuspenseDemo.vue:
<template>
<h2>{{ msg }}</h2>
<Suspense>
<!-- 加载完成渲染的内容 -->
<template #default>
<DialogDemo></DialogDemo>
</template>
<!-- 加载中显示的内容 -->
<template #fallback>
<span>{{ loadingInfo }}</span>
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent, ref } from "vue";
const DialogDemo = defineAsyncComponent(() => import("./FetchDataDemo.vue"));
const msg = ref("Suspense Demo");
const loadingInfo = ref("加载中...");
</script>
<style lang="scss" scoped>
</style>