defineProps
用于接收父组件传递的属性值。
父组件:
<!-- 父组件 -->
<template>
<Child1 str="字符串" :num="num" />
-----------------
<Child2 str="字符串" :num="num" />
</template>
<script setup>
import { ref } from 'vue';
import Child1 from './views/Child1.vue';
import Child2 from './views/Child2.vue';
const num = ref(18)
setInterval(() => {
num.value++
}, 1000);
</script>
子组件1:
<!-- Child1 -->
<template>
<div>
<p>{{ str }}</p>
<!-- 两种写法都可以 -->
<p>{{ props.str }}</p>
<p>{{ num }}</p>
<p>{{ obj }}</p>
<p>{{ fun }}</p>
<!-- 会报错 -->
<!-- v-model cannot be used on a prop, because local prop bindings are not writable. -->
<!-- <input v-model="str" /> -->
<!-- 在 input 框中输入值并不会引起 str 的变化 -->
<input v-model="newStr" />
</div>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
str: String,
/**
* 通过 defineProps 定义的 props 是响应式的。
* 这意味着当父组件传递给子组件的 prop 值发生变化时,
* 子组件中的相应 prop 也会自动更新,
* 并且任何依赖于这些 props 的计算属性、侦听器(watchers)或模板都会更新。
*/
num: {
type: Number,
default: 0
},
obj: {
type: Object,
default: () => {
return {}
}
},
fun: {
type: Function,
default: null
}
});
console.log("child1 created props:", props, typeof props); // Proxy 对象
console.log("child1 created str:", props.str, typeof props.str); // 字符串
console.log("child1 created num:", props.num);
console.log("child1 created obj:", props.obj, typeof props.obj); // object
console.log("child1 created fun:", props.fun); // object
const newStr = ref(props.str)
console.log("child2 newStr:", newStr.value)
</script>
子组件2:
<!-- Child2 -->
<template>
<div>
<p>{{ str }}</p>
<!-- 这里就不能这样写了,会报错 -->
<!-- <p>{{ props.str }}</p> -->
<p>{{ num }}</p>
<p>{{ obj }}</p>
<p>{{ fun }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const { str, num, obj, fun } = defineProps({
str: String,
num: {
type: Number,
default: 0
},
obj: {
type: Object,
default: () => {
return {}
}
},
fun: {
type: Function,
default: null
}
});
console.log("child2 created str:", str);
console.log("child2 created num:", num);
console.log("child2 created obj:", obj);
console.log("child2 created fun:", fun);
/**
* 在 Vue 3 中,通过 defineProps 定义的 props 是只读的,不能直接修改。
* 这是为了确保数据流保持单向,即父组件传递给子组件的数据不会被子组件意外地改变。
* 非要修改只能使用计算属性或者创建一个响应式变量
*/
// str = '改变字符串' // 会报错
const changeStr1 = computed(() => {
return `使用计算属性改变${str}`
})
console.log("child2 changeStr1:", changeStr1.value);
const changeStr2 = ref(str)
changeStr2.value = `使用响应式变量改变${str}`
console.log("child2 changeStr2:", changeStr2.value);
</script>
defineProps() 返回的是一个 Proxy 对象,它既不是 ref 对象,也不是 reactive 对象。
defineProps() 返回的对象的属性值是普通数据类型或普通对象,也不是 ref/reactive 对象。
defineEmits
用在子组件中。表面上看它的作用似乎是用于子组件调用父组件的方法。
更准确的说法是用来定义子组件可以发出的事件,父组件可以通过监听这些事件来响应子组件的行为。
它实际上是在告诉父组件:当‘我’使用 emit
的时候,你父组件需要做出相应的响应,你想怎么响应是你父组件自己的事情。
<!-- 子组件 -->
<template>
<button @click="handleClick">子组件按钮</button>
</template>
<script setup>
// 定义可以发出的事件
const emit = defineEmits(['update'])
function handleClick() {
// 发出 'update' 事件给父组件
emit('update', '我是参数')
}
</script>
<!-- 父组件 -->
<template>
<!-- 监听子组件的 'update' 事件 -->
<Child1 @update="handleUpdate" />
</template>
<script setup>
import Child1 from './views/Child1.vue';
function handleUpdate(params) {
// 父组件做出响应:打印参数值
console.log("params:", params);
}
</script>
defineExpose
用在子组件中,用于暴露子组件实例的方法和数据。它可以让父组件通过 ref
获取子组件的特定方法或数据。
<!-- 子组件 -->
<template>
子组件
</template>
<script setup>
import { ref } from 'vue';
const str = ref('子组件字符串')
const fun = function () {
console.log("子组件方法触发...");
}
const other = '其他'
defineExpose({
str, // ES6 简化写法
num: 18,
fun: fun
})
</script>
<!-- 父组件 -->
<template>
<Child1 ref="child1" />
<button @click="handleClick">
父组件按钮
</button>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import Child1 from './views/Child1.vue';
const child1 = ref(null)
console.log("created str:", child1.value.str); // 第 15 行,报错
onMounted(() => {
console.log("mounted str:", child1.value.str); // 子组件字符串
})
function handleClick() {
console.log("str:", child1.value.str); // 子组件字符串
console.log("num:", child1.value.num); // 18
child1.value.fun() // 子组件方法触发...
console.log("other:", child1.value.other); // undefined
}
</script>
为什么第 15 行会报错?
因为在 setup
函数执行时,子组件还没有被挂载到 DOM 上,组件的实例也没有准备好。因此,此时 child1.value
为 null
。
为什么 child1.value.other 是 undefined?
因为子组件中没有通过 defineExpose
来暴露 other
defineAsyncComponent
异步组件。可以理解为延迟加载或按需加载。比如当一个大型页面中包含很多个子组件,如果一次性加载所有内容势必会导致页面渲染速度过慢,这时候就可以对那些不需要第一时间就加载的、或者需要经过某些操作后才加载的子组件使用异步组件。
同步组件写法:
<!-- App.vue -->
<template>
<button @click="handleClick">加载子组件</button>
<SyncComponent v-if="show" />
</template>
<script setup>
import { ref } from 'vue';
import SyncComponent from "./views/Child1.vue";
const show = ref(false)
function handleClick() {
show.value = true
}
</script>
异步组件写法:
<!-- App.vue -->
<template>
<button @click="handleClick">加载子组件</button>
<AsyncComponent v-if="show" />
</template>
<script setup>
import { defineAsyncComponent, ref } from 'vue';
const AsyncComponent = defineAsyncComponent(() => import('./views/Child1.vue'))
const show = ref(false)
function handleClick() {
show.value = true
}
</script>
检查控制台发现:无论同步组件还是异步组件,页面元素都没有渲染子组件内容。
那么他们有什么区别?
区别是你可以在控制台的 Network 中发现:
-
同步组件会在页面初始化时一次性加载 App.vue 和 Child1.vue
-
异步组件在页面初始化时只加 App.vue,当点击按钮时再加载 Child1.vue。
知道了 defineAsyncComponent 的作用,下面详细介绍 defineAsyncComponent 的用法:
defineAsyncComponent 方法接收一个返回 Promise 的加载函数:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
ES6 模块动态导入也会返回一个 Promise,所以也可以这样写:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
异步组件会将接收到的 props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。
异步操作不可避免地会涉及到加载和错误状态,因此 defineAsyncComponent 也支持在高级选项中处理这些状态:
import LoadingComponent from './views/Loading.vue';
import ErrorComponent from './views/Error.vue';
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./views/Child1.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 2000,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了,
// 也会显示加载失败后展示的组件,默认值是:Infinity
timeout: 3000
})
注意:delay: 2000 并不是指等待 2s 后才开始加载异步组件,而是指在异步组件开始加载后,等待 2s 再显示 loadingComponent。
当使用服务器端渲染时还可以配置:在空闲时进行激活、在可见时激活、自定义策略等等…这里不做拓展,详情可以直接访问官网。
Suspense
[səˈspens]
Suspense 是一个包裹异步组件的容器组件,用来处理异步组件加载期间的 UI 状态。
Suspense 组件有两个插槽:
-
#default
:默认插槽,这个插槽用于放置异步组件。当异步组件加载完成后,#default 插槽中的内容将被渲染。 -
#fallback
:备用插槽,当异步组件正在加载时,#fallback 插槽中的内容会被渲染。
父组件:
<!-- 父组件 -->
<template>
<div>我是父组件内容</div>
<Suspense>
<AsyncComponent />
<template #fallback>
<!-- <h1>正在加载中...</h1> -->
<LoadingComponent />
</template>
</Suspense>
</template>
<script setup>
import LoadingComponent from "./views/LoadingComponent.vue";
import { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent(function () {
return new Promise(resolve => {
setTimeout(() => {
resolve(import('./views/AsyncComponent.vue'))
}, 5000);
});
})
</script>
异步子组件:
<!-- 异步子组件 -->
<template>
<div>我是异步子组件</div>
</template>
<script setup>
import { onMounted } from 'vue';
onMounted(() => {
console.log("子组件 onMounted 执行");
})
</script>
备用组件 LoadingComponent:
<!-- 备用组件 -->
<template>
<div>
加载中...
</div>
</template>
5 秒后页面内容替换:
注意点:#default 和 #fallback 两个插槽都只允许一个直接子节点。
<template #fallback> Loading... </template>
<template #fallback>
<LoadingComponent />
</template>
<template #fallback>
<h1>正在加载中...</h1>
</template>
都是可以的,但是
<template #fallback>
哈哈
<h1>正在加载中...</h1>
</template>
就会报错。因为它有两个直接子节点。
Suspense 组件会触发三个事件:pending
、fallback
、resolve
。
-
pending
事件是在进入挂起状态时触发。 -
fallback
事件是在 #fallback 插槽的内容显示时触发。 -
resolve
事件是在 #default 插槽完成获取新内容时触发。
<template>
<div>我是父组件内容</div>
<Suspense
@pending="handlePending"
@fallback="handleFallback"
@resolve="handleResolve"
>
<AsyncComponent />
<template #fallback>
<h1>正在加载中...</h1>
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent(function () {
return new Promise(resolve => {
setTimeout(() => {
resolve(import('./views/AsyncComponent.vue'))
}, 5000);
});
})
let num = 1
setInterval(() => {
console.log(num++);
}, 1000)
function handlePending() {
console.log("pending...");
}
function handleFallback() {
console.log("fallback...");
}
function handleResolve() {
console.log("resolve...");
}
</script>
defineAsyncComponent 的实际作用:
手摸手教你利用defineAsyncComponent实现长页面按需加载组件
路由懒加载:
const routes = [
{
path: '/dashboard',
component: defineAsyncComponent(() => import('./views/Dashboard.vue'))
},
{
path: '/profile',
component: defineAsyncComponent(() => import('./views/Profile.vue'))
}
];
通过 Vue Router 的懒加载机制,只有在用户访问特定路由时,相关页面组件才会被加载。
拓展:CommonJS 的 require() 也可以实现路由懒加载。
defineOptions
在 Vue 3.3 及之后的版本中,defineOptions 是一个新引入的宏(macro),它允许开发者在 <script setup> 语法糖中声明组件的选项(options)。
这个特性解决了之前需要额外编写一个非 setup 的<script>标签来配置选项的问题。
// 设置组件名并禁止属性继承
defineOptions({
name: 'MyComponent', // 组件名称
inheritAttrs: false // 禁止属性继承
});
注意:
可以在<script setup>之外使用<script>标签来配置选项,但是<script setup>和普通<script>中的 setup() 函数不能同时用来定义响应式数据。
例如:
<template>
<div>
{{ name }}
{{ age }}
</div>
</template>
<script setup>
import { ref } from 'vue';
const name = ref('张三');
</script>
<script>
import { ref } from 'vue';
export default {
setup() {
const age = ref(10);
return { age };
}
}
</script>
同时使用了<script setup>和 setup(),则页面不会按预期展示。
<template>
<div>
{{ name }}
{{ age }}
</div>
</template>
<script setup>
import { ref } from 'vue';
const name = ref('张三');
const age = ref(10);
</script>
<script>
export default {
// 这里可以配置选项式 API 的内容,比如 props、emits、components 等
props: {
// 示例 props 配置
someProp: String
},
emits: ['someEvent'],
components: {
// 示例组件配置
// SomeComponent
}
}
</script>
在这个示例里,<script setup>负责定义响应式数据和逻辑,普通<script>负责配置选项式 API 的内容。这样就能把两种语法风格结合起来使用。
defineComponent
从 API 名称来看,意思是定义一个组件,是 Vue 3 中引入的一个辅助函数,主要用于 TypeScript 项目中。它允许你在定义组件选项时获得更好的类型推断和 IDE(如 VSCode)中的自动补全功能。通过使用 defineComponent,IDE 能够识别这是一个 Vue 组件,并据此提供 Vue 特有的 API 提示和类型检查。
什么意思呢?
在 Vue2 中,我们会习惯这样写:
export default {
//...
}
这个时候,对于开发工具而言,{} 只是一个普通的 Object 对象,开发工具不会对一个普通对象做任何特殊的处理。
但是增加一层 defineComponet 的话:
export default defineComponent({
//...
})
你实际上就是在告诉开发工具,我使用的是 Vue3,你需要给我一些 Vue3 相关的自动提示。这样在你写代码的时候,开发工具会给出更多的一些自动提示帮你补全代码。
核心源码:
var Vue = (function (exports) {
// 定义组件
function defineComponent(options) {
return isFunction(options) ? { setup: options, name: options.name } : options;
}
exports.defineComponent = defineComponent;
}({}));
参数 options 是一个选项对象或 setup 函数。
什么是选项对象?
Vue2 中写的:
export default {
data() {
// ...
},
methods: {
// ...
}
}
这些就是选项对象。
defineSlots
与 defineComponent 类似,辅助功能,用于类型检查和 IDE 的自动补全,主要用于 TypeScript 环境下。
defineCustomElement
defineCustomElement 作用是定义一个自定义元素。在 Vue 中我们可以自己写 .vue 文件封装组件,那么它存在的意义是什么呢?
在 Vue 3 中,defineCustomElement 是一种特殊的 API,它允许你将 Vue 组件转换为 Web Components,这样它们就能在任何现代浏览器中作为原生的自定义 HTML 元素使用,而不仅仅是在 Vue 应用中使用。与常规的 Vue 组件不同,使用 defineCustomElement 创建的组件可以独立于 Vue 环境运行,也可以在非 Vue 项目中使用。
这个方法接收的参数和 defineComponent 完全相同。但它会返回一个继承自 HTMLElement
的自定义元素构造器:
import { defineCustomElement } from 'vue'
const MyVueElement = defineCustomElement({
// 这里是同平常一样的 Vue 组件选项
props: {},
emits: {},
template: `...`,
// defineCustomElement 特有的:注入进 shadow root 的 CSS
styles: [`/* inlined css */`]
})
// 注册自定义元素
// 注册之后,所有此页面中的 `<my-vue-element>` 标签都会被升级
customElements.define('my-vue-element', MyVueElement)
有兴趣的可以看看 Web Component 的相关知识 【zh-CN】。可以将 Web Components 简单理解为一个自定义的 HTML 标签。
用的很少,就不做具体研究。
defineModel
仅在 3.4+ 中可用
v-model
defineModel 就是简化 v-model 实现过程
下面使用 defineModel 来实现双向数据绑定的例子:
父组件:
<!-- 父组件 -->
<template>
<div>
<p>Count in parent: {{ count }}</p>
<Children v-model="count" />
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import Children from './Children.vue';
const count = ref(1)
watch(count, (newVal, oldVal) => {
console.log(newVal, oldVal, typeof newVal) // number 类型
})
</script>
子组件:
<!-- 原子组件代码 -->
<template>
<div>
<!--
这里不能直接 v-model="modelValue" 会报编译错误
v-model cannot be used on a prop, because local prop bindings are not writable.
-->
<el-select v-model="selectValue" @change="handleChange">
<el-option label="选项1" :value="1" />
<el-option label="选项2" :value="2" />
<el-option label="选项3" :value="3" />
</el-select>
</div>
</template>
<script setup>
import { ref, defineProps, defineEmits } from 'vue'
const props = defineProps({
modelValue: Number
})
const emit = defineEmits(['update:modelValue'])
const selectValue = ref(props.modelValue)
function handleChange() {
console.log('选项变化了');
emit('update:modelValue', selectValue.value);
}
</script>
<!-- 使用 defineModel 的子组件代码 -->
<template>
<div>
<el-select v-model="selectValue" @change="handleChange">
<el-option label="选项1" :value="1" />
<el-option label="选项2" :value="2" />
<el-option label="选项3" :value="3" />
</el-select>
</div>
</template>
<script setup>
import { ref, defineProps, defineEmits } from 'vue'
// const props = defineProps({
// modelValue: Number
// })
// const emit = defineEmits(['update:modelValue'])
// const selectValue = ref(props.modelValue)
// function handleChange() {
// console.log('选项变化了');
// emit('update:modelValue', selectValue.value);
// }
const selectValue = defineModel()
function handleChange() {
console.log('选项变化了:', selectValue.value);
// emit('update:modelValue', selectValue.value);
}
</script>
非常简单!
defineModel 就是封装了之前的实现过程:在子组件内定义了一个叫 selectValue
的 ref 变量(当然也可以取别的变量名)和名字叫 modelValue
的 props,并且 watch
了 props 中的 modelValue
。当父组件改变 modelValue
的值后会同步更新 selectValue
变量的值;当子组件改变 selectValue
变量的值后会调用 update:modelValue
事件,父组件收到这个事件后就会更新父组件中对应的变量值。
defineModel 中的 type 和 default
默认情况就使用:
const model = defineModel();
如果想定义类型:
const model = defineModel({ type: String })
类型 + 默认值:
const model = defineModel({ type: String, default: "张三" });
自定义属性名:
<!-- 父组件 -->
<Children v-model:aa="count"></Children>
// 子组件
const model = defineModel('aa', { type: String, default: "张三" })
绑定多个属性:
<!-- 父组件 -->
<Children v-model:name="myName" v-model:age="myAge"></Children>
// 子组件
const model1 = defineModel('name')
const model2 = defineModel('age', { type: Number, default: 8 })
setup() 和 <script setup> 的区别
编译
在编译时 setup() 难以进行深度静态分析, 因为它的返回值是动态的(比如返回的对象可能包含运行时才能确定的属性或方法)。
<script setup>是编译时语法糖,它的顶层绑定(变量、函数、import 等)是直接暴露给模板的,编译器可以明确知道哪些内容会被模板使用,从而进行更多优化,例如更好的 Tree-shaking:未在模板中使用的代码可以被标记并移除。
总结:
Vue 编译器可以对<script setup>语法糖内部的代码进行静态分析,从而进行更多的编译时优化,减少运行时的开销,提高组件的渲染性能。因此在性能优化上 setup() 不如<script setup>
上下文
setup() 函数通过参数 props 和 context 来访问组件的属性和上下文。
-
props 就是 Vue2 中组件中的 props,指父组件传递来的参数
-
context 有三个属性 attrs slots emit 分别对应 Vue2 中的 attrs 属性、slots 插槽、$emit 事件
子组件接收父组件传递的值:
setup():
<script>
export default {
props: {
num: {
type: Number,
default: 1
}
},
setup (props) {
console.log(props)
}
}
</script>
<script setup>:
<script setup>
import { defineProps } from 'vue'
const props = defineProps({
num: {
type: Number,
default: 1
}
})
</script>
子组件给父组件传值:
setup():
<script>
export default {
setup (props, context) {
const sendNum = () => {
context.emit('submit', 1200)
}
return { sendNum }
}
}
</script>
<script setup>:
<script setup>
import { defineProps, defineEmits } from 'vue'
const emit = defineEmits(['submit'])
const sendNum = () => {
emit('submit', 1000)
}
</script>
<script setup>使用 defineProps 和 defineEmits 宏来访问组件的属性和触发自定义事件,不需要手动接收 props 和 context。
return
setup() 函数是一个标准的组件选项(Component Option),由 Vue 运行时直接解析。需显式返回对象,其属性暴露给模板。
即:setup() 中的内容需要显式地 return 才能在模板中访问(属性和方法都需要 return):
<template>
<div>
<p>{{ message }}</p>
<button @click="changeMessage">
测试
</button>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const message = ref('Hello, Vue 3!');
const changeMessage = () => {
message.value = 'Message changed!';
};
return { message, changeMessage };
}
};
</script>
例如上面这段代码,如果没有 return,则功能无法实现。
如果使用 <script setup> 则不需要 return:
<template>
<div>
<p>{{ message }}</p>
<button @click="changeMessage">
测试
</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const message = ref('Hello, Vue 3!');
const changeMessage = () => {
message.value = 'Message changed!';
};
</script>
这是因为:
编译器会对 <script setup>块进行静态分析和转换,生成等效的 setup() 函数。编译后的代码会提取顶层变量(包括 import 的组件),形成 setup() 的返回对象。
即:<script setup> 是 setup() 的语法糖,在 <script setup> 中定义的变量和方法会自动暴露给模板,无需手动 return。
expose
父组件:
<template>
<div>
<Children1 ref="child1" />
<Children2 ref="child2" />
<button @click="handleClick">按钮</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Children1 from './Children1.vue';
import Children2 from './Children2.vue';
const child1 = ref(null);
const child2 = ref(null);
const handleClick = () => {
console.log("child1:", child1);
console.log("child1 message:", child1.value.message);
console.log("child1 obj:", child1.value.obj);
console.log("child1 fn:", child1.value.fn);
console.log("----------");
console.log("child2:", child2);
console.log("child2 message:", child2.value.message);
console.log("child2 obj:", child2.value.obj);
console.log("child2 fn:", child2.value.fn);
}
</script>
setup() 子组件:
<template>
<div>
setup() 子组件
</div>
</template>
<script>
import { reactive, ref } from 'vue';
export default {
setup() {
const message = ref('Hello, Vue 3!');
const obj = reactive({ name: '张三', age: 10 })
const fn = () => {
console.log("子组件方法");
}
return { message, obj }
}
}
</script>
<script setup> 子组件:
<template>
<div>
<script setup> 子组件
</div>
</template>
<script setup>
import { reactive, ref } from 'vue';
const message = ref('Hello, Vue 3!');
const obj = reactive({ name: '张三', age: 10 })
const fn = () => {
console.log("子组件方法");
}
</script>
可以发现:setup() 会向父组件暴露所有 return 的属性和方法,而<script setup>就不会,<script setup>语法糖只会对外暴露手动 defineExpose 的内容。
其他
其他的一些使用细节上的区别:
注册组件:
setup():需要手动注册
<script>
import Hello from '@/components/HelloWorld'
export default {
components: {
Hello
}
}
</script>
<script setup>:不需要手动注册
<script setup>
import Hello from '@/components/HelloWorld'
</script>
自定义指令:
setup():
<template>
<h1 v-onceClick>使用了setup函数</h1>
</template>
<script>
export default {
directives: {
onceClick: {
mounted (el, binding, vnode) {
console.log(el)
}
}
},
}
</script>
<script setup>:
不需要显式注册,但他们必须遵循 vNameOfDirective
这样的命名规范。
<template>
<h1 v-once-Directive>使用了script setup</h1>
</template>
<script setup>
const vOnceDirective = {
beforeMount: (el) => {
console.log(el)
}
}
</script>
setup 函数特点
4、setup 函数在 beforeCreate 钩子函数之前执行
export default {
setup() {
console.log("setup");
},
beforeCreate() {
console.log("beforeCreate");
},
created() {
console.log("created");
},
mounted() {
console.log("mounted");
}
}
// setup
// beforeCreate
// created
// mounted
setup(props, context) 详细说明
执行顺序
setup() 是组件中使用组合式 API 的入口点,它在组件实例创建之前执行,在 beforeCreate 和 created 生命周期钩子之前调用。
注:有说法认为 Vue3 中没有 beforeCreate 和 created 钩子函数,这是不准确的。
-
组合式 API 中确实没有 beforeCreate 和 created 钩子函数,他们的功能被 setup 取代;
-
但是在选项式 API 中他们依然存在。
<template>
</template>
<script>
export default {
beforeCreate() {
console.log("beforeCreate");
},
created() {
console.log("created");
},
setup() {
console.log("setup");
}
}
</script>
执行顺序:setup——beforeCreate——created
props 参数
特性:
-
是响应式的,包含组件接收的所有 prop,当父组件更新 props 时会自动更新
-
不能使用 ES6 解构,否则会失去响应性
-
如果需要解构,可以使用 toRefs 或 toRef 保持响应性
特性 1 举例:
<!--父组件-->
<template>
<div>
<Children name="张三" :age="10" />
</div>
</template>
<script>
import Children from './Children.vue';
export default {
components: { Children }
}
</script>
<!--子组件-->
<template>
<div>
<p>{{ name }}</p>
<p>{{ age }}</p>
</div>
</template>
<script>
export default {
props: {
name: String,
// age: Number 没有接收 age 属性
},
setup(props) {
console.log("props:", props);
console.log("name:", props.name);
console.log("age:", props.age);
}
}
</script>
组件没有接收 age 属性,所以 props 参数中的 age 为 undefined。
特性 2、3 举例:
<!--父组件-->
<template>
<div>
<Children :name="name" :age="age" @change="handleChange" />
</div>
</template>
<script>
import { ref } from 'vue';
import Children from './Children.vue';
export default {
components: { Children },
setup() {
const name = ref('张三');
const age = ref(10);
const handleChange = () => {
age.value++
}
return { name, age, handleChange };
}
}
</script>
<!--子组件-->
<template>
<div style="margin-left: 50px;">
<p>{{ props.name }}</p>
<p>{{ props.age }}</p>
<p>{{ age }}</p>
<button @click="handleClick">按钮
</button>
</div>
</template>
<script>
import { toRefs } from 'vue';
export default {
props: {
name: String,
age: Number
},
setup(props, context) {
const { age } = props
const age1 = props.age
const { age: age2 } = toRefs(props)
const handleClick = () => {
context.emit('change');
setTimeout(() => {
console.log("props.age:", props.age);
console.log("age:", age);
console.log("age1:", age1);
console.log("age2.value:", age2.value);
console.log("age2:", age2);
})
};
return { props, age, handleClick }
}
}
</script>
初始页面:
点击一次按钮后:
可以看到:
const { age } = props
和 const age1 = props.age
丢失了响应性,
props.age
和 const { age: age2 } = toRefs(props)
保留了响应性。
拓展:
使用 toRefs(props) 创建的 age2 是一个 ref 对象,它包含了多个内部属性:
属性 | 类型 | 说明 |
---|---|---|
__v_isRef | boolean | 标识这是一个 ref 对象,值为 true |
_defaultValue | any | 默认值 |
_key | string | 对应的 props 键名,这里是 “age” |
_object | object | 指向原始的 props 响应式对象 |
_value | any | 当前存储的 age 的值(与 value 相同) |
dep | Set<ReactiveEffect> | 存储依赖该 ref 的副作用(effect) |
value | any | 访问或修改 |
context 参数
context 是一个普通对象(非响应式),包含组件的三个属性:
-
attrs
-
包含所有未在 props 中声明的 attribute
-
相当于 Vue 2 中的 this.$attrs
-
非响应式
-
示例:
setup(props, { attrs }) { console.log(attrs.class) // 访问 class 属性 }
-
-
slots
-
包含所有插槽内容的对象
-
相当于 Vue 2 中的 this.$slots
-
非响应式
-
示例:
setup(props, { slots }) { const defaultSlot = slots.default() // 获取默认插槽内容 return () => h('div', defaultSlot) }
-
-
emit
-
用于触发自定义事件的函数
-
相当于 Vue 2 中的 this.$emit
-
示例:
setup(props, { emit }) { const handleClick = () => { emit('change', 'new value') } return { handleClick } }
-
当然也可以不使用解构写法:
setup(props, context) {
console.log(context.attrs.class)
const defaultSlot = context.slots.default()
const handleClick = () => {
context.emit('change', 'new value')
}
}
返回值
setup() 可以返回一个对象,该对象的属性/函数将被暴露给模板使用,也可以返回一个渲染函数:
返回对象:
import { ref } from 'vue'
setup() {
const count = ref(0)
return {
count,
increment: () => count.value++
}
}
返回渲染函数:
import { h, ref } from 'vue'
setup() {
const count = ref(0)
return () => h('div', count.value)
}