父子组件传参 学习笔记

143 阅读3分钟

父子组件传参 学习笔记

一.前言

对于VUE的组件间传递我早有耳闻父->子子->父 方式是不一样的。在学习这个东西之前我一直很纠结,不就是个传参么咋父->子子->父 还不一样呢?你是否和我有一样的疑问?是的话不要着急,第一张结尾会给出答案~

二.父->子(props)

2.1 props

如果我们正在构建一个博客,我们可能需要一个表示博客文章的组件。我们希望所有的博客文章分享相同的视觉布局,但有不同的内容。要实现这样的效果自然必须向组件中传递数据,例如每篇文章标题和内容,这就会使用到 props。

Props 是一种特别的属性,你可以在组件上声明注册。

一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute (关于透传 attribute,点这个查看)。

Tips翻译成人话就是:想给子组件传参需要用一个东西叫props。那么怎么用呢?

我的李姐是:我爹给我一个title通过Vue转交给我,但是我不能直接用,我得用props给Vue提个申请,说我爹给我的title我需要用。否则VUE认为我爹给我的title我不想要。

官方举例:我想传递给博客文章组件(子组件)一个标题,我们必须在文章组件的 props 列表上声明它,如果你没有使用 <script setup>,props 必须以 props 选项的方式声明,props 对象会作为 setup() 函数的第一个参数被传入:

export default {
  props: ['title'],
  setup(props) {
    console.log(props.title)
  }
}

如果使用了 <script setup>,可以使用 defineProps 宏,它是一个仅 <script setup> 中可用的编译宏命令,并不需要显式地导入。它声明的 props 会自动暴露给模板,同时defineProps 会返回一个对象,其中包含了可以传递给组件的所有 props:

<!-- BlogPost.vue -->
<script setup>
const props = defineProps(['title'])
console.log(props.title)
</script><template>
  <h4>{{ title }}</h4>
</template>

Tips:额外说一下:这个东西做的是简单的替换,宏在编译之前进行,即先用宏体替换宏名,然后再编译的。

虽然可以在模板中直接使用{{ title }},但是在函数中是不可以的,在函数中只能使用props.title这样的形式获取Props的内容

除了使用字符串数组来声明 prop 外,还可以使用对象的形式:

// 使用 <script setup>
defineProps({
  title: String,
  likes: Number
})

2.2 细节

  1. 静态 vs. 动态 Prop

    至此,你已经见过了很多像这样的静态值形式的 props:

<BlogPost title="My journey with Vue" ></pre>

相应地,还有使用 v-bind 或缩写 : 来进行动态绑定的 props:

<!-- 根据一个变量的值动态传入 -->
<BlogPost :title="post.title" /><!-- 根据一个更复杂表达式的值动态传入 -->
<BlogPost :title="post.title + ' by ' + post.author.name" />
  1. 使用一个对象绑定多个 prop

如果你想要将一个对象的所有属性都当作 props 传入,你可以使用没有参数的 v-bind,即只使用 v-bind 而非 :prop-name。例如,这里有一个 post 对象:

const post = {
  id: 1,
  title: 'My Journey with Vue'
}

以及下面的模板:

<BlogPost v-bind="post" />

而这实际上等价于:

<BlogPost :id="post.id" :title="post.title" />

2.3 单向数据流

所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。

另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。若你这么做了,Vue 会在控制台上向你抛出警告:

const props = defineProps(['foo'])
​
// ❌ 警告!prop 是只读的!
props.foo = 'bar'

导致你想要更改一个 prop 的需求通常来源于以下两种场景:

  1. prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性。在这种情况下,最好是新定义一个局部数据属性,从 props 上获取初始值即可:

    const props = defineProps(['initialCounter'])
    ​
    // 计数器只是将 props.initialCounter 作为初始值
    // 像下面这样做就使 prop 和后续更新无关了
    const counter = ref(props.initialCounter)
    
  2. 需要对传入的 prop 值做进一步的转换。在这种情况中,最好是基于该 prop 值定义一个计算属性:

    const props = defineProps(['size'])
    ​
    // 该 prop 变更时计算属性也会自动更新
    const normalizedSize = computed(() => props.size.trim().toLowerCase())
    

更改对象 / 数组类型的 props

当对象或数组作为 props 被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,而对 Vue 来说,禁止这样的改动,虽然可能生效,但有很大的性能损耗,比较得不偿失。

这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该抛出一个事件来通知父组件做出改变。

解释文章首段的疑惑,为何父->子子->父 是不一样的实现

三 子->父(emit)

书接上文,父->子的传递我们清除了,用props,在上章末尾,官方提示我们:"子组件应该抛出一个事件来通知父组件做出改变"。我们就先来研究研究什么是组件事件

3.1 组件事件

让我们继续关注我们的 <BlogPost> 组件(子组件)。我们会发现有时候它需要与父组件进行交互。例如,要在此处实现无障碍访问的需求,将博客文章的文字能够放大,而页面的其余部分仍使用默认字号。

在父组件中,我们可以添加一个 postFontSize ref 来实现这个效果:

const posts = ref([
  { id: 1, title: 'Oracle从入门到删库跑路' },
  { id: 2, title: 'Java从入门到炖了汤姆猫' },
  { id: 3, title: 'Vue从入门到彻底放弃' }
])
​
const postFontSize = ref(1)

在模板中用它来控制所有博客文章的字体大小:

<div :style="{ fontSize: postFontSize + 'em' }">
  <BlogPost
    v-for="post in posts"
    :key="post.id"
    :title="post.title"
   />
</div>

然后,给 <BlogPost> 组件(子组件)添加一个按钮:

<!-- BlogPost.vue, 省略了 <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button>字体变大</button>
  </div>
</template>

这个按钮目前还没有做任何事情,我们想要点击这个按钮来告诉父组件它应该放大所有博客文章的文字。

要解决这个问题,组件实例提供了一个自定义事件系统。父组件可以通过 v-on@ 来选择性地监听子组件上抛的事件,就像监听原生 DOM 事件那样:

<BlogPost
  ...
  @enlarge-text="postFontSize += 0.1"
 />

子组件可以通过调用内置的 $emit 方法通过传入事件名称来抛出一个事件:

Tips:$emit()在当前组件触发一个自定义事件

<!-- BlogPost.vue, 省略了 <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button @click="$emit('enlarge-text')">Enlarge text</button>
  </div>
</template>

因为父组件有了 @enlarge-text="postFontSize += 0.1" 的监听,父组件会接收这一事件,从而更新 postFontSize 的值。

我们可以通过 defineEmits 宏来声明需要抛出的事件:

<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>

这声明了一个组件可能触发的所有事件,还可以对事件的参数进行验证。同时,这还可以让 Vue 避免将它们作为原生事件监听器隐式地应用于子组件的根元素。

defineProps 类似,defineEmits 仅可用于 <script setup> 之中,并且不需要导入,它返回一个等同于 $emit 方法的 emit 函数。它可以被用于在组件的 <script setup> 中抛出事件,因为此处无法直接访问 $emit

<script setup>
const emit = defineEmits(['enlarge-text'])
//直接这么写,渲染的时候就直接触发回调。我们一般不会这么玩儿,一般都是在页面上写个按钮啥的,点击然后里面调用emit 
emit('enlarge-text')
</script>

如果你没有在使用 <script setup>,你可以通过 emits 选项定义组件会抛出的事件。你可以从 setup() 函数的第二个参数,即 setup 上下文对象上访问到 emit 函数:

export default {
  emits: ['enlarge-text'],
  setup(props, ctx) {
    ctx.emit('enlarge-text')
  }
}

3.2 触发与监听

在组件的模板表达式中,可以直接使用 $emit 方法触发自定义事件 (例如:在 v-on 的处理函数中):

<!-- MyComponent -->
<button @click="$emit('someEvent')">click me</button>

父组件可以通过 v-on (缩写为 @) 来监听事件的时候,支持 .once 修饰符:

<MyComponent @some-event.once="callback" />

像组件与 prop 一样,事件的名字也提供了自动的格式转换。注意这里我们触发了一个以 camelCase 形式命名的事件,但在父组件中可以使用 kebab-case 形式来监听。与 prop 大小写格式一样,在模板中我们也推荐使用 kebab-case 形式来编写监听器。

Tips:和原生 DOM 事件不一样,组件触发的事件没有冒泡机制。你只能监听直接子组件触发的事件。平级组件或是跨越多层嵌套的组件间通信,应使用一个外部的事件总线,或是使用一个全局状态管理方案

3.3 事件参数

有时候我们会需要在触发事件时附带一个特定的值。我们可以给 $emit 提供一个额外的参数:

<button @click="$emit('increaseBy', 1)">
  Increase by 1
</button>

然后我们在父组件中监听事件,我们可以先简单写一个内联的箭头函数作为监听器,此函数会接收到事件附带的参数:

<MyButton @increase-by="(n) => count += n" />

或者,也可以用一个组件方法来作为事件处理函数,该方法也会接收到事件所传递的参数:

<MyButton @increase-by="increaseCount" />
//处理上面的回调
function increaseCount(n) {
  count.value += n
}

3.4 声明触发的事件

defineEmits() 宏来声明它要触发的事件的时候,emit 可以安全地被解构:

const emit = defineEmits(['inFocus', 'submit'])
​
const send = () => {
  emit('inFocus',1)
  emit('submit',1)
}

这个 emits 选项还支持对象语法,它允许我们对触发事件的参数进行验证:

<script setup>
const emit = defineEmits({
  inFocus(a){
    return true
  },
  submit(b){
    return false
  }
})
​
const send = () => {
  emit('inFocus',2)
  emit('submit',3)
}
</script>

Tips:关于效验。Vue 组件可以更细致地声明对传入的 props 的校验要求。比如我们上面已经看到过的类型声明,如果传入的值不满足类型要求,Vue 会在浏览器控制台中抛出警告来提醒使用者。这在开发给其他开发者使用的组件时非常有用。

四.总结

为了在声明 propsemits 选项时获得完整的类型推导支持,我们可以使用 definePropsdefineEmits API,它们将自动地在 <script setup> 中可用:

<script setup>
const props = defineProps({
  foo: String
})
​
const emit = defineEmits(['change', 'delete'])
// setup 代码
</script>
  • definePropsdefineEmits 都是只能在 <script setup> 中使用的编译器宏。他们不需要导入,且会随着 <script setup> 的处理过程一同被编译掉。
  • defineProps 接收与 props 选项相同的值,defineEmits 接收与 emits 选项相同的值。
  • definePropsdefineEmits 在选项传入后,会提供恰当的类型推导。
  • 传入到 definePropsdefineEmits 的选项会从 setup 中提升到模块的作用域。因此,传入的选项不能引用在 setup 作用域中声明的局部变量。这样做会引起编译错误。但是,它可以引用导入的绑定,因为它们也在模块作用域内。