本文发表于 295 天前,其中的信息可能已经事过境迁
文章摘要
加载中...|
此内容根据文章生成,并经过人工审核,仅用于文章内容的解释与总结

ref、toRef、toRefs的区别

ref

  1. ref的本质是拷贝粘贴一份数据,脱离了与源数据的交互。
  2. 通过ref绑定的数据会变成响应式数据。ref的值在setUp()函数中通过.value形式获取/修改。
  3. 修改ref,会触发视图更新,但修改原数据不会改变。

注意:当 ref 绑定的数据类型不是基本类型时,ref 会在内部调用 reactive(),将该对象及其嵌套属性都转换成深度响应式。

javascript
const age = 60;
const age_in = ref(age);
age_in.value = 70;
console.log(age); // 60
console.log(age_in.value); // 70

toRef

  1. 可以创建一个响应式对象,也可以基于响应式对象上的属性创建一个对应的ref,这样创建的ref 与其源属性保持同步:改变源属性的值将更新ref的值,反之亦然。
  2. 当使用对象属性签名时,即使源属性当前不存在,toRef()也会返回一个可用的ref
javascript
// 按原样返回现有的 ref
toRef(existingRef);

// 创建一个只读的 ref,当访问 .value 时会调用此 getter 函数
toRef(() => props.foo);

// 从非函数的值中创建普通的 ref
// 等同于 ref(1)
toRef(1);

更多请查看官网toRef

toRefs

  1. 将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的ref。每个单独的ref 都是使用toRef()创建的。
  2. 在需要解构一个响应式对象时,toRefs()很有用。
javascript
function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2,
  });

  // ...基于状态的操作逻辑

  // 在返回时都转为 ref
  return toRefs(state);
}

// 可以解构而不会失去响应性
const { foo, bar } = useFeatureX();

组件传参(父传子)

prop传值

html
<!-- 父组件 -->
<!-- 引入子组件,绑定需要传的参数 -->
<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>

跨组件传参

javascript
const p1 = reactive({ name: "兜兜", age: 20 });
provide("p", p1); // 父组件向子组件传值

const res = inject("p"); // 子组件接收

点击触发事件传参

javascript
// 父组件
// 引入子组件,使用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,
});

组件传参(子传父)

事件传参

javascript
// 父组件
const getData = function (id) {
  console.log(id);
};
// 子组件定义方法并调用父组件函数
// <Content @getData="getData" />

// 子组件
const emit = defineEmits(["getData"]);
// 调用父组件方法
emit("getData", 1);

双向绑定 defineModel

html
// 父组件
<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函数、或多个数据源组成的数组。

javascript
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}`);
});

注意,你不能直接侦听响应式对象的属性值,例如

javascript
const obj = reactive({ count: 0 });

// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
  console.log(`Count is: ${count}`);
});

这里需要用一个返回该属性的 getter 函数:

javascript
// 提供一个 getter 函数
watch(
  () => obj.count,
  (count) => {
    console.log(`Count is: ${count}`);
  },
);

当传入整个对象时,默认会开启深度监听,当为一个对象的属性时,那么只有当属性值发生变化时才会触发

javascript
const obj = reactive({ count: 0 });

watch(obj, (newValue, oldValue) => {
  // 在嵌套的属性变更时触发
  // 注意:`newValue` 此处和 `oldValue` 是相等的
  // 因为它们是同一个对象!
});

obj.count++;

watch(
  () => state.someObject,
  () => {
    // 仅当 state.someObject 被替换时触发
  },
);

单个属性也可以加上深度监听

javascript
watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // 注意:`newValue` 此处和 `oldValue` 是相等的
    // *除非* state.someObject 被整个替换了
  },
  { deep: true },
);

更多扩展功能请查看官方文档

子组件接收父组件传过来的标签属性($attrs)

在子组件中,通过 $attrs 获取父组件传过来的标签属性。

javascript
<!-- 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当中的内容时,原始对象也会发生改变

javascript
let obj = { a: 1 };
const myObj = reactive(obj);
myObj.a = 2;
console.log(obj); // {a:2}
console.log(myObj); // {a:2}

不要尝试替换reactive里面的内容,因为这会使响应式失效 以下是错误写法

javascript
const myObj = reactive({ a: 1 });
myObj = { a: 2 }; // 这是错误的写法,因为这会替换掉里面的内容而不是修改

刷新某个组件数据

有些时候,更换组件数据时,组件内的某些标签数据可能不会刷新,例如 video 标签。 这时候,可以通过更改组件的 key 来刷新组件数据。

html
<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>

按钮双击事件

html
<template>
  <button @dblclick="handleDblClick">双击</button>
</template>
<script setup>
  const handleDblClick = () => {
    alert("双击");
  };
</script>

表单提交事件

html
<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阻止事件向上传递(冒泡)

vue
<div @click="downloadImg('png')">
  <div @click.stop="downloadImg('jpeg')">保存为JPEG</div>
  保存图片
</div>

@click.stop点击事件不会向上传递

静态文件

对于静态文件例如css、图片等,尽量放在assets文件夹中

样式穿透

style标签中,使用deep关键字,可以实现样式穿透。

html
<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方法来监听节点的生命周期。

html
<template>
  <div
    @vue:mounted="handleMounted"
    @vue:updated="handleUpdated"
  >
</template>
<script setup>
  const handleMounted = () => {
    console.log("节点已挂载");
  };
  const handleUpdated = () => {
    console.log("节点已更新");
  };
</script>

自定义Ref

在Vue3中,我们可以使用customRef方法来创建自定义的Ref。

javascript
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-plusTable组件的二次封装

  • 使用withDefaults方法,设置默认参数
  • 使用defineOptions方法,设置是否继承父组件的属性
  • 使用 ExtractPropTypes方法获取组件的属性
html
<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-plusinput组件的二次封装

  • 使用h创建组件,是为了方便继承当前组件的插槽
  • getCurrentInstance() 获取当前组件实例
  • $props 获取外部传入当前组件的props
  • $attrs 获取外部传入当前组件的属性
  • $slots 获取外部传入当前组件的插槽
  • vm.expose 暴露当前组件的方法
  • vm.exposeProxy 暴露当前组件的代理方法
html
<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里内容转换为响应式对象

html
<script setup>
   import {storeToRefs} from 'pinia'

   import {useUserStore} from '@/store'
   const userStore = useUserStore()

   const {name,age} =  storeToRefs(userStore)
<script/>

在组件中更改全局样式 (:global)

使用 :global 可以在组件中更改全局样式。

注意: 通过 :global 设置的全局样式即使组件被销毁也会保留。

html
<template>
  <el-drawer title="标题"></el-drawer>
</template>
<style scoped>
  :global(.el-drawer__body) {
    padding: 0;
  }
</style>

更改组件插槽样式 (:slotted)

使用 :slotted 可以更改组件插槽样式。

html
<template>
  <el-drawer title="标题">
    <template #default>
      <el-button>按钮</el-button>
    </template>
  </el-drawer>
</template>
<style scoped>
  :slotted(.el-button) {
    background-color: red;
  }
</style>
评论 隐私政策