ref、toRef、toRefs的区别
ref
- ref的本质是拷贝粘贴一份数据,脱离了与源数据的交互。
- 通过ref绑定的数据会变成响应式数据。ref的值在
setUp()
函数中通过.value
形式获取/修改。 - 修改ref,会触发视图更新,但修改原数据不会改变。
注意:当 ref
绑定的数据类型不是基本类型时,ref
会在内部调用 reactive()
,将该对象及其嵌套属性都转换成深度响应式。
const age = 60;
const age_in = ref(age);
age_in.value = 70;
console.log(age); // 60
console.log(age_in.value); // 70
toRef
- 可以创建一个响应式对象,也可以基于响应式对象上的属性创建一个对应的
ref
,这样创建的ref
与其源属性保持同步:改变源属性的值将更新ref
的值,反之亦然。 - 当使用对象属性签名时,即使源属性当前不存在,
toRef()
也会返回一个可用的ref
。
// 按原样返回现有的 ref
toRef(existingRef);
// 创建一个只读的 ref,当访问 .value 时会调用此 getter 函数
toRef(() => props.foo);
// 从非函数的值中创建普通的 ref
// 等同于 ref(1)
toRef(1);
更多请查看官网toRef
toRefs
- 将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的
ref
。每个单独的ref
都是使用toRef()
创建的。 - 在需要解构一个响应式对象时,
toRefs()
很有用。
function useFeatureX() {
const state = reactive({
foo: 1,
bar: 2,
});
// ...基于状态的操作逻辑
// 在返回时都转为 ref
return toRefs(state);
}
// 可以解构而不会失去响应性
const { foo, bar } = useFeatureX();
组件传参(父传子)
prop传值
<!-- 父组件 -->
<!-- 引入子组件,绑定需要传的参数 -->
<script setup>
const p1 = reactive({ name: "兜兜", age: 20 });
</script>
<template>
<Vue ref="val" :data="p1" />
</template>
<!-- 子组件 -->
<script setup>
const props = defineProps({
data: {
type: Object,
default: {},
},
});
console.log(props.data);
</script>
<template>
<!-- 在模板中使用 defineProps 时,可以不使用props.data -->
<div v-for="item in data">item</div>
</template>
跨组件传参
const p1 = reactive({ name: "兜兜", age: 20 });
provide("p", p1); // 父组件向子组件传值
const res = inject("p"); // 子组件接收
点击触发事件传参
// 父组件
// 引入子组件,使用ref调用该子组件内函数
<Vue ref="val" />;
const val = ref();
const p1 = reactive({ name: "兜兜", age: 20 });
function btn() {
val.value.receive(p1);
}
// 子组件
const receive = (data) => {
console.log(data);
};
// 将子组件方法暴露出去
defineExpose({
receive,
});
组件传参(子传父)
事件传参
// 父组件
const getData = function (id) {
console.log(id);
};
// 子组件定义方法并调用父组件函数
// <Content @getData="getData" />
// 子组件
const emit = defineEmits(["getData"]);
// 调用父组件方法
emit("getData", 1);
双向绑定 defineModel
// 父组件
<UserName v-model:first-name="first" v-model:last-name="last" />
// 子组件
<script setup>
const firstName = defineModel("firstName");
const lastName = defineModel("lastName");
</script>
<template>
<input type="text" v-model="firstName" />
<input type="text" v-model="lastName" />
</template>
defineEmits 与 defineExpose
defineEmits 用于定义子组件向父组件传递的事件名,而 defineExpose 则用于定义子组件暴露给父组件的属性或方法。
watch
watch的第一个参数可以是不同形式的数据源:它可以是一个ref(包括计算属性)、一个响应式对象、一个getter函数、或多个数据源组成的数组。
const x = ref(0);
const y = ref(0);
// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`);
});
// getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`);
},
);
// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`);
});
注意,你不能直接侦听响应式对象的属性值,例如
const obj = reactive({ count: 0 });
// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`Count is: ${count}`);
});
这里需要用一个返回该属性的 getter 函数:
// 提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`Count is: ${count}`);
},
);
当传入整个对象时,默认会开启深度监听,当为一个对象的属性时,那么只有当属性值发生变化时才会触发
const obj = reactive({ count: 0 });
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
});
obj.count++;
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
},
);
单个属性也可以加上深度监听
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true },
);
更多扩展功能请查看官方文档
子组件接收父组件传过来的标签属性($attrs)
在子组件中,通过 $attrs 获取父组件传过来的标签属性。
<!-- MyComponent 模板使用 $attrs 时 -->
// <p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>
<MyComponent class="baz"/>
// 这将被渲染为
<p class="baz">Hi!</p>
<span>This is a child component</span>
reactive
reactive通常用来绑定非基本类型数据。 需要注意的是,当修改reactive当中的内容时,原始对象也会发生改变
let obj = { a: 1 };
const myObj = reactive(obj);
myObj.a = 2;
console.log(obj); // {a:2}
console.log(myObj); // {a:2}
不要尝试替换reactive里面的内容,因为这会使响应式失效 以下是错误写法
const myObj = reactive({ a: 1 });
myObj = { a: 2 }; // 这是错误的写法,因为这会替换掉里面的内容而不是修改
刷新某个组件数据
有些时候,更换组件数据时,组件内的某些标签数据可能不会刷新,例如 video 标签。 这时候,可以通过更改组件的 key 来刷新组件数据。
<template>
<video class="video" :key="videoKey" ref="videoPlayer" type="video/mp4" controls>
<source :src="src" type="video/mp4" />
</video>
</template>
<script setup>
const prop = defineProps({
src: String,
});
const videoKey = ref(0);
watch(
() => prop.src,
(val) => {
// 重新加载
videoKey.value += 1;
},
);
</script>
按钮双击事件
<template>
<button @dblclick="handleDblClick">双击</button>
</template>
<script setup>
const handleDblClick = () => {
alert("双击");
};
</script>
表单提交事件
<template>
<form @submit.prevent="handleSubmit">
<input type="text" name="name" />
<button type="submit">提交</button>
<button type="reset">重置</button>
</form>
</template>
<script setup>
const handleSubmit = (e) => {
alert("表单提交");
};
</script>
修饰符
@keydown
键盘按下事件,默认键盘所有按键按下都会触发,也可以指定按钮触发。
例如:
@keydown.enter
:当键盘按下enter键时会触发。
@keydown.tab
:当键盘按下tab键时会触发。
@keydown.esc
:当键盘按下esc键时会触发。
@event.prevent
拦截浏览器默认事件。
例如:
@keydown.tab.prevent
拦截浏览器tab默认事件
@event.stop
阻止事件向上传递(冒泡)
<div @click="downloadImg('png')">
<div @click.stop="downloadImg('jpeg')">保存为JPEG</div>
保存图片
</div>
@click.stop
点击事件不会向上传递
静态文件
对于静态文件例如css、图片等,尽量放在assets
文件夹中
样式穿透
在style
标签中,使用deep
关键字,可以实现样式穿透。
<style scoped lang="sass">
.folded {
:deep(.ant-collapse-header) {
padding: 0 0 8px;
color: #1677ff;
}
:deep(.ant-collapse-content-box) {
padding: 0;
}
}
</style>
监听节点的生命周期
在Vue3中,我们可以使用@vue
方法来监听节点的生命周期。
<template>
<div
@vue:mounted="handleMounted"
@vue:updated="handleUpdated"
>
</template>
<script setup>
const handleMounted = () => {
console.log("节点已挂载");
};
const handleUpdated = () => {
console.log("节点已更新");
};
</script>
自定义Ref
在Vue3中,我们可以使用customRef
方法来创建自定义的Ref。
import { customRef } from "vue";
export const loading = customRef((track, trigger) => {
let loadingCount = 0;
return {
get() {
track(); // 收集依赖
return loadingCount > 0;
},
set(value) {
if (value) loadingCount++;
else loadingCount--;
loadingCount = Math.max(0, loadingCount);
trigger(); // 通知更新
},
};
});
二次封装组件
简单封装
该示例为对element-plus
Table组件的二次封装
- 使用
withDefaults
方法,设置默认参数 - 使用
defineOptions
方法,设置是否继承父组件的属性 - 使用
ExtractPropTypes
方法获取组件的属性
<script setup lang="ts">
import { ElMessage, type TableProps } from "element-plus";
import type { ExtractPropTypes } from "vue";
interface CustomInputProps extends Omit<ExtractPropTypes<TableProps<any>>, "data"> {
// 自定义的一些参数
loading?: boolean;
data: any[];
}
// 默认参数
const props = withDefaults(defineProps<CustomInputProps>(), {
loading: false,
border: true,
height: "100%",
});
defineOptions({
// 是否继承父组件的属性
inheritAttrs: false,
});
</script>
<template>
<el-table v-bind="{...$attrs,...$props}" v-loading="loading"></el-table>
</template>
完整封装
该示例为对element-plus
input组件的二次封装
- 使用
h
创建组件,是为了方便继承当前组件的插槽 getCurrentInstance()
获取当前组件实例$props
获取外部传入当前组件的props$attrs
获取外部传入当前组件的属性$slots
获取外部传入当前组件的插槽vm.expose
暴露当前组件的方法vm.exposeProxy
暴露当前组件的代理方法
<script setup lang="ts">
import { h, getCurrentInstance } from "vue";
import { ElInput, type InputProps } from "element-plus";
const props = defineProps<InputProps>(); // 类型安全地声明组件接收的 props
const vm = getCurrentInstance(); // 获取当前组件实例
// 用于将子组件的实例暴露给外部使用
function changeRef(inputInstance) {
vm.exposed = inputInstance || {}; // 相当于把 el-input 的实例挂载在了当前组件实例上并暴露出去
vm.exposeProxy = inputInstance || {};
}
</script>
<template>
<div class="my-input">
<div>二次封装组件</div>
<!-- 通过 h() 手动渲染 ElInput 组件,并传入 $attrs, $props 和 ref -->
<component :is="h(ElInput,{...$attrs,...$props,ref:changeRef},$slots)"></component>
</div>
</template>
将pina里内容转换为响应式对象
<script setup>
import {storeToRefs} from 'pinia'
import {useUserStore} from '@/store'
const userStore = useUserStore()
const {name,age} = storeToRefs(userStore)
<script/>
在组件中更改全局样式 (:global)
使用 :global
可以在组件中更改全局样式。
注意: 通过 :global
设置的全局样式即使组件被销毁也会保留。
<template>
<el-drawer title="标题"></el-drawer>
</template>
<style scoped>
:global(.el-drawer__body) {
padding: 0;
}
</style>
更改组件插槽样式 (:slotted)
使用 :slotted
可以更改组件插槽样式。
<template>
<el-drawer title="标题">
<template #default>
<el-button>按钮</el-button>
</template>
</el-drawer>
</template>
<style scoped>
:slotted(.el-button) {
background-color: red;
}
</style>