◆

Vue emit 子组件向父组件传值示例

2023-02-02 01:370

emit 解决的是什么问题

Vue 的数据流是单向的:父组件通过 props 把数据传给子组件,子组件不能直接改写收到的 props。一旦子组件在内部对 props 赋值,开发环境会立刻收到 Avoid mutating a prop directly 的警告,因为父组件下一次重新渲染仍会用自己的数据覆盖掉子组件的改动,两边状态就对不上了。

所以子组件想「改父组件的值」,正确做法是拆成两步:子组件用 $emit 抛出一个自定义事件并带上新值,父组件监听这个事件、在自己的作用域里完成赋值。数据的所有权始终留在父组件手里,子组件只是发出请求。v-model 和 Vue 2 的 .sync 修饰符,底层做的就是这件事。

下面这组示例把三种常见写法排在一起对比:模板里内联赋值、父组件用方法接收、以及事件名用 update:xxx 约定名与用自定义名的区别。

vue2 中 emit 示例

父组件里同时挂了三个子组件,分别对应三种接收方式。update:属性名 这种事件名是一个约定,它能配合 .sync 修饰符使用::title.sync="title" 等价于手写 :title="title" @update:title="title = $event"。注意示例中事件名是 update:title,而 props 名叫 emitStringOne,两者并不对应,所以这里只能手写监听,不能直接换成 .sync。

父组件

<template>
  <div>
    <EmitChildOne
      :emitStringOne="emitStringOne"
      @update:title="val => (emitStringOne = val)"
    ></EmitChildOne>
    <EmitChildTwo
      :emitStringTwo="emitStringTwo"
      @update:title="updateEmitTwo"
    ></EmitChildTwo>
    <EmitChildThree
      :emitStringThree="emitStringThree"
      @updateTitle="updateEmitThree"
    ></EmitChildThree>
  </div>
</template>

<script>
import EmitChildOne from './emitChildOne.vue';
import EmitChildTwo from './emitChildTwo.vue';
import EmitChildThree from './emitChildThree.vue';

export default {
  components: {
    EmitChildOne,
    EmitChildTwo,
    EmitChildThree,
  },
  data() {
    return {
      emitStringOne: 'emitStringOne',
      emitStringTwo: 'emitStringTwo',
      emitStringThree: 'emitStringThree',
    };
  },
  methods: {
    updateEmitTwo(val) {
      this.emitStringTwo = val;
    },
    updateEmitThree(val) {
      this.emitStringThree = val;
    },
  },
};
</script>

第一个子组件里给出了两种触发方式:一种是在 methods 里定义 test 方法再调用 this.$emit,另一种是直接在模板的事件处理里写 $emit(...)。模板里能直接用 $emit 是因为它挂在组件实例上,模板表达式的作用域就是当前实例。逻辑简单时用内联写法更省事,需要做校验或者拼装数据时再抽成方法。

子组件 emitChildOne.vue

<template>
  <div>
    <div>值:{{ emitStringOne }}</div>
    <button @click="test">测试</button>
    <br />
    <button @click="$emit('update:title', '测试直接 emit')">
      测试直接 emit
    </button>
  </div>
</template>

<script>
export default {
  props: {
    emitStringOne: String,
  },
  methods: {
    test() {
      this.$emit(
        'update:title',
        'emitChildOne:emitStringOne:我在子组件中的值',
      );
    },
  },
};
</script>

第二个子组件抛出的事件名和第一个完全一样,都是 update:title。这说明事件名的作用域只在当前这一对父子之间,不同子组件用同名事件互不干扰,父组件在各自的标签上分别监听即可。父组件这次没有用内联赋值,而是把处理逻辑放进 updateEmitTwo 方法,值多、需要额外处理时这种写法更清晰。

子组件 emitChildTwo.vue

<template>
  <div>
    <div>值:{{ emitStringTwo }}</div>
    <button @click="test">测试</button>
  </div>
</template>

<script>
export default {
  props: {
    emitStringTwo: String,
  },
  methods: {
    test() {
      this.$emit(
        'update:title',
        'emitStringTwo:emitStringTwo:我在子组件中的值',
      );
    },
  },
};
</script>

第三个子组件换成了自定义事件名 updateTitle,父组件对应写成 @updateTitle。这里有个容易踩的坑:HTML 标签的属性名不区分大小写,如果模板写在 .html 文件里而不是单文件组件里,@updateTitle 会被浏览器解析成全小写的 @updatetitle,事件就监听不到了。稳妥的做法是事件名统一用 kebab-case,也就是子组件抛 update-title、父组件监听 @update-title。

子组件 emitChildThree.vue

<template>
  <div>
    <div>值:{{ emitStringThree }}</div>
    <button @click="test">测试</button>
  </div>
</template>

<script>
export default {
  props: {
    emitStringThree: String,
  },
  methods: {
    test() {
      this.$emit(
        'updateTitle',
        'emitStringThree:emitStringThree:我在子组件中的值',
      );
    },
  },
};
</script>
💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEVue emit 子组件向父组件传值示例加载中