vue3学习 --- 父子组件通信

1,698 阅读4分钟

这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战

如果我们一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变成非常的臃肿和难以维护

所以组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件, 再将这些组件组合嵌套在一起,最终形成我们的应用程序

组件拆分

<template>
  <div>
    <!-- 组件引入时候,即可以使用中划线法(推荐)或者驼峰法 -->
    <child-cpn></child-cpn>
    <ChildCpn></ChildCpn>

    <!-- 如果组件没有内容(没有使用slot)的时候,可以写成单标签,但必须有闭合 -->
    <child-cpn />

    <cpn />
    <Cpn />
  </div>
</template>

<script>
  // 虽然vue-cli帮助我们对webpack的reolve.extensions进行的vue后缀的设置
  // 使我们引入vue组件的时候不需要添加后缀
  // 但是,在开发的时候,还是推荐写上vue后缀
  // 原因有以下2点: (vscode的缺陷)
  // 1. 有vue后缀的时候, 点击文件可以调转 不加没有
  // 2. 有vue后缀的时候, 在模板中使用组件的时候,会有提示 不加没有
  import ChildCpn from './components/ChildCpn.vue'

  export default {
    name: 'App',

    components: {
      // ChildCpn: ChildCpn 的简写
      // 组件使用的名称: 组件对象
      ChildCpn,

      // 如果注册名为cpn,那么只能使用cpn来使用组件
      // 但是如果注册名为Cpn,那么可以通过cpn或者Cpn来使用组件
      Cpn: ChildCpn
   }
}
</script>

组件中的样式

css作用域

如果在组件的样式上,加上了scoped属性,那么在这个组件上设置的样式的作用域仅在当前组件内部,是局部的

如果没有设置scoped, 那么设置的样式的作用域为全局,即为全局样式

<style>
  /* 这里设置的样式是全局样式 */
  h2 {
    color: red;
  }
</style>
<style scoped>
  /* 这里设置的样式是局部样式 */

  /*
    之所以可以变为局部样式
    是因为vue会为当前组件生成唯一hash,并以data-v-[hash值]的方式将其设置为组件的属性,例如: data-v-7ba5bd90
    对应的样式会被修改为 h2[data-v-7ba5bd90],
    所以对应的样式只会在当前组件生效,不会污染别的组件的样式
  */
  h2 {
    color: red;
  }
</style>

样式hash值

父组件

<template>
  <div>
    <h2>Parent Component</h2>
    <child-cpn />
  </div>
</template>

<script>
  import ChildCpn from './components/ChildCpn.vue'

  export default {
    name: 'App',

    components: {
      ChildCpn
    }
  }
</script>

<style scoped>
  h2 {
    color: red;
  }
</style>

子组件

<template>
  <div>
    <h2>Child Component</h2>
    <h2>Child Component</h2>
  </div>
</template>

<script>
export default {
  name: 'ChildCpn'
}
</script>

<style scoped>/style>

实际编译后的html格式为:

IX9xfH.png

可以发现的是,组件hash值属性,会被加载组件的根元素和每一个直接子元素上

那么就意味着,子组件的根元素,也会被加上父组件的hash值

在vue2中,要求我们的每一个组件必须有且只能有一个根组件

但是在vue3中,我们可以不需要有根组件了,vue3在编译的时候,会自动为我们组件的最外层包裹上fragment

所以子组件有的时候,可以写成

<template>
   <h2>Child Component</h2>
   <h2>Child Component</h2>
</template>

<script>
  export default {
    name: 'ChildCpn'
  }
</script>

<style scoped></style>

此时编译后的html为:

IXTWqk.png

可以认为,子组件在编译的时候。

如果有多个元素,那么会自动在最外层包裹一个不会被渲染为实际元素的根元素标签fragment

所以父组件的样式hash值虽然被加载到了fragment标签上,但是因为在渲染的时候fragment最终会被移除

所以如果子组件有多个元素的时候,父组件的样式hash值不会被添加到子组件的顶层元素上

但是如果设置了子组件的样式的时候,因为样式hash值会被设置在根元素和直接子元素上,

所以此时即使fragment被移除,对应的顶层元素上依旧存在对应的样式hash值

IXT5fy.png

但是这仅仅只是子组件有多个顶层元素的时候,如果子元素仅仅只有一个根元素的时候

vue编译器并不会为我们添加fragment标签,因为这没有必要,

此时如果使用的是样式选择器设置的样式的时候,会存在一个问题

因为此时对应的html会被编译为:

IXT7Zr.png

可以看到的是,子组件的元素也被打上了父组件的样式hash值,

所以此时父组件所设置的样式(主要是那些使用标签选择器设置的样式 如将h2{ color: blue; }) 会污染子组件中元素

即使在父组件的样式标签上设置了scoped属性,也是没有意义的

所以不推荐在vue中, 过多的使用标签选择器来设置对应的样式

组件通信

在开发过程中,我们会经常遇到需要组件之间相互进行通信(数据传递)

  • 比如App可能使用了多个Header,每个地方的Header展示的内容不同,那么我们就需要使用者传递给Header一些数据,让其进行展示

  • 又比如我们在Main中一次性请求了Banner数据和ProductList数据,那么就需要传递给它们来进行展示

  • 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件

父子通信

父子组件之间如何进行通信呢?

  • 父组件传递给子组件:通过props属性
  • 子组件传递给父组件:通过$emit触发事件

IXTTlt.png

父组件传递给子组件

父组件传递给子组件可以通过props来完成组件之间的通信

props

什么是Props呢?

  • props是properties的简写
  • Props是你可以在组件上注册一些自定义的attribute
  • 父组件给这些attribute赋值,子组件通过attribute的名称获取到对应的值

Props有两种常见的用法: 字符串数组对象类型

『字符串数组』

父组件

<template>
  <div>
    <!-- 通过props向子组件传递对应的props -->
    <child-cpn title="标题" content="具体内容" />
  </div>
</template>

<script>
  import ChildCpn from './components/ChildCpn.vue'

  export default {
    name: 'App',

    components: {
      ChildCpn
    }
  }
</script>

子组件

<template>
  <h2>{{ title }}</h2>
  <p>{{ content }}</p>
</template>

<script>
export default {
  name: 'ChildCpn',

  // 接收父组件传入过来的值,会被自动加入vue响应式系统,可以直接通过this来进行获取
  props: ['title', 'content']
}
</script>

属性解构

<template>
  <div>
    <!-- 如果传入的props都是一个对象的属性,可以使用以下方式进行属性的解构 -->
    <child-cpn v-bind="info" />
    <!-- 上边的这种方式和下边的方式所对应的结果是一致的 -->
    <child-cpn :title="info.title" :content="info.content" />
  </div>
</template>

<script>
  import ChildCpn from './components/ChildCpn.vue'

  export default {
    name: 'App',

    components: {
      ChildCpn
    },

    data() {
      return {
        info: {
          title: '标题',
          content: '具体内容'
        }
      }
    }
}
</script>

『对象类型』

数组用法中我们只能说明传入的attribute的名称,并不能对其进行任何形式的限制

如果需要进行更多的限制,那么就需要使用对象语法来进行更多的约束

props: {
  // 默认情况下,null和undefined可以作为其它任意类型的子类型
  // 所以默认情况下,null和undefined可以通过除了required:true之外的所有检测
  title: String,
  content: String
}
props: {
  title: {
    // type的可选值为 String | Number | Boolean | Array | Object | Date | Function | Symbol
    type: String, // 设置对应的类型 
    required: true // 必须传值, 值不能为undefined
  },
    
  content: {
    type: String,
    // default和 rquire是两个互斥的操作,所以设置一个即可
    // 如果父组件没有为子组件传递任何的值,那么子组件对应的属性的值为undefined
    // 当父组件显示(主动设置值为undefined)或隐式(没有设置具体的值)时,子组件中就会使用对应的默认值
    default: '默认的content' // 没有传值的时候显示的默认值 
    }
}
title: {
  type: [String, Number], // 多个可能的类型
  require: true
}
props: {
  foo: {
    type: Array,
    default() {
      // 如果props对应的值是引用类型的数据,那么在设置默认值的时候,需要设置成一个函数
      // 对应的默认值应设置为对于函数的返回值
      // 目的是为了避免多个组件实例之间使用同一个引用类型的props而产生数据污染问题
      return []
    }
  }
}
props: {
  title: {
    type: String, // type校验的优先级比自定义校验高
    required: true, // validator可以和其它属性一起使用
    // default: 'chart',


    // 使用校验器自定义校验规则 --- 父组件传入的值会被作为校验器的参数传入
    // 返回值应为boolean值,如果为true则通过校验,否则校验失败
    validator(v) {
      return ['article', 'chart', 'push'].includes(v)
    }
  }
}
props: {
  foo: {
    type: Function,
    // 函数比较特殊, 函数设置默认值的时候,不需要通过一个函数再来返回一个新的函数
    // 因为函数只有在实际调用的时候才会创建对应空间,调用完毕以后会自动释放对应的地址空间
    default() {
      return 'default foo value'
    }
  }
}

「Prop 的大小写命名规则(camelCase vs kebab-case)」

  • HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符

  • 这意味着当你使用 DOM 中的模板时,

    camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名

  • 如果当前模板是字符串模板,因为该模板在交给浏览器进行解析之前会被vue转换一遍

    所以此时props既可以使用camelCase (驼峰命名法),也可以使用 kebab-case (短横线分隔命名)

  • 所以在vue中传递props的时候,推荐使用kebab-case来设置对应props的名称

非Prop的Attribute

当我们传递给一个组件某个属性(包括那些值为函数的属性)

如果该属性没有使用props或emits声明的时候,该属性就称之为 非Prop的 Attribute

对于非prop的attribute,vue默认会进行Attribute继承

ps: 组件上的style和class等属性也会作为非props的attributes存储到$attrs对象中

Tips: 在下边示例中,组件上的某些元素上存在的id属性的作用是为了在编译后生成的html结构中,可以更为方便的判断出那些结构来源于父组件,那些结构来源于子组件, 并不存在实际的逻辑意义

父组件

<template>
  <div id="parent">
     <!-- 父组件, 有一个非props的attribute title  -->  
     <child-cpn :title="title" />
  </div>
</template>

子组件

  1. 单个根节点

当组件有单个根节点时,非Prop的Attribute将自动继承到根节点的Attribute中

<template>
  <div id="child">
    <span>ChildCpn</span>
  </div>
</template>

IXaVoL.png

  1. 多个根节点

    多个根节点的attribute如果没有显示的绑定,vue无法自动进行属性绑定,会报警告,并不进行任何的继承,我们必须手动的指定要绑定到哪一个属性上

    <template>
      <div id="child">ChildCpn</div>
      <div id="child" :title="$attrs.title">ChildCpn</div>
      <div id="child">ChildCpn</div>
    </template>
    

    IXaX6U.png

  2. 组件不希望继承attribute

    在子组件中设置 inheritAttrs: false即可

    <script>
      export default {
        name: 'ChildCpn',
    
        inheritAttrs: false
      }
    </script>
    
$attrs

$attrs是一个proxy对象,里面存放着所有非props的attribute

$attrs的默认值是一个空的proxy对象

我们可以通过 $attrs来访问所有的 非props的attribute

// vue2 --- options api
console.log(this.$attrs)
// vue3 --- composition api
import { useAttrs } from 'vue'
console.log(useAttrs())
子组件传递给父组件

当子组件中有一些内容想要传递给父组件或者子组件发生改变而导致父组件中需要相应发生改变的时候,我们就需要子组件向父组件进行通信

父组件

<template>
  <div id="parent">
    <!-- 监听的事件和实际触发的响应式函数名不一定要一致 -->
    <child-cpn @sendMsg="handleSendMessage" />
    <p>{{ msg }}</p>
  </div>
</template>

<script>
  import ChildCpn from './components/ChildCpn.vue'

  export default {
    name: 'App',

    components: {
      ChildCpn
    },

    data() {
     return {
       msg: ''
     }
  },

  methods: {
    handleSendMessage(v) {
      this.msg = v
    }
  }
}
</script>

子组件

<template>
  <button @click="sendMessage">click me</button>
</template>

<script>
export default {
  name: 'ChildCpn',

  // vue3新增选项, 用来声明需要被触发的事件
  // 可以是一个字符串数组,也可以是一个对象
  // 如果设置为对象的时候,可以对事件进行验证
  emits: ['sendMsg'],
  // 虽然就算不在emits中对对应的事件进行注册,代码也不会报错
  // 和vue2不同的是,在vue3中, 事件也会被作为非props的属性存入$attrs
  // 只有在emits声明后,事件才会从$attrs属性中被移除
  // 没有在emits声明的时候 $attrs = { onSendMsg: 函数体 }
  // 在emits中声明后 $attrs才会变成 {}
  // 注意: 存入$attrs的时候,事件的名称为onSendMsg,不是传入的sendMsg,会自动加上前缀on

  props: ['title'],

  methods: {
    sendMessage() {
      // 监听的事件和触发的事件名不一定要一致
      this.$emit('sendMsg', '需要传递的数据')
    }
  }
}
</script>

上述的案例中emits的属性值是数组格式的,emits的值也可以是对象格式的

emits: {
  // 属性值为空的时候,表示不进行任何的校验
  sendMsg: null
}
emits: {
  // 参数列表为触发事件时候传递的参数列表
  // 触发事件的时候,传递了几个参数, 这里就可以接收几个参数
  // 这个函数是一个自定义validator,要求返回一个boolean值
  // 如果为true,校验通过
  // 如果为false,校验失败 --- 代码依旧会正常执行,只不过会在控制台中输出对应的警告信息
  sendMsg(v) {
    return v > 10
  }
}