Vue 系列之:defineProps、defineEmits、...
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); // objectconst 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); // 18child1.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,// 展示加载组件前的延迟时间,默认为 200msdelay: 2000,// 加载失败后展示的组件errorComponent: ErrorComponent,// 如果提供了一个 timeout 时间限制,并超时了,// 也会显示加载失败后展示的组件,默认值是:Infinitytimeout: 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 = 1setInterval(() => {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: {// ...}
}
这些就是选项对象。
Vue3中defineComponent 的作用详解
Vue 中的 defineComponent
Vue3源码解析-defineComponent
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 的 CSSstyles: [`/* inlined css */`]
})// 注册自定义元素
// 注册之后,所有此页面中的 `<my-vue-element>` 标签都会被升级
customElements.define('my-vue-element', MyVueElement)
有兴趣的可以看看 Web Component 的相关知识 【zh-CN】。可以将 Web Components 简单理解为一个自定义的 HTML 标签。
用的很少,就不做具体研究。
Vue 与 Web Components
Vue3中defineCustomElement的使用
defineModel
仅在 3.4+ 中可用
v-model
Vue 系列之:自定义双向数据绑定
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 } = propsconst age1 = props.ageconst { 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)
}