实现原理:
Object.defineProperty()对属性的读取、修改、拦截存在问题:
解决以上问题:
// 给对象新增属性
this.$set(this.person, 'sex', '女')
// 删除对象的name属性
this.$delete(this.person, 'name')
// 修改数组的第一个元素
this.$set(this.arr, 0, 'xxx')
实现原理:
v-model 默认对应 modelValue prop 和 update:modelValue 事件。子组件可以使用 defineModel()(Vue 3.4+),也可以显式声明 prop 和事件。父组件:
<template>
<Child v-model="isShow" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
const isShow = ref(true);
</script>
子组件:
<template>
<button @click="submit">{{ modelValue ? '关闭' : '打开' }}</button>
</template>
<script setup lang="ts">
const props = defineProps<{
modelValue: boolean;
}>();
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>();
const submit = () => {
emit('update:modelValue', !props.modelValue);
};
</script>
ref 适合基本类型,也适合需要整体替换的对象或数组;通过 .value 的读取和赋值追踪变化。reactive 使用 Proxy 包装对象或数组,适合直接修改其属性或元素。ref 包装对象时,内部值默认会被转换为深层响应式对象。ref 使用 .value;在模板中通常会自动解包。<script setup lang="ts">
import { onMounted, ref } from 'vue';
const myRef = ref<HTMLElement | null>(null);
onMounted(() => {
console.log(myRef.value);
});
</script>
<template>
<div ref="myRef">获取单个 DOM 元素</div>
</template>
reactive 变量会断开原代理的引用;数组的 concat() 会返回新数组,不会修改原数组。import { reactive, ref } from 'vue';
const items = reactive<number[]>([]);
const response = [2, 3, 4, 5];
items.splice(0, items.length, ...response); // 修改原代理,保持响应式
const replaceableItems = ref<number[]>([]);
replaceableItems.value = response; // 需要整体替换时使用 ref
reactive 返回的是代理对象。若把变量重新指向普通数组,已有的代理引用不会跟着变化;用 const 声明时重新赋值还会直接报错。需要替换整个数组时,使用 ref 的 .value;需要保留代理时,使用 splice() 等原地修改方法。两者通常都可以用 const 声明。
import { computed, reactive } from 'vue';
const person = reactive({ firstName: 'Li', lastName: 'Li' });
// 简写
const fullName = computed(() => {
return person.firstName + " " + person.lastName;
});
需要修改计算属性时,使用带 get 和 set 的写法(延续上例的 person):
const fullName = computed({
get() {
return person.firstName + " " + person.lastName;
},
set(val) {
let names = val.split(" ");
person.firstName = names[0];
person.lastName = names[1];
}
});
reactive 对象时会隐式深度监听。由内部属性变更触发时,newValue 与 oldValue 指向同一个对象,不能用它们比较变更前后的快照;immediate 首次回调的旧值为 undefined。import { reactive, ref, watch } from 'vue';
const sum = ref(0);
const msg = ref('');
const person = reactive({ name: 'Li', job: { title: '前端工程师' } });
watch(sum, (newValue, oldValue) => console.log(newValue, oldValue));
watch([sum, msg], ([newSum, newMsg]) => console.log(newSum, newMsg));
watch(person, (newValue, oldValue) => {
console.log(newValue, oldValue); // 内部属性变化时,两者是同一个代理对象
});
watch(() => person.job.title, (newTitle, oldTitle) => {
console.log(newTitle, oldTitle);
});
watch(() => person.job, (newJob) => {
console.log(newJob);
}, { deep: true }); // 需要追踪 job 内部属性时显式开启
watchEffect 会立即执行,并自动追踪回调运行时读取的响应式属性;它不会无条件深度监听整个对象。
不需要预先列出监听源,回调中实际读取的属性会成为依赖。
watchEffect(() => {
console.log(sum.value, msg.value);
});
watchEffect 与 computed 的区别:
| 选项式 API | Hook inside setup |
|---|---|
| beforeCreate | Not needed* |
| created | Not needed* |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
| errorCaptured | onErrorCaptured |
| renderTracked | onRenderTracked |
| renderTriggered | onRenderTriggered |
| activated | onActivated |
| deactivated | onDeactivated |
export default {
setup() {
// mounted
onMounted(() => {
console.log("Component is mounted!");
});
}
};
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了实现这个效果,Vue 有一个 defineAsyncComponent 方法。
<template>
<div>
{{ list }}
</div>
</template>
<script setup lang="ts">
// 模拟http请求
const httpRequest = () => {
return new Promise((resolve) => {
setTimeout(() => {
const arr = [1, 2, 3, 4];
resolve(arr);
}, 3000);
});
};
// 在setup最外层使用await不需要用async
const list = await httpRequest();
</script>
<template>
<div class="bg-slate-50 h-5">
<!-- 使用Suspense包裹异步组件,组件A加载完才会显示 -->
<Suspense>
<!-- 组件加载成功后显示 -->
<template #default> <A></A></template>
<!-- 在A组件加载中时显示 -->
<template #fallback>
<div>loading...</div>
</template>
</Suspense>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent } from "vue";
// 将组件A以异步组件的方式导入
const A = defineAsyncComponent(() => import("./components/A.vue"));
</script>
to 可以是 CSS 选择器或 DOM 元素。目标元素必须在 Teleport 挂载时已经存在;h1 这类选择器只要能匹配到元素,也是有效的。
<!-- 将内容移动到 id 为 popup 的元素下 -->
<Teleport to="#popup">
<video src="./my-movie.mp4" controls></video>
</Teleport>
组件首次挂载时仍会调用 mounted。被 <KeepAlive> 缓存后,在显示与隐藏之间切换会触发 activated 和 deactivated,而不会每次都重新挂载或卸载。
主要用于保留组件状态或避免重新渲染。
<!-- 基本 -->
<keep-alive>
<component :is="view"></component>
</keep-alive>
<!-- 多个条件判断的子组件 -->
<keep-alive>
<comp-a v-if="a > 1"></comp-a>
<comp-b v-else></comp-b>
</keep-alive>
<!-- 和 `<transition>` 一起使用 -->
<transition>
<keep-alive>
<component :is="view"></component>
</keep-alive>
</transition>
<template>
<button @click="flag = !flag">切换按钮</button>
<transition
enter-active-class="animate__animated animate__rubberBand"
leave-active-class="animate__animated animate__rubberBand"
>
<div class="w-20 h-20 bg-red-200" v-show="flag"></div>
</transition>
</template>
用法与 transition 基本一致
<transition-group> 提供了多个元素/组件的过渡效果。默认情况下,它不会渲染一个 DOM 元素包裹器,但是可以通过 tag attribute 来定义。
注意,每个 <transition-group> 的子节点必须有独立的 key,动画才能正常工作。
当一个子节点被更新,从屏幕上的位置发生变化,这个子节点就会产生动画效果。
<transition-group
tag="ul"
enter-active-class="animate__animated animate__rubberBand"
leave-active-class="animate__animated animate__rubberBand"
>
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</transition-group>
父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。

祖先组件中:
<script setup lang="ts">
import { provide, ref } from "vue";
let num = ref<number>(1);
// 为xxx提供一个响应式数据
provide("xxx", num);
</script>
孙组件中:
<script setup lang="ts">
import { inject, type Ref } from "vue";
// 将数据xxx注入,并赋值给data
const data = inject("xxx");
// 点击孙组件的按钮,改变祖先组件的数据
const add = () => {
(data as Ref<number>).value++;
};
</script>
由于 Vue3 没有Prototype 属性 使用 app.config.globalProperties 代替 然后去定义变量和函数
// Vue 2
Vue.prototype.$http = () => {};
// Vue3
const app = createApp({});
app.config.globalProperties.$http = () => {};
举个例子: main.ts 中定义
// ts中需要类型声明
declare module "@vue/runtime-core" {
export interface ComponentCustomProperties {
$hello: () => string;
}
}
// 挂载方法
app.config.globalProperties.$hello = () => {
return "hello";
};
使用全局方法:
<template>
<div>{{ $hello() }}</div>
</template>
<script lang="ts" setup>
import { getCurrentInstance, ComponentInternalInstance } from "vue";
//getCurrentInstance获取的是当前vue实例
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
console.log(proxy?.$hello());
</script>
<script setup><script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。相比于普通的 <script> 语法,它具有更多优势:
要使用这个语法,需要将 setup attribute 添加到 <script> 代码块上:
<script setup>
console.log("hello script setup");
</script>
<script setup>中的代码会在每次组件实例被创建的时候执行。
任何在 <script setup> 声明的顶层的绑定 (包括变量,函数声明,以及 import 引入的内容) 都能在模板中直接使用:
<script setup>
// 变量
const msg = "Hello!";
// 函数
function log() {
console.log(msg);
}
</script>
<template>
<div @click="log">{{ msg }}</div>
</template>
import 导入的内容也会以同样的方式暴露。意味着可以在模板表达式中直接使用导入的 helper 函数,并不需要通过 methods 选项来暴露它:
<script setup>
import { capitalize } from "./helpers";
</script>
<template>
<div>{{ capitalize("hello") }}</div>
</template>
响应式状态需要明确使用响应式APIs来创建。和从 setup() 函数中返回值一样,ref 值在模板中使用的时候会自动解包:
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
<script setup> 范围里的值也能被直接作为自定义组件的标签名使用:
<script setup>
import MyComponent from "./MyComponent.vue";
</script>
<template>
<MyComponent />
</template>
将 MyComponent 看做被一个变量所引用。如果你使用过 JSX,在这里的使用它的心智模型是一样的。其 kebab-case 格式的 <my-component>同样能在模板中使用。不过,我们强烈建议使用 PascalCase 格式以保持一致性。同时也有助于区分原生的自定义元素。
由于组件被引用为变量而不是作为字符串键来注册的,在 <script setup> 中要使用动态组件的时候,就应该使用动态的 :is 来绑定:
<script setup>
import Foo from "./Foo.vue";
import Bar from "./Bar.vue";
</script>
<template>
<component :is="Foo" />
<component :is="someCondition ? Foo : Bar" />
</template>
请注意组件是如何在三元表达式中被当做变量使用的。
一个单文件组件可以通过它的文件名被其自己所引用。例如:名为 FooBar.vue 的组件可以在其模板中用 <FooBar/> 引用它自己。
请注意这种方式相比于 import 导入的组件优先级更低。如果有命名的 import 导入和组件的推断名冲突了,可以使用 import 别名导入:
import { FooBar as FooBarChild } from "./components";
可以使用带点的组件标记,例如 <Foo.Bar> 来引用嵌套在对象属性中的组件。这在需要从单个文件中导入多个组件的时候非常有用:
<script setup>
import * as Form from "./form-components";
</script>
<template>
<Form.Input>
<Form.Label>label</Form.Label>
</Form.Input>
</template>
全局注册的自定义指令将以符合预期的方式工作,且本地注册的指令可以直接在模板中使用,就像上文所提及的组件一样。
本地自定义指令需要以 vNameOfDirective 的形式命名,才能直接在模板中使用。
<script setup lang="ts">
import type { Directive } from 'vue';
const vBackground: Directive<HTMLElement, string> = {
mounted(el, binding) {
el.style.background = binding.value;
},
updated(el, binding) {
el.style.background = binding.value;
}
};
</script>
<template>
<div v-background="'red'">示例</div>
</template>
<script setup lang="ts">
import type { Directive } from 'vue';
// 函数简写会在 mounted 和 updated 时调用
const vBackground: Directive<HTMLElement, string> = (el, binding) => {
el.style.background = binding.value;
};
</script>
在 <script setup> 中必须使用 defineProps 和 defineEmits API 来声明 props 和 emits ,它们具备完整的类型推断并且在 <script setup> 中是直接可用的:
<script setup>
const props = defineProps({
foo: String
});
const emit = defineEmits(["change", "delete"]);
// setup code
</script>
defineProps 和 defineEmits 都是只在 <script setup> 中才能使用的编译器宏。他们不需要导入且会随着 <script setup> 处理过程一同被编译掉。defineProps 接收与 props 选项相同的值,defineEmits 也接收 emits 选项相同的值。defineProps 和 defineEmits 在选项传入后,会提供恰当的类型推断。defineProps 和 defineEmits 的选项会从 setup 中提升到模块的范围。因此,传入的选项不能引用在 setup 范围中声明的局部变量。这样做会引起编译错误。但是,它可以引用导入的绑定,因为它们也在模块范围内。如果使用 TypeScript,也可以通过类型声明 props 和 emits。
使用 <script setup> 的组件是默认关闭的,也即通过模板 ref 或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定。
为了在 <script setup> 组件中明确要暴露出去的属性,使用 defineExpose 编译器宏:
<script setup>
import { ref } from "vue";
const a = 1;
const b = ref(2);
defineExpose({
a,
b
});
</script>
当父组件通过模板 ref 的方式获取到当前组件的实例,获取到的实例会像这样 { a: number, b: number } (ref 会和在普通实例中一样被自动解包)
在 <script setup> 使用 slots 和 attrs 的情况应该是很罕见的,因为可以在模板中通过 $slots 和 $attrs 来访问它们。在你的确需要使用它们的罕见场景中,可以分别用 useSlots 和 useAttrs 两个辅助函数:
<script setup>
import { useSlots, useAttrs } from "vue";
const slots = useSlots();
const attrs = useAttrs();
</script>
useSlots 和 useAttrs 是真实的运行时函数,它会返回与 setupContext.slots和 setupContext.attrs 等价的值,同样也能在普通的组合式 API 中使用。
<script> 一起使用<script setup> 可以和普通的 <script> 一起使用。普通的 <script> 在有这些需要的情况下或许会被使用到:
<script setup> 声明的选项,例如 inheritAttrs 或通过插件启用的自定义的选项。<script>
// 普通 <script>, 在模块范围下执行(只执行一次)
runSideEffectOnce();
// 声明额外的选项
export default {
inheritAttrs: false,
customOptions: {}
};
</script>
<script setup>
// 在 setup() 作用域中执行 (对每个实例皆如此)
</script>
render 函数。请使用一个普通的 <script> 结合 setup 选项来代替。<script setup> 中可以使用顶层 await。结果代码会被编译成 async setup():
<script setup>
const post = await fetch(`/api/post/1`).then((r) => r.json());
</script>
另外,await 的表达式会自动编译成在 await 之后保留当前组件实例上下文的格式。
await 的组件会变成 async setup(),需要由 <Suspense> 包裹。<Suspense> 仍属于实验性特性,升级 Vue 时应核对其行为。props 和 emits 都可以使用传递字面量类型的纯类型语法做为参数给 defineProps 和 defineEmits 来声明:
const props = defineProps<{
foo: string;
bar?: number;
}>();
const emit = defineEmits<{
(e: "change", id: number): void;
(e: "update", value: string): void;
}>();
defineProps 或 defineEmits 只能是要么使用运行时声明,要么使用类型声明。同时使用两种声明方式会导致编译报错。foo: string 会推断为 foo: String;无法静态分析的复杂类型会退化为 null。['foo', 'bar'])。defineProps 中引用导入的类型,但基于 AST 的转换仍无法处理某些需要完整类型分析的复杂类型表达式。仅限类型的 defineProps 声明的不足之处在于,它没有可以给 props 提供默认值的方式。为了解决这个问题,提供了 withDefaults 编译器宏:
interface Props {
msg?: string;
labels?: string[];
}
const props = withDefaults(defineProps<Props>(), {
msg: "hello",
labels: () => ["one", "two"]
});
上面代码会被编译为等价的运行时 props 的 default 选项。此外,withDefaults 辅助函数提供了对默认值的类型检查,并确保返回的 props 的类型删除了已声明默认值的属性的可选标志。