# Vue 单文件组件,父子组件间传值、通信,透传属性

TIP

在刚开始学习 Vue 时,我们就对 Vue 中的组件做了以下相关的了解

  • 什么是组件,组件有什么用
  • 什么是根组件,什么是子组件
  • 如何注册全局组件与局部组件,以及组件的使用
  • 什么是单文件组件及定义与使用
  • 什么是单根组件,什么是多根组件

以上内容详细查阅博客图文教程:Vue 组件基础 (opens new window) 部分

本章节开始我们会深入来学习 Vue 组件,本章节主要内容如下:

  • 单文件组件 CSS 功能
  • 组件间通信
  • 父组件向子组件传值 - props
  • 子组件向父组件传值 - emits
  • 组件 v-model
  • 透传属性(Atrributes)

# 一、单文件组件 CSS 功能

TIP

本小节我们来学习单文件组件 CSS 功能,内容涉及:

  • CSS 的默认处理行为
  • scoped属性
  • :deep()伪类
  • :global()伪类
  • CSS Modules
  • CSS 中的v-bind()函数
  • CSS 预处理器

# 1、CSS 的默认处理行为

TIP

默认情况下<style>标签中的样式会作用于所有组件的元素。因为vite

  • 在启动开发服务时,会所有的单文件组件中<style>标签中的样式添加到index.html入口文件的<style>标签中,所以相同的类名,写在后面的会覆盖前面的。
  • 在生产环境下打包时,所有单文件组件<style>标签中的样式会被打包到一个*.css的文件中,所以相同的类名,写在后面的会覆盖前面的。

代码示例

App.vue根组件内容

<script>
  import A from "./components/A.vue";
  import B from "./components/B.vue";
  export default {
    components: {
      A,
      B,
    },
  };
</script>
<template>
  <div class="main">
    <a></a>
    <b></b>
  </div>
</template>

<style>
  * {
    margin: 0px;
    padding: 0px;
  }
</style>

/src/components/A.vue组件内容

<template>
  <div class="box">
    <h3>组件A</h3>
  </div>
</template>

<style>
  .box {
    width: 300px;
    height: 300px;
    margin: 10px;
    border: 1px solid skyblue;
  }

  .box h3 {
    height: 30px;
    text-indent: 1em;
    background-color: skyblue;
  }
</style>

/src/components/B.vue组件内容

<template>
  <div class="box">
    <h3>组件B</h3>
  </div>
</template>

<style>
  .box {
    width: 300px;
    height: 300px;
    margin: 10px;
    border: 1px solid tomato;
  }

  .box h3 {
    height: 30px;
    text-indent: 1em;
    background-color: tomato;
    color: #fff;
  }
</style>

以上代码在开发环境下,最终渲染效果如下:

image-20230623145422566

在生产环境下,最终所有样式打包到*.css文件中,然后在index.html文件中引用,如下图:

image-20230623150604722

相同的样式名 ,后面的会覆盖前面,所以同样以后面的为主。

# 2、scoped 属性

TIP

<style> 标签带有 scoped attribute 的时候,它的 CSS 只会影响当前组件的元素。因为vite

  • 在构建项目时,不管生产还是开发环境,都会给当前组件的 html 元素添加data-v-xxx的自定义属性
  • 同时组件中<style>标签中的选择器后会加上对应的data-v-xxx的属性选择。

以上处理后,就达到了两个组件中的 CSS 样式只能应用于当前组件中的元素,从实现了样式的隔离.

代码示例

在上面的案例的基础上,给 A 和 B 组件中的<style>标签添加scoped属性

/src/components/A.vue组件

<template>
  <div class="box">
    <h3>组件A</h3>
  </div>
</template>

<!-- style标签上添加了 scoped属性 -->
<style scoped>
  .box {
    width: 300px;
    height: 300px;
    margin: 10px;
    border: 1px solid skyblue;
  }

  .box h3 {
    height: 30px;
    text-indent: 1em;
    background-color: skyblue;
  }
</style>

/src/components/B.vue组件

<template>
  <div class="box">
    <h3>组件B</h3>
  </div>
</template>
<!-- style标签上添加了 scoped属性 -->
<style scoped>
  .box {
    width: 300px;
    height: 300px;
    margin: 10px;
    border: 1px solid tomato;
  }

  .box h3 {
    height: 30px;
    text-indent: 1em;
    background-color: tomato;
    color: #fff;
  }
</style>

最终开发环境下渲染后效果如下:

image-20230623151335687

在生产环境下,最终所有样式打包到*.css文件中,然后在index.html文件中引用,如下图:

image-20230623151948954

# 2.1、注意事项

TIP

使用 scoped 后,父组件的样式将不会渗透到子组件中。不过,单根子组件的根节点会同时被父组件的作用域样式和子组件的作用域样式影响。因为vite在构建时,会给单根子组件的根节点元素上加上父组件对应的data-v-xxx的属性。

注意:只有当子组件是单根组件时,才会在根节点元素上添加父组件的data-v-xx属性。

设计初衷

这样设计是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式。常用于调整应用的第三方子组件的样式。

代码示例

在上面案例的基础上

  • App.vue文件中添加如下 CSS 样式
<!--style标签上添加了 scoped -->
<style scoped>
  .box {
    border: 1px solid khaki;
  }

  .box h3 {
    background-color: khaki;
  }
</style>
  • A 组件不做任何修改,在 B 组件中再添加 p 标签,将 B 组件变成多根组件,如下:
<template>
  <div class="box">
    <h3>组件B</h3>
  </div>
  <p></p>
</template>

最终在生产环境下打包后效果如下:

image-20230623154027589

注:

  • B 组件的样式并没有发生任何的改变,因为 B 组件是多根组件,所以并不会受到父组件样式的影响。
  • A 组件的.box元素的边框确实被修改成App组件中设置的样式,但 h3 标签的样式确没有生效。这是为什么呢?我们来看下打包后生成的html结构和*.css文件。
  • 打包后生成的html结构如下:
<div data-v-7a7a37b1="" class="main">
  <!--A start-->
  <div data-v-65097ce7 data-v-7a7a37b1 class="box">
    <h3 data-v-65097ce7="">组件A</h3>
  </div>
  <!--A end-->
  <!--B start -->
  <div data-v-f6858c4e class="box">
    <h3 data-v-f6858c4e>组件B</h3>
  </div>
  <p data-v-f6858c4e=""></p>
  <!--B end -->
</div>
  • 打包后生产的*.css文件内容如下:
/* A组件样式 */
.box[data-v-5c2cdbff] {
  width: 300px;
  height: 300px;
  margin: 10px;
  border: 1px solid skyblue;
}
.box h3[data-v-5c2cdbff] {
  height: 30px;
  text-indent: 1em;
  background-color: #87ceeb;
}
/* B组件样式 */
.box[data-v-deef0120] {
  width: 300px;
  height: 300px;
  margin: 10px;
  border: 1px solid tomato;
}
.box h3[data-v-deef0120] {
  height: 30px;
  text-indent: 1em;
  background-color: tomato;
  color: #fff;
}
/* App 组件样式 */
* {
  margin: 0;
  padding: 0;
}
.box[data-v-a805ad3c] {
  border: 1px solid khaki;
}
.box h3[data-v-a805ad3c] {
  background-color: khaki;
}

注:

通过观察html结构与css,我们发现.box h3[data-v-a805ad3c]选择器并没有生效,因为页面中 h3 标签并没有data-v-a805ad3c属性。

如果我们就是想在根组件App中调整 A 子组件中h3标签的样式,App根组件中控制 h3 标签的 CSS 样式应该是如下写法

.box[data-v-a805ad3c] h3 {
  background-color: khaki;
}

如果想使 App 中的 CSS 样式编译成如下效果,就需要借助:deep()伪类

# 3、:deep() 伪类

TIP

处于scoped样式中的选择器,如果想做更“深度”的。比如:在父组件中调整子组件中根元素之外的其它元素的样式,可以借助:deep()这个伪类。

在上面案例的基础上,修改APP.vue文件中的 CSS 样式

  • 修改前 CSS 样式
.box h3 {
  background-color: khaki;
}
  • 修改后 CSS 样式
.box :deep(h3) {
  background-color: khaki;
}
  • 上面修改后的 CSS 代码最终会被编译成
.box[data-v-7a7a37b1] h3 {
  background-color: khaki;
}

这样就可以在App组件中调整A组件中h3标签的样式。最终渲染后效果如下:

image-20230623160553010

# 4、:global() 伪类

TIP

如果想让某个样式规则应用于全局

有以下两种方式:

  • 写在不带scoped属性的<style>标签中的样式全局可用。注意:<style>标签与<style scoped>标签可以在一个组件中共存
<style>
  /* 写在这里的样式,全局可用 */
</style>
<style scoped>
  /* 写在这里的样式,如果没有使用:global(),只能当前组件内可用 */
</style>
  • 使用:global伪类来,在scoped中被:global()选中的选择器,最终编译后原样输出,不会加上data-v-xx属性选择
<style scoped>
  /* scoped 中写在 :global()中的 css 选择器全局可用*/
  :global(.box) {
    width: 200px;
    height: 200px;
    border: 1px solid khaki;
    margin: 10px;
  }

  :global(.box h3) {
    height: 30px;
    background-color: khaki;
  }
</style>

以上代码最终编译后代码如下:

<style>
  .box {
    width: 200px;
    height: 200px;
    border: 1px solid khaki;
    margin: 10px;
  }

  .box h3 {
    height: 30px;
    background-color: khaki;
  }
</style>

代码示例

APP.vue文件内容如下

<script>
  import A from "./components/A.vue";
  import B from "./components/B.vue";
  export default {
    components: {
      A,
      B,
    },
  };
</script>
<template>
  <div class="main">
    <a></a>
    <b></b>
  </div>
</template>

<!--写在 style标签中的样式,全局可用-->
<style>
  * {
    margin: 0px;
    padding: 0px;
  }

  p {
    height: 30px;
    background-color: skyblue;
    margin: 10px;
  }
</style>

<style scoped>
  /* scoped 中写在 :global()中的 css 选择器全局可用*/
  :global(.box) {
    width: 200px;
    height: 200px;
    border: 1px solid khaki;
    margin: 10px;
  }

  :global(.box h3) {
    height: 30px;
    background-color: khaki;
  }
</style>

A.vueB.vue中内容如下

<!-- A.vue 文件中内容-->
<template>
  <div class="box">
    <h3>组件A</h3>
  </div>
</template>

<!-- B.vue 文件中内容-->
<template>
  <div class="box">
    <h3>组件B</h3>
  </div>
  <p></p>
</template>

最终渲染后效果如下:

image-20230623164308792

# 5、CSS Modules

TIP

一个 <style module> 标签会被编译为 CSS Modules 并且将生成的 CSS class 作为 $style 对象暴露给组件。

代码示例

<template>
  <div :class="$style.main">
    <div>$style对象值</div>
    div
    <div :class="$style.box">
      <h3>App组件</h3>
    </div>
  </div>
</template>
<style module>
  * {
    margin: 0px;
    padding: 0px;
  }

  .main {
    margin: 20px;
  }

  .box {
    width: 200px;
    height: 200px;
    border: 1px solid skyblue;
    margin: 20px;
  }

  .box h3 {
    height: 30px;
    background-color: skyblue;
    color: #fff;
  }
</style>

以上代码,最终渲染后效果如下:

image-20230623172218207

# 5.1、自定义注入名称

TIP

你可以通过给 module attribute 一个值来自定义注入 class 对象的属性名:

<template>
  <!--注意使用时,不要加$符-->
  <div :class="root.main">
    <h3>$style对象值:{{ root }}</h3>
    <div :class="root.box"></div>
  </div>
</template>
<!--root为注入名-->
<style module="root">
  * {
    margin: 0px;
    padding: 0px;
  }

  .main {
    margin: 20px;
  }

  .main h3 {
    font-weight: 400;
    line-height: 40px;
    border-bottom: 1px solid skyblue;
  }

  .box {
    width: 100px;
    height: 100px;
    background-color: skyblue;
    margin: 20px;
  }
</style>

最终渲染后效果与前面的$style效果一模一样。

# 6、CSS 中的 v-bind()

TIP

单文件组件的 <style> 标签中支持使用 v-bind CSS 函数将 CSS 的值链接到动态的组件状态。

v-bind()常用于接受父组件传递给子组件的 CSS 属性的值

代码示例

<script>
  export default {
    data() {
      return {
        color: "red",
      };
    },
  };
</script>

<template>
  <div class="box">Hello Vue!!</div>
</template>
<style scoped>
  .box {
    color: v-bind(color);
  }
</style>

以上代码最终渲染后效果如下:

image-20230623182044460

注:

v-bind()函数中的变量最终被转换成哈希化的 CSS 自定义属性,然后在内联样式中通过 CSSvar()函数使用自定义属性的值

当前组件中通过v-bind()形式绑定的 CSS,最终最会转换成哈希化的 CSS 自定义属性添加到当前组件的根元素上,同时会在在源值变更的时候响应式地更新

<script>
  export default {
    data() {
      return {
        color: "red",
      };
    },
  };
</script>

<template>
  <div class="box">
    <p>Hello Vue!!</p>
    <h3>h3</h3>
  </div>
</template>
<style scoped>
  .box {
    color: green;
  }

  h3 {
    color: v-bind(color);
  }
</style>

以上代码最终渲染后效果如下:

image-20230623183702349

# 7、CSS 预处理器

TIP

<style>标签上可以使用lang这个属性来声明 CSS 的预处理器语言。

比如:<style lange='scss'> ,表示可以使用 sass 来编写 CSS。

<template>
  <div class="box">
    <p>Hello Vue!!</p>
  </div>
</template>
<style scoped lang="scss">
  .box {
    width: 100px;
    height: 100px;
    border: 1px solid skyblue;

    p {
      color: red;
    }
  }
</style>

要使sass最终能被成功编译成 CSS,还需要执行以下命令安装Sass的预处理器依赖

npm i sass -D # 安装sass 依赖包

最终上面<style>标签中的 Sass 编译成如下 CSS 样式

.box[data-v-7a7a37b1] {
  width: 100px;
  height: 100px;
  border: 1px solid skyblue;
}

.box p[data-v-7a7a37b1] {
  color: red;
}

# 8、总结

TIP

本小节重点掌握如下单文件组 CSS 功能

# 8.1、scoped 属性

TIP

  • <style> 标签带有 scoped attribute 的时候,它的 CSS 只会影响当前组件的元素。因为 html 标签被编译后会被加上data-v-xx属性,对应的 CSS 选择器会加上[data-v-xx]属性选择。
  • 单根子组件的根元素会被加上父组件的data-v-xx属性,这样涉及的目的是为了让父组件可以调整子组件的根元素样式

编译前

<template>
  <div class="box"></div>
</template>
<style scoped>
  .box {
  }
</style>

编译后

<div data-v-7a7a37b1="" class="box">scoped</div>

<style>
  .box[data-v-7a7a37b1] {
    color: red;
  }
</style>

# 8.2、:deep() 伪类

TIP

<style scoped>标签中使用:deep()伪类可以帮助我们在父组件中选择子组件中根元素以外的元素,这样我们就可以在父组件中调整子组件的样式

编译前

<style scoped>
  .box :deep(h3) {
    background-color: skyblue;
  }
</style>

编译后

<style>
  .box[data-v-7a7a37b1] h3 {
    background-color: skyblue;
  }
</style>

# 8.3、:global() 伪类

TIP

<style scoped>标签中使用:global()伪类可以让某一个样式规则应用于全局。

要让某个样式规则应用于全局还可以直接把该样式规则写在<style>标签中。

编译前

<style scoped>
  :global(.box h3) {
    background-color: skyblue;
  }
  /* 注意,不要这样写*/
  .box :global(p) {
    color: red;
  }
</style>

编译后

<style>
  .box h3 {
    background-color: skyblue;
  }
  p {
    color: red;
  }
</style>

# 8.4、v-bind() 函数

TIP

v-bind()函数可以将 CSS 的值链接到动态的组件状态。

编译前

<script>
  export default {
    data() {
      return {
        color: "red",
      };
    },
  };
</script>

<template>
  <div class="box">Hello Vue3</div>
</template>

<style scoped>
  .box {
    color: v-bind(color);
  }
</style>

编译后

<style>
  .box[data-v-7a7a37b1] {
    color: var(--7a7a37b1-color);
  }
</style>
<body>
  <div data-v-7a7a37b1 class="box" style="--7a7a37b1-color: red;">
    Hello Vue3
  </div>
</body>

# 8.5、CSS Modules

TIP

一个 <style module> 标签会被编译为 CSS Modules 并且将生成的 CSS class 作为 $style 对象暴露给组件。

编译前

<template>
  <div :class="$style.box">Hello Vue3</div>
</template>

<style module>
  .box {
    color: red;
  }
</style>

编译后

<style>
  ._box_1tlvs_2 {
    color: red;
  }
</style>
<body>
  <div class="_box_1tlvs_2">Hello Vue3</div>
</body>

# 8.6、CSS 预处理器

TIP

  • <style>标签上可以使用lang这个属性来声明 CSS 的预处理器语言,如:<style lang="scss"> ,表示可以采用 Sass 语法编写 CSS。
  • 要使代码最终编译成CSS,需要安装对应的预处理器依赖。执行npm i sass -D安装 sass 依赖

编译前

<style lang="scss">
  .box {
    width: 100px;
    height: 100px;
    border: 1px solid skyblue;

    p {
      color: red;
    }
  }
</style>

编译后

<style>
  .box {
    width: 100px;
    height: 100px;
    border: 1px solid skyblue;
  }
  .box p {
    color: red;
  }
</style>

# 二、组件间通信

TIP

组件间通信是指各个组件间之间可以相互传递数据,实现数据的共享。

比如:

父组件可以给子组件传数据,子组件也可以给父组件传数据,各个兄弟组件之间也可以互相传递数据。

组件间为什么需要相互通信 ?

我们来看下面这副图

image-20230510164957048

注:

上图展示了在父组件App中调用同一个子组件List两次,但是要求这两个子组件最终展示数据都不一样。这时候,子组件 List 要展现的数据就不能写死,需要父组件传递过来,父组件传递的数据决定了子组件最终显示的数据。

在往后的课程中,我们将会学习以下几种组件间通信的方式:

  • 父子组件之间的通信
  • 兄弟组件之间的通信
  • 祖孙与后代组件之间的通信
  • 非关系组件之间的通信

# 三、父组件向子组件传值 - props

TIP

本小节重点学习父组件如何通过props向子组件传递数据,内容主要有:

  • props 选项
  • Prop 名字格式
  • 静态 VS 动态 Prop
  • props 对象写法
  • Prop 校验
  • Boolean 类型 Prop 简写形式
  • 使用一个对象绑定多个 Prop
  • props 选项被处理的时机
  • 单向数据流
  • 总结

# 1、props 选项

TIP

父组件向子组件传递数据,可以通过自定义 Prop(属性)来传递。

  • 在使用子组件的时候,在子组件标签上添加自定义属性(Prop),把需要传递的数据做为属性的值就可以,如下:
<!--List子组件  title与 info 属性用来实现在父组件中传递数据给到子组件-->
<List title="新闻标题" info="新闻内容" />
  • 子组件需要通过prpos选项来接受父组件传递过来的 Prop(属性)。然后就可以在模板标签中通过插值语法使用接受的属性,也可以通过组件实例直接访问。
<!--List组件-->
<script>
  export default {
    // 通过props选项接受父组件通过Prop传递的数据
    props: ["title", "info"],
    created() {
      console.log(this.title); // 新闻标题
      console.log(this.info); // 新闻内容
    },
  };
</script>
<template>
  <!--对于接受过来的数据,可以直接在模板中使用-->
  <h3>{{ title }}</h3>
  <div>{{ info }}</div>
</template>

代码演示

App.vue 根组件

<script>
  // 导入组件
  import List from "./components/List.vue";
  export default {
    // 注册组件
    components: {
      List,
    },
  };
</script>

<template>
  <!-- 使用组件,title与info属性值为父组件向子组件传入的数据-->
  <List title="新闻标题" info="新闻内容" />
  <List title="最新动态" info="动态内容"></List>
</template>

List.vue 子组件

<script>
  export default {
    // 通过props选项接受父组件通过Prop传递的数据
    props: ["title", "info"],
    beforeCreate() {
      console.log("title", this.title);
      console.log("info", this.info);
    },
  };
</script>
<template>
  <!--对于接受过来的数据,可以直接在模板中使用-->
  <h3>{{ title }}</h3>
  <div>{{ info }}</div>
</template>

代码最终渲染后结果

image-20230510171706123

image-20230630132934048

# 2、Prop 名字格式

TIP

定义属性: 如果在传递数据时,属性的名字很长,由两个以上单词组成,建议在HTML标签上使用kebab-case形式来定义属性名。

主要目标是为了与 HTML 原生的属性对齐。如下:

<List data-title="新闻标题" data-info="新闻内容" />

接受属性:

在子组件的props选项来接受属性时,推荐使用camelCase形式,因为他是一个合法的 JS 标识符,而kebab-case形式并不是合法的 JS 标识符,在插值语法中没有办法使用。

<!--data-title为不合法标识符,Vue没办法解析-->
<div>{{ data-title }}</div>
<!--正确写法-->
<div>{{ dataTitle }}</div>

代码演示

App.vue根组件

<script>
  // 导入组件
  import List from "./components/List.vue";
  export default {
    // 注册组件
    components: {
      List,
    },
  };
</script>

<template>
  <!-- 使用组件,title与info属性值为父组件向子组件传入的数据-->
  <List data-title="新闻标题" data-info="新闻内容" />
  <List data-title="最新动态" data-info="动态内容"></List>
</template>

List.vue 根组件

<script>
  export default {
    data() {
      return {};
    },
    // 以下两种props写法都可以,但在模板中使用时需要采用 dataTitle与dataInfo写法
    // props: ["data-title", "data-info"],
    props: ["dataTitle", "dataInfo"],
  };
</script>
<template>
  <h3>{{ dataTitle }}</h3>
  <div>{{ dataInfo }}</div>

  <!--以下为错误写法-->
  <!--
    <h3>{{ data-title }}</h3>
    <div>{{ data-info }}</div>
	-->
</template>

# 3、静态 VS 动态 Prop

TIP

组件在进行 prop 传值时,属性的值可以是一个静态值,也可以是一个动态值。

  • 以下写法的属性为静态属性,其属性值在被接受时,类型永远是字符串
<List title="新闻标题" info="{a:1,b:2}" num="10" />
  • 使用v-bind或缩写:绑定的属性为动态属性,他可以传递任意类型的值,值在被接受时,可以区分不同类型。
<List :title="news.title" :info="{a:1,b:2}" :num="10" />

代码演示

App.vue根组件

<script>
  import List from "./components/List.vue";
  export default {
    data() {
      return {
        news: {
          title: "动态值",
        },
        bool: true,
      };
    },
    components: {
      List,
    },
  };
</script>

<template>
  <List title="静态值" info="{a:1,b:2}" num="10" bool="bool" />
  <List :title="news.title" :info="{ a: 1, b: 2 }" :num="10" :bool="bool" />
</template>

List.vue子组件

<script>
  export default {
    data() {
      return {};
    },
    props: ["title", "info", "num", "bool"],
  };
</script>

<template>
  <h3>{{ title }}</h3>
  <div>{{ info }}----{{ info.a }} -- {{ info.b }}--{{ typeof info }}</div>
  <div>num传递过来的值是{{ num }} 类型为 {{ typeof num }}</div>
  <div>bool传过来的值 {{ bool }} 类型为{{ typeof bool }}</div>
</template>

image-20230510192021929

# 4、props 对象写法

TIP

props 选项的值可以是一个字符串类型的数组,也可以是一个对象。

  • 如果 props 接受的属性值不需要做类型的校验,可以采用前面讲到的数组写法
  • 如果 props 对于传过来的属性值有严格的类型要求,我们可以采用对象的形式来声明 props

如:

export default {
  props: {
    title: String, // title值必需为子符串类型
    age: Number, // num值必需为数字类型
    bool: Boolean, // bool 值必需为数字类型
    address: [Object, String], // info值可以是字符串类型,也可以是对象类型
  },
};

如果传递的值类型不符合要求,控制台会抛出警告

代码演示

App.vue根组件

<script>
  import Person from "./components/Person.vue";
  export default {
    data() {
      return {
        userName: "清心",
        age: 12,
        address: {
          province: "湖南",
          city: "长沙",
        },
      };
    },
    components: {
      Person,
    },
  };
</script>

<template>
  <!-- user-name的值是字符,age的值是number,address的值是一个对象-->
  <Person :user-name="userName" :age="age" :address="address" />
  <!-- 以下age传的是字符串,不符合要求-->
  <Person :user-name="userName" age="30" address="陕西西安" />
</template>

Person.vue子组件

<script>
  export default {
    props: {
      userName: String,
      age: Number,
      bool: Boolean,
      address: [Object, String],
    },
  };
</script>

<template>
  <div>用户姓名:{{ userName }}</div>
  <div>用户年龄:{{ age }} 岁</div>
  <div>
    用户地址: {{ typeof address === "string" ? address : address.province +
    address.city }}
  </div>
  <div>---------------------------------</div>
</template>

以上代码,最终渲染效果如下:

image-20230511144552371

# 4.1、属性支持的检测类型

TIP

属性支持的检测类型可以是以下这些原生的构造函数:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

也可以是自定义的类或构造函数,Vue 将会通过 instanceof 来检查类型是否匹配。

例如下面这个类:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

你可以将其作为一个 Prop 的类型

<script>
  export default {
    props: {
      userInfo: Person,
    },
  };
</script>

代码演示

Person.js 中定义 Person 类

export class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

App.vue 根组件

<script>
  // 导入People组件
  import People from "./components/People.vue";
  // 导入 Person类
  import { Person } from "./common/Person.js";
  export default {
    data() {
      return {
        // userInfo 数据类型为 Person类的实例
        userInfo: new Person("艾编程", 33),
      };
    },
    components: {
      People,
    },
  };
</script>

<template>
  <People :user-info="userInfo" />
</template>

People.vue 子组件

<script>
  import { Person } from "../common/Person.js";
  export default {
    props: {
      userInfo: Person,
    },
  };
</script>

<template>
  <div>{{ userInfo.name }} -- {{ userInfo.age }}</div>
</template>

最终渲染后,结果如下:

image-20230511150543923

如果userInfo的内容如下

userInfo:{
    name:"艾编程",
    age:33
}

最终渲染后效果如下图,可以正常显示效果,但是控制台会抛出警告

image-20230629145118670

# 5、Prop 校验

TIP

如果我们需要对传过来的值做更严格的校验,可以将 props 选项对象的属性值写成一个带有校验选项的对象。

如下:

<script>
  export default {
    props: {
      myProps: {
        // 数据类型
        type: Number,

        // 属性是否为必传,true表示必传
        required: true,

        // 表示未传该属性时,属性的默认值,如果没有配置default选项
        // 对于没有传的非bool类型属性,默认值为undefind,bool类型属性为false
        // required与default 不能同时出现,因为必传,就决定了不会启用默认值
        default: 17,
        /* default可以是一个函数,函数返回值属性默认值
                default(rawProps){
                	return 28
            		}
            	*/

        // 数据校验函数,如果返回值为false,表示校验失败,控制台会抛出警告
        validator(value) {
          // ....
          return true;
        },
      },
    },
  };
</script>

代码演示

App.vue 根组件

<script>
  import Person from "./components/Person.vue";
  export default {
    data() {
      return {
        username: "清心",
        age: 12,
        tel: 13687382323,
      };
    },
    components: {
      Person,
    },
  };
</script>

<template>
  <!--
        username用户名没传,启用默认值,
        tel电话号码不符合要求,会抛出警告
        age年龄为必填,没有填,也会抛出警告
	-->
  <Person :tel="12345678912" />
  <Person :age="age" :username="username" :tel="tel" />
</template>

Person.vue 子组件

<script>
  export default {
    props: {
      username: {
        type: String, // 数据类型
        default: "艾编程粉丝", // 默认值
      },
      age: {
        type: Number, // 数据类型
        required: true, // 属性必传
      },
      tel: {
        type: Number,
        required: true, // 属性必传
        // 数据校验函数,如果不是11位电话数字,则抛出错误
        validator(value) {
          return /^1[3-9]\d{9}$/.test(value);
        },
      },
    },
  };
</script>

<template>
  <div>用户姓名:{{ username }}</div>
  <div>用户年龄:{{ age }} 岁</div>
  <div>电话号码:{{ tel }}</div>
  <div>---------------------------------</div>
</template>

以上代码,最终渲染效果如下:

image-20230510201703963

# 6、Boolean 类型 Prop 简写形式

TIP

当父组件向子组件通过 Prop 传值时,传递的属性值为 Boolean 类型时,可以支持以下简写形式

<!--完整写法-->
<Person :bool="true" />
<Person :bool="false" />

<!--以下为简写形式-->
<Person bool />
<!-- 等同于传入 :bool="true" -->
<Person />
<!-- 等同于传入 :bool="false" -->

注意:

在子组件的props选项中要指定该属性值的类型为 Boolean 类型。

如下:

export default {
  props: {
    bool: Boolean, // 或 [Boolean, String]  Boolean一定要出现在第一位,以下简写形式才支持
  },
};

代码演示

App.vue根组件

<script>
  import Person from "./components/Person.vue";
  export default {
    components: {
      Person,
    },
  };
</script>

<template>
  <Person :bool="true" />
  <Person bool />
  <Person />
</template>

Person.vue 子组件

<script>
  export default {
    props: {
      bool: Boolean, // 或[Boolean, String]  Boolean一定要出现在第一个
    },
  };
</script>

<template>
  <div>bool的值{{ bool }}</div>
</template>

以上代码,最终编译结果如下:

image-20230510205033391

# 7、使用一个对象绑定多个 Prop

TIP

如果你想要将一个对象的所有属性都当作 props 传入,你可以使用没有参数的 v-bind

如下:

<script>
  import Person from "./components/Person.vue";
  export default {
    data() {
      return {
        userInfo: {
          username: "清心",
          age: 33,
          tel: 13523456543,
        },
      };
    },
    components: {
      Person,
    },
  };
</script>

<template>
  <Person v-bind="userInfo" />
  <!--以上方式等价于-->
  <Person
    :username="userInfo.username"
    :age="userInfo.age"
    :tel="userInfo.tel"
  />
</template>

# 8、Props 选项被处理的时机

TIP

Props 选项是在生命周期函数beforeCreate之前被解析,且 prop 的校验是在组件实例被创建之前处理的。

  • 所以在datacomputedmethod选项中可以访问到Props选项中的属性。
  • Props选项中没有办法访问到``datacomputedmethod`等选项中的属性。

代码演示

App.vue

<script>
  import Person from "./components/Person.vue";
  export default {
    data() {
      return {
        userInfo: {
          username: "清心",
          age: 35,
          tel: 13678453321,
        },
      };
    },
    components: {
      Person,
    },
  };
</script>

<template>
  <!--一次性绑定多个属性-->
  <Person v-bind="userInfo" />
</template>

Person.vue根组件

<script>
  export default {
    data() {
      return {
        dataName: "艾编程",
        dataAge: 33,
        _username: this.username, // 可以访问到
        _age: this.age, // 可以访问到
        _tel: this.tel, // 可以访问到
      };
    },
    props: {
      username: {
        type: String,
      },
      age: {
        type: Number,
        default: 16,
      },
      tel: {
        type: Number,
        validator(value) {
          // 这里面访问不到组件实例,因为此时组件实例还没有创建
          console.log("this指向", this); // undefined
          return /^1[3-9]\d{9}$/.test(value);
        },
      },
    },
    beforeCreate() {
      // 无法访问到data中属性
      console.log("无法访问到data中属性", this.dataName, this.dataAge);
      // 可以访问到props中属性
      console.log("可以访问到props属性", this.username, this.age, this.tel);
    },
  };
</script>

<template>
  <div>用户名:{{ _username }}</div>
  <div>年龄:{{ _age }}</div>
  <div>电话:{{ _tel }}</div>
</template>

image-20230510213454413

# 9、更新 prop 的值

TIP

如果需要更新 prop 的值,推荐在父组件中更新,每次父组件中的数据更新后,所有子组件中对应的 props 都会被更新到最新值。

<script>
  import Count from "./components/Count.vue";
  export default {
    data() {
      return {
        num: 10,
      };
    },
    components: {
      Count,
    },
  };
</script>
<template>
  <!--父组件中更新num,子组件中会同步更新-->
  <button @click="num++">更新num值</button>
  <Count :num="num" />
</template>
<!--Count 子组件-->
<script>
  export default {
    props: ["num"],
  };
</script>
<template>
  <div>Count的值:{{ num }}</div>
</template>

以上案例最终渲染效果如下:

GIF2023-6-2915-42-32

# 10、单向数据流

TIP

所有的 props 都遵循着单向绑定原则,子组件中可以使用父组件传过来的数据,但这些数据是只读的,在子组件中不能去修改这些数据。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。

如果在子组件中更改了 prop 的值,会在控制台抛出警告,这就意味着我们不应该这样做。

export default {
  props: ["money"],
  created() {
    // ❌ 警告!prop 是只读的!  子组件中不能修改父组件传过来的prop
    this.money = 1000;
  },
};

注:

但在某些情况下,我们确实需要在子组件中更改 props 中属性的值,比如以下两个场景:

  • 场景一: prop 用来传入初始值,而子组件想在之后将其作为一个局部数据属性
  • 场景二: 需要对传入的 prop 值做进一步的转换

那应该如何操作呢 ?

# 10.1、更改基本数据类型的 prop 值

TIP

针对场景一: prop 用来传入初始值,而子组件想在之后将其作为一个局部数据属性。

如果属性的值是基本数据类型,在这种情况下,最好是新定义一个局部数据属性,从 props 上获取初始值即可。

代码演示

App.vue

<script>
  import Person from "./components/Person.vue";
  export default {
    data() {
      return {
        money: 1000,
      };
    },
    components: {
      Person,
    },
  };
</script>

<template>
  <Person :money="money" />
</template>

m Person.vue

<script>
  export default {
    props: ["money"],
    data() {
      return {
        // 只是将money作为初始值
        // 然后将其赋值给到一个新的属性,这样后期更新就和money无关了
        newMoney: this.money,
      };
    },
  };
</script>

<template>
  <!--子组件中,绑定newMoney属性-->
  <div>当前账户初始金额:{{ newMoney }}</div>
  <button @click="newMoney = newMoney - 200">消费200</button>
</template>

代码渲染后效果如下:

GIF2023-5-1022-15-45

针对场景二:

需要对传入的 prop 值做进一步的转换

如果属性的值是基本数据类型,在这种情况中,最好是基于该 prop 值定义一个计算属性

<script>
  export default {
    props: ["money"],
    data() {
      return {
        // 只是将money作为初始值
        // 然后将其赋值给到一个新的属性,这样后期更新就和money无关了
        newMoney: this.money,
      };
    },
    computed: {
      myMoney() {
        return "¥" + this.newMoney + "元";
      },
    },
  };
</script>

<template>
  <!--子组件中,绑定newMoney属性-->
  <div>当前账户初始金额:{{ myMoney }}</div>
  <button @click="newMoney = newMoney - 200">消费200</button>
</template>

代码渲染后效果如下:

GIF2023-5-1022-12-42

# 10.2、更改对象 / 数组类型的 prop 值

TIP

如果 prop 的值是数组或对象,我们虽然不能直接修改属性的值(重新给属性赋值),但我们依然可以更改数组和对象内部的值。

因为对象和数组是引用数据类型,所以修改对象的值,是可以修改父组件中的的数据。

App.vue

<script>
  import Person from "./components/Person.vue";
  export default {
    data() {
      return {
        userInfo: {
          userName: "清心",
          age: 33,
        },
        list: [1, 2, 3],
      };
    },
    components: {
      Person,
    },
  };
</script>

<template>
  <Person :user-info="userInfo" :list="list" />
</template>

Person.vue

<script>
  export default {
    props: ["userInfo", "list"],
    methods: {
      update() {
        this.userInfo.age = 55;
        this.list.push("A");
      },
    },
  };
</script>

<template>
  <div>{{ userInfo.age }} -- {{ list }}</div>
  <button @click="update">更新数据</button>
</template>

以上代码渲染后效果如下:

GIF2023-5-1023-10-31

注:

但官方非常不推荐我们这样做,因为这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。

假设父组件中的某个数据传递给了 10 个子组件,然后每个子组件内部都有一套修改该数据的方法,那最后数据的管理将会变得非常的混乱。如何所有修改数据的方法都交组父组件来管理,那对数据的管理将会更清淅。

所以,针对 prop 的值是对象或数组,而我们又想在子组件中更改这些数据时,官方给我们提供了一种最佳的实现方式:子组件抛出一个事件来通知父组件做出改变。这也是我们下小节需要学习的内容。

# 11、总结

TIP

本小节我们主要学习了以下内容:

props 的两种写法

  • 数组写法:如果 Prop 的值没有严格的类型要求,可以采用数组写法,比较简洁
  • 对象写法:
    • 如果 Prop 的值有严格的类型要求,则需要采用对象写法,如果类型不符何要求,会在控制台抛出警告。
    • 属性类型检测,支持StringNumberBooleanArrayObjectDateFunctionSymbol原生的 JS 类型,也支持处自定义的类或构造函数。
// 函数写法
props:["title","info"]

// 对象写法
props:{
    title:String,
    info:Array
}

Prop 名字格式

属性名由多个单词组成,标签中推荐使用kebab-case写法,在 props 选项与模板中推荐使用camelCase写法,Vue 内部会自动实现转换

<List data-title="新闻" />

<script>
  export default {
    props: ["dataTitle"],
  };
</script>
<template>
  <div>{{dataTitle}}</div>
</template>

静态 Prop 与动态 Prop

  • 静态属性的值,在接受收时永远都是字符串类型。
  • 动态属性的值,可以是任意的类型,在接受会也会做类型区分。
<!--静态Prop  num后面的10在被接受后类型为:String-->
<List num="10" />
<!--动态Prop  num后面的10在被接受后类型为:Number-->
<List :num="10" />

Prop 校验

如果我们需要对接受的 Prop 做更严格的校验时,我们可以将 Prop 的的值配置为一个带有校验选项的对象

<script>
  export default {
    props: {
      myProps: {
        typeof: Number, // 数据类型
        required: true, // 属性是否为必传,true表示必传
        default: 17, // 默认值
        // 数据校验函数,如果返回值为false,表示校验失败,控制台会抛出警告
        validator(value) {
          // ....
          return true;
        },
      },
    },
  };
</script>

Prop 的两种特殊情况

  • Boolean 类型的 Prop 支持以下简写形式
<!--完整写法-->
<Person :bool="true" />
<!--简写形式-->
<!-- 等同于传入 :bool="true" -->
<Person bool />
<!-- 等同于传入 :bool="false" -->
<Person />

注意在子组件的 props 选项中声明必属性时,必需要指明类型为 Boolean,否则不支持简写

export default {
  props: {
    bool: Boolean,
  },
};
  • 可以使用一个对象绑定多个 Prop
<script>
  export default {
    data() {
      return {
        obj: {
          a: 1,
          b: 2,
          c: 3,
        },
      };
    },
  };
</script>
<template>
  <Person v-bind="obj" />
  <!--以上方式等价于-->
  <Person :a="obj.a" :b="obj.b" :c="obj.c" />
</template>

props 选项被处理时机

Props 选项是在生命周期函数beforeCreate之前被解析,且 prop 的校验是在组件实例被创建之前处理的,所以

  • 在校验方法validator中,没有办法访问到组件实例,validator方法中 this 为undefined
  • datacomputedmethod选项中可以访问到Props选项中的属性。
  • Props选项中没有办法访问到``datacomputedmethod`等选项中的属性

单向数据流

Prop 数据只能从父组件流向子组件,在子组件中 Prop 是只读的,在子组件中不能更改 Prop 的值。

修改 prop 的值

  • 如果我们想要修改prop的值,推荐在父组件中来修改。
  • 不过在有些情况下,我们确实需要在子组件中更改 Prop 的值,比如以下两个场景:

场景一: prop 用来传入初始值,而子组件想在之后将其作为一个局部数据属性。

如果属性的值是基本数据类型,最好是新定义一个局部数据属性,从 props 上获取初始值即可。

针对场景二: 需要对传入的 prop 值做进一步的转换。

如果属性的值是基本数据类型,最好是基于该 prop 值定义一个计算属性

  • 如果 prop 的值是数组或对象,需要在子组件中更改他们的值,则推荐使用下一小节讲到的:子组件抛出一个事件来通知父组件做出改变

# 四、子组件向父组件传值 - emits

TIP

在上一小节中我们学习了在父组件中通过props向子组件传递数据,但在子组件中不能修改父组件中传过来的数据。但某些情况下我们确实需要通过子组件去操作父组件中的数据,或传一些数据给到父组件。所以本小节要讲到的自定义事件,就可以帮助我们实现这个目标。

本小节,我们会从以下几个方面来展开讲解

  • 监听与触发自定义事件
  • emits 选项
  • 自定义事件名格式
  • 自定义事件参数
  • 自定义事件校验
  • 自定义事件注意事项

# 1、监听与触发自定义事件

TIP

自定义事件也可以称为组件事件,因为自定义事件主要是绑定在组件身上,用来解决子组件向父组件传递数据或修改父组件中数据。

所以这里要与原生 JS 事件做区分,原生 JS 事件主要是绑定在原生的 HTML 元素身上。

监听事件

在父组件中可以通过v-on(缩写为@)来监听事件:

<script>
  methods: {
      // 下面的update-event事件触发后,就会调用update更新数据
      update() { /* ... */}
  }
</script>
<!-- 
    update-event为事件名,类似于原生JS中click,mouseover等事件名
    update为事件处理函数,即事件触发后调用的函数
-->
<MyComponent @update-event="update" />

触发事件

在子组件的模板表达式中,可以直接使用$emit()方法触发自定义事件

<!--MyComponent 子组件-->
<!--updateEvent是触发的事件名,事件名在这里要采用驼峰命名-->
<button @click="$emit(updateEvent)">更新数据</button>

$emit() 方法在组件实例上以 this.$emit() 的方式触发自定义事件

// MyComponent 子组件
export default {
    methods: {
        updateData() {
            <!--updateEvent是触发的事件名,事件名在这里要采用驼峰命名-->
            this.$emit('updateEvent')
        }
    }
}

代码演示

App.vue 根组件

<script>
  import List from "./components/List.vue";
  export default {
    data() {
      return {
        num: 1,
        arr: [1, 2, 3],
      };
    },
    components: {
      List,
    },
    methods: {
      // 下面的update-event事件触发后,就会调用update更新数据
      update() {
        this.num++;
        this.arr.push("A");
      },
    },
  };
</script>
<template>
  <!--监听update-event自定义事件,update为事件处理回调函数-->
  <List :num="num" :arr="arr" @update-event="update" />
</template>

List.vue 子组件

<script>
  export default {
    // 接受传过来的Prop
    props: ["arr", "num"],
    methods: {
      updateDate() {
        this.$emit("updateEvent");
      },
    },
  };
</script>
<template>
  <div>
    <!--button绑定了click事件,点击按扭后,通过$emit方法触发了updateEvent自定义事件,事件触发后,就会调用事件绑定的update方法,更新数据-->
    <button @click="$emit('updateEvent')">更新数据1</button>
    <!--点击按扭,会调用updateDate方法,在此方法中通过this.$emit方法,触发了updateEvent自定义事件,事件触发后,就会调用事件绑定的update方法,更新数据-->
    <button @click="updateDate">更新数据2</button>

    <div>num的值是:{{ num }}</div>
    <ul>
      <li v-for="item in arr">{{ item }}</li>
    </ul>
  </div>
</template>

以上代码最终渲染效果如下

GIF2023-5-1119-52-14

总结:自定义事件监听与触发

  • 在父组件中通过v-on(缩写为@)监听自定义事件,同时添加事件处理回调函数
  • 在子组件模板中通过$emit方法触发自定义事件;
  • 在子组件实例上通过this.$emit()方式触发自定义事件

# 2、emits 选项

TIP

emits选项用来显示声明子组件需要触发的事件,他支持数组与对象两种写法

以下是数组写法

export default {
  emits: ["updateEvent", "delEvent"],
};

在单根组件中,不用emits选项来声明子组件要触发的事件,好像对我们执行代码没有什么影响,控制台也不报警告。

<template>
  <!--单个元素为根-->
  <div>
    <button @click="$emit('updateEvent')">更新数据1</button>
    <button @click="updateDate">更新数据2</button>
    <div>num的值是:{{ num }}</div>
    <ul>
      <li v-for="item in arr">{{ item }}</li>
    </ul>
  </div>
</template>

在多根组件中不用emits选项来声明子组件要触发的事件,控制台就会抛出如下警告

<template>
  <!--多个元素为根 -->
  <button @click="$emit('updateEvent')">更新数据1</button>
  <button @click="updateDate">更新数据2</button>
  <div>num的值是:{{ num }}</div>
  <ul>
    <li v-for="item in arr">{{ item }}</li>
  </ul>
</template>

image-20230511215305658

温馨提示:

不管子组件是单根组件还是多根组件,我们都推荐使用emits选项来完整声明所有要触发的自定义事件。这样有两个好处:

  • 通过emits选项,一眼就能知道当前组件中需要触发那些自定义事件
  • 可以让 Vue 更好的将事件透传属性作出区分

关于透传属性,我们在下小节讲会讲到

# 3、自定义事件名格式

TIP

与 prop 名一样,事件的名字也提供了自动的格式转换。在父组件中监听自定义事件时,推荐采用kebab-case形式为自定义事件命名。

在组子件中触发自定义事件时,推荐采用camelCase 形式书写自定义事件名。

<!-- 父组件中监听事件,事件名以kebab-case形式命令 -->
<List @update-event="update" />

<!-- 在子组件中触发事件时,采用camelCase形式书写自定义事件名 -->
<button @click="$emit('updateEvent')">更新数据1</button>

# 4、自定义事件参数

TIP

有时候我们需要在触发自定义事件时传一些数据给到父组件,则可以将需要传递的数据作为$emit()方法的第二个及之后的参数传入。

<!--$emit的第一个参数为自定义事件名,之后的每个参数为需要传放的数据-->
<button @click="$emit('updateEvent',5,6,7)">更新数据1</button>

在父组件中监听事件,传递过来的数据可以通过事件处理回调函数的参数接受到。事件处理函数可以采用以下两种写法

  • 事件处理回调函数,写成一个箭头函数
<List @update-event="(a,b,c)=>sum=a+b+c" />
  • 事件处理函数为组件的方法,定义在methods选项中
<List @update-event="update" />
methods: {
    update(a, b, c) {
        // a b c对应传过来的数据 5 6 7
        console.log(a, b, c);
    }
    // 或通过arguments来接收
    /*
    update() {
        Array.from(arguments).forEach((item) => {
            this.arr.push(item)
        })
    }
    */
}

代码演示

App.vue根组件

<script>
  import List from "./components/List.vue";
  export default {
    data() {
      return {
        list: [1, 2, 3, 4],
      };
    },
    components: {
      List,
    },
    methods: {
      del(index) {
        this.list.splice(index, 1); // 从index下标删除1个元素
      },
    },
  };
</script>
<template>
  <List
    :list="list"
    @add-event="(...arg) => list.push(...arg)"
    @del-event="del"
  />
</template>

List.vue子组件

<script>
  export default {
    props: ["list"],
    emits: ["addEvent", "delEvent"],
  };
</script>

<template>
  <!--addEvent事件,用于向数组末尾添加元素-->
  <button @click="$emit('addEvent', 5, 6, 7)">添加多条数据</button>
  <!--delEvent事件,根据指定下标删除数组list中元素-->
  <button @click="$emit('delEvent', 1)">删除下标为1的元素</button>
  <ul>
    <li v-for="item in list">{{ item }}</li>
  </ul>
</template>

以上代码,最终渲染结果如下:

GIF2023-5-1121-30-04

# 5、自定义事件校验

TIP

  • 如果我们想要对触发的自定义事件的参数进行验证,可以将emits选项写成一个对象。
  • 对象中每个事件被赋值为一个函数,函数可接受的参数为this.$emit触发事件时传入的参数(除第一个参数之外的所有参数)
export default {
  emits: {
    // 事件没有校验
    updateEvent: null,

    // 校验delEvent事件
    // ...params 剩余参数,用来接受传过来的所有参数
    delEvent(...params) {
      // 通过返回值为 `true` 还是为 `false` 来判断事件是否合法
      // 如果返回值为fasle,则事件不合法,控制台会抛出警告
      // 如果返回值为true,则事件合法
      return true; // 或false
    },
  },
};

代码演示

App.vue 根组件

<script>
  import List from "./components/List.vue";
  export default {
    data() {
      return {
        list: [1, 2, 3, 4],
      };
    },
    components: {
      List,
    },
    methods: {
      del(index) {
        console.log("delEvent事件触发了!!");
        this.list.splice(index, 1); // 从index下标删除1个元素
      },
    },
  };
</script>
<template>
  <List
    :list="list"
    @add-event="(...arg) => list.push(...arg)"
    @del-event="del"
  />
</template>

List.vue 子组件

<script>
  export default {
    props: ["list"],
    emits: {
      // 不校验
      addEvent: null,
      // 校验delEvent事件是否合法,主要判断参数是否合法
      delEvent(index) {
        if (typeof index === "number") {
          console.log("事件合法的");
          return true;
        } else {
          console.warn("事件不合法的");
          return false;
        }
      },
    },
  };
</script>

<template>
  <button @click="$emit('addEvent', 5, 6, 7)">添加多条数据</button>
  <button @click="$emit('delEvent', '1')">删除下标为1的元素</button>
  <ul>
    <li v-for="item in list">{{ item }}</li>
  </ul>
</template>

以上代码渲染后结果如下:

GIF2023-5-1122-38-55

注:

因为@click="$emit('delEvent', '1')"代码中,传入的参数 1 是一个字符串,而真正希望传入的是一个数字。

所以在delEvent(index)函数中校验不成功,所以每次执行时,都会在控制台抛出警告,但事件最终还是触发了。

# 6、自定义事件注意事项

TIP

以下是我们在使用自定义事件时,需要注意的相关事项:

  • 自定义事件只能用来实现子组件向父组件传递数据或修改父组件中数据,不能实现兄弟组件间通信。因为事件需要在父组件中被监听,然后在子组件中触发。
  • 自定义事件本身也支持.once修饰符,添加了.once修饰符后,自定义事件只能被触发一次。
  • 在组件上监听 JS 原生的事件,可以直接透传到单根组件的根元素上,但多根组件就没办法接收
  • $emit选项中不要声明一个 JS 原生的事件名(例如:click),如果click出现在$emit选项中,则监听器只会监听组件触发的 click 事件而不会再响应原生的 click 事件。

代码演示

App.vue根组件

<script>
  import List from "./components/List.vue";
  export default {
    data() {
      return {
        num: 0,
      };
    },
    components: {
      List,
    },
  };
</script>
<template>
  <List :num="num" @add.once="num++" @click="num = num + 10" />
</template>

List.vue 子组件

<script>
  export default {
    props: ["num"],
    emits: ["add"],
  };
</script>

<template>
  <div class="box">
    <div>num的值{{ num }}</div>
    <button @click.stop="$emit('add')">num+1</button>
  </div>
</template>

<style>
  .box {
    width: 100px;
    height: 100px;
    background-color: skyblue;
    user-select: none;
  }
</style>

以上代码中最终渲染效果如下:

GIF2023-5-1123-13-28

以上效果原因分析

  • 因为 @add.once="num++"添加了.once修饰符,所以add事件只能被触发一次,即num+1按扭只有在第一次被点击事,num的值才会被 +1。
  • 因为@click为 JS 原生事件,同时List组件为单根组件,所以@click事件直接透传到了.box元素身上,相当于.box元素监听了click事件,所以当我们在.box元素上点击时,会调用click事件,num 的值为 +10。
  • 如果你在<List>组件的根元素.box后面再添加一个兄弟元素,你会发现click事件也不生效了。因为此时组件为多根组件,事件没有办法直接被透传。

修改相关代码,测试效果

如果你把在上面的emits选项修改为:emits["add","click"],再点击.box元素时,你会发现点击无效,num 属性不会+10。

因为此时"click"事件被当成自定义事件处理,原生的click事件将不会再触发了。你可以通过this.$emit('click')来触发自定义的 click 事件。

# 7、总结

TIP

本小节我们主要学习了以下内容

监听与触发自定义事件

  • 在父组件中可以通过v-on(缩写为@)来监听事件
<List @event-add="add" />
  • 在子组件的模板表达式中,可以直接使用$emit()方法触发自定义事件
<div @click="$emit('eventAdd')"></div>
  • 在子组件实例上通过this.$emit()方式触发自定义事件
export default {
  emits: ["eventAdd"],
  created() {
    this.$emit("eventAdd");
  },
};

自定义事件名格式

  • 在父组件中监听自定义事件时,自定义事件名要采用kebab-case写法,其目的是为了与原生的 HTML 标签属性名对齐。
  • 在子组件的emits选项和触发事件时,事件名要采用camelCase写法,因为它是一个合法的 JS 标识符,在模板中可用,而kebab-case写法是不合法的,在模板中无法使用。

自定义事件参数

  • 如果我们想在触发事件时,给父组件传递数据,我们可以把传递的数据做为$emit()方法的参数传入。如:$emit("event-name",1,2,3)(第一个参数为事件名,之后的每个参数为传递的数据)
  • 在父组件触发的事件处理回调函数的参数中,可以接受到传过来的数据,如:<MyComponent @event-name="(a,b,c)=>num=a+b=c"/> ,代码中的 a,b,c 对应上面事件触发时传递的 1,2,3。

emits选项与事件校验

  • emits选项作用: 用来显示声明子组件需要触发的事件,他支持数组与对象两种写法
  • emits的数组形式写法:如果只是简单的声明需要触发的事件,则可以采用数组写法,比较简洁
emits: ["eventAdd"];

emits的对象形式写法:

  • 如果需要对触发事件的参数作校验,则需要采用对象写法。
  • 对象中每个事件赋值为一个函数,函数返回值为true表示事件合法,返回值为false表示事件不合法,会在控制台抛出警告。
emits:{
    // arg用来接受传过来的参数
    eventAdd(...arg){
        return true
    }
}

自定义事件注意事项

  • 自定义事件主要用来实现子组件向父组件传递数据或通过子组件修改父组件中数据。
  • 自定义事件也可以添加.once修饰符,表示事件只能触发一次
  • 原生的 JS 事件,不要出现在emits选项中,如果出现在emits选项中,会被当成了自定义事件,需要通过$emits()方法来触发,同时原生 JS 事件不会被触发。
  • 在组件上监听 JS 原生的事件,可以直接透传到单根组件的根元素上,但多根组件就没办法接收

# 五、组件 v-model

TIP

v-model指令不仅可以用在输入元素上,用来实现双向数据绑定,也可以用在组件上实现双向绑定。

# 1、v-model 绑定输入元素

以下是v-model指令应用在输入元素上,实现双向数据绑定

<input v-model="searchText" />

以上v-model指令背后的实现原理:

  • 通过v-bind指令绑定将searchText变量的值绑定到输入框的 value 值
  • 通过监听@input事件,在表单中输入内容时触发input事件,把表单 value 的值绑定到searchText变量
<input :value="searchText" @input="searchText = $event.target.value" />

本质上v-model是一个语法糖,帮我们简化了以上代码的书写。

# 2、v-model 绑定组件

以下是v-model指令应用于一个自定义弹窗组件,用来实现双向绑定

<Popup v-model="show" />

以上v-model指令背后的实现原理:

  • 将变量show的值绑定到属性modelValue传递给到子组件<Popup>
  • 父组件监听update:modelValue事件,事件接受一个参数,该参数用来修改变量show的值。

注意:默认情况下属性名modelValue和事件update:modelValue是固定写法,不能修改

<Popup
  :modelValue="show"
  @update:modelValue="(newValue) => { show = newValue }"
/>

本质上v-model是一个语法糖,帮我们简化了以上代码的书写。

# 3、v-model 实战应用:点击弹窗

TIP

利用v-model指令实现如下弹窗效果,点击显示按扭时会弹出弹窗,点击弹窗关闭按扭时会关闭弹窗

GIF2023-6-2414-03-00

实现步骤

  • 创建弹窗子组件<Popup>,并实现弹窗的布局

image-20230624135802401

<template>
  <div class="popup">
    <!--确认关闭按扭-->
    <div class="close">确认</div>
  </div>
  <!-- 黑色半透明遮罩层-->
  <div class="mask"></div>
</template>

<style>
  .mask {
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    position: fixed;
    top: 0;
    left: 0;
  }

  .popup {
    width: 300px;
    height: 200px;
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    border: 1px solid #ddd;
    z-index: 2222;
    background-color: #fff;
    border-radius: 10px;
  }

  .popup .close {
    width: 100%;
    line-height: 30px;
    border-top: 1px solid #ddd;
    text-align: center;
    position: absolute;
    bottom: 0px;
    left: 0px;
    color: #1989fa;
  }
</style>
  • 在根组件<App>中创建一个按扭,点击按扭时就会显示弹窗。
  • <App>组件中定义一个布尔类型变量show用来控制弹窗的显示与隐藏,默认弹窗隐藏,则默认值为false
  • 点击显示按扭,显示弹窗,则show=true
<script>
  // 引入组件
  import Popup from "./components/Popup.vue";
  export default {
    data() {
      return {
        show: false, // 用来控制弹窗的显示与隐藏
      };
    },
    components: {
      Popup,
    },
  };
</script>

<template>
  <button @click="show = true">显示</button>
  <Popup />
</template>
  • 弹窗的显示与隐藏,本质是控制.popup.mask元素的显示与隐藏。所以父组件需要把show变量的值通过 props 方式传递给到子组件,并且还需要传递操作show变量值的方法。
  • 这里我们把show变量做为modelValue属性的值传递给到子组件,同时监听操作show变量的方法update:modelValue
<Popup
  :modelValue="show"
  @update:modelValue="(newValue) => { show = newValue }"
/>

<!--  或以下为简这与形式 v-model指令实现 -->
<Popup v-model="show" />

在子组件<Popup>中需要做以下 4 步处理

  • props选项中接收父组件传过来的modelValue属性的值
  • emits选项中接收父组件传过来的事件update:modelValue
  • modelValue做为v-show指令的值,用来控制.popup元素与.mask元素的显示与隐藏
  • 点击确认按扭时,在click事件中通过$emit方法来触发update:modelValue方法,实现关闭弹窗
<script>
  export default {
    // 1:接受父组件传递的属性
    props: ["modelValue"],
    // 2:接受父组件传递的事件
    emits: ["update:modelValue"],
  };
</script>

<template>
  <!-- 3: v-show 控制元素显示与隐藏-->
  <div class="popup" v-show="modelValue">
    <!-- 4:
		点击确定按扭,触发update:modelValue事件将modelValue的值更新为false,关闭弹窗
	   -->
    <div class="close" @click="$emit('update:modelValue', false)">确认</div>
  </div>
  <!-- 3: v-show 控制元素显示与隐藏-->
  <div class="mask" v-show="modelValue"></div>
</template>

经过以上步骤后,最终渲染出想要的目标效果,如下:

GIF2023-6-2414-03-00

最终完整版代码

App.vue文件内容

<script>
  import Popup from "./components/Popup.vue";
  export default {
    data() {
      return {
        show: false,
      };
    },
    components: {
      Popup,
    },
  };
</script>
<template>
  <button @click="show = true">显示</button>
  <!-- <Popup :modelValue="show" @update:modelValue="(newValue) => { show = newValue }" /> -->
  <Popup v-model="show" />
</template>

Popup.vue 文件内容

<script>
  export default {
    // 1:接受父组件传递的属性
    props: ["modelValue"],
    // 2:接受父组件传递的事件
    emits: ["update:modelValue"],
  };
</script>

<template>
  <!-- 3: v-show 控制元素显示与隐藏-->
  <div class="popup" v-show="modelValue">
    <!-- 4:
点击确定按扭,触发update:modelValue事件将modelValue的值更新为false,关闭弹窗
-->
    <div class="close" @click="$emit('update:modelValue', false)">确认</div>
  </div>
  <!-- 3: v-show 控制元素显示与隐藏-->
  <div class="mask" v-show="modelValue"></div>
</template>
<style>
  .mask {
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    position: fixed;
    top: 0;
    left: 0;
  }

  .popup {
    width: 300px;
    height: 200px;
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    border: 1px solid #ddd;
    z-index: 2222;
    background-color: #fff;
    border-radius: 10px;
  }

  .popup .close {
    width: 100%;
    line-height: 30px;
    border-top: 1px solid #ddd;
    text-align: center;
    position: absolute;
    bottom: 0px;
    left: 0px;
    color: #1989fa;
  }
</style>

# 4、组件 v-model 参数

TIP

默认情况下,v-model底层在组件上绑定的是modelValue属性,并且监听update:modelValue事件。

我们可以通过给v-model指定一个参数来更改绑定的属性名和事件名。

<Popup v-model:bool="show" />

上面v-model底层绑定的是bool属性,监听的是update:bool事件,如下代码

<Popup :bool="show" @update:bool="(newValue) => { show = newValue }" />

代码示例

在上面弹窗案例的基础上,为v-model指定bool参数,实现效果不变,对应代码修改如下:

App.vue中其它代码不变,模板代码修改如下

<template>
  <button @click="show = true">显示</button>
  <Popup :bool="show" @update:bool="(newValue) => { show = newValue }" />
  <Popup v-model:bool="show" />
</template>

Popup.vue文件中

  • 所有modelValue修改成 bool
  • 所有update:modelValue修改成update:bool
<script>
  export default {
    // 接受传递的属性与事件
    props: ["bool"],
    emits: ["update:bool"],
  };
</script>

<template>
  <div class="popup" v-show="bool">
    <div class="close" @click="$emit('update:bool', false)">确认</div>
  </div>
  <div class="mask" v-show="bool"></div>
</template>

# 5、多个 v-model 绑定

TIP

通过给v-model指定一个参数可以更改绑定的属性名和事件名,所以我们可以同时为一个组件绑定多个v-model

<Popup v-model:bool="show" v-model:title="title" />

以上代码等价于以下写法

<Popup
  :bool="show"
  @update:bool="(newValue) => { show = newValue }"
  :title="title"
  @update:title="(newValue) => { title = newValue }"
/>

代码示例

我们在之前弹窗的基础之上做了相关优化,在弹出的框中可以输入内容,将输入的内容显示到父组件中。

GIF2023-6-2415-03-00

完整版代码如下:

App.vue文件内容

<script>
  import Popup from "./components/Popup.vue";
  export default {
    data() {
      return {
        show: false, //
        title: "",
      };
    },
    components: {
      Popup,
    },
  };
</script>
<template>
  <button @click="show = true">显示</button>
  <div>弹窗输入内容:{{ title }}</div>
  <!-- 
	<Popup 
        :bool="show" 
        @update:bool="(newValue) => { show = newValue }" 
        :title="title"
        @update:title="(newValue) => { title = newValue }" 
	/> 
	-->

  <Popup v-model:bool="show" v-model:title="title" />
</template>

Popup.vue文件内容

<script>
  export default {
    // 接受传递的属性与事件
    props: ["bool", "title"],
    emits: ["update:bool", "update:title"],
  };
</script>

<template>
  <div class="popup" v-show="bool">
    <textarea
      :value="title"
      @input="$emit('update:title', $event.target.value)"
    ></textarea>
    <div class="close" @click="$emit('update:bool', false)">确认</div>
  </div>
  <div class="mask" v-show="bool"></div>
</template>
<style>
  /* 多行文本输入框 */
  textarea {
    display: block;
    height: 50%;
    width: 80%;
    margin: 30px auto;
    border: 1px solid #ddd;
  }

  .mask {
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    position: fixed;
    top: 0;
    left: 0;
  }

  .popup {
    width: 300px;
    height: 200px;
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    border: 1px solid #ddd;
    z-index: 2222;
    background-color: #fff;
    border-radius: 10px;
  }

  .popup .close {
    width: 100%;
    line-height: 30px;
    border-top: 1px solid #ddd;
    text-align: center;
    position: absolute;
    bottom: 0px;
    left: 0px;
    color: #1989fa;
    cursor: pointer;
  }
</style>

# 6、v-model 修饰符

TIP

v-model指令绑定表单输入元素时可以使用一些内置的修饰符,如:.trim.number.lazy

在自定义组件上的v-model不支持以上修饰符,但允许我们自定义修饰符。

只有修饰符

<MyComponent v-model.capitalize="msg" />
  • 上面组件v-model上添加的修饰符,在子组件的props选项中通过modelModifiers来接受
  • modelModifiers属性的值是一个{capitalize:true}对象
// MyComponent 组件
export default {
  props: {
    // v-model指令的修饰符
    modelModifiers: {
      //  默认值为一个空对象
      default: () => ({}),
    },
  },
  created() {
    console.log(this.modelModifiers); //  { capitalize: true }
  },
};

同时具有修饰符和参数

<MyComponent v-model:text.capitalize="msg" />
  • 上面组件v-modle上同时添加了参数与修饰符,在子组件中可以通过textModifiers属性在组件内访问到
  • textModifiers属性的值是一个{capitalize:true}对象
// MyComponent 组件
export default {
  props: {
    text: String,
    // v-model:text 指令的修饰符
    textModifiers: {
      default: () => ({}),
    },
  },
  created() {
    console.log(this.textModifiers);
  },
};

# 6.1、实战应用:输入内容首字母大写

TIP

如果<MyComponent>组件上的v-modle添加capitalize修饰符,则返回的内容首字母大小,否则正常输出

<MyComponent v-model.capitalize="msg" />

GIF2023-6-2416-43-26

代码示例

App.vue组件内容

<script>
  import MyComponent from "./components/MyComponent.vue";
  export default {
    data() {
      return {
        msg: "",
        text: "",
      };
    },
    components: {
      MyComponent,
    },
  };
</script>

<template>
  <div>输入框内容:{{ msg }}</div>
  <MyComponent v-model.capitalize="msg" />
</template>

MyComponent.vue组件内容

<script>
  export default {
    props: {
      modelValue: String,
      modelModifiers: {
        default: () => ({}),
      },
    },
    emits: ["update:modelValue"],
    methods: {
      onInput(e) {
        let value = e.target.value;
        if (this.modelModifiers.capitalize) {
          value = value.charAt(0).toUpperCase() + value.slice(1);
        }
        this.$emit("update:modelValue", value);
      },
    },
  };
</script>
<template>
  <input type="text" :value="modelValue" @input="onInput" />
</template>

# 7、总结

TIP

本小节需要重点掌握的以下相关内容

组件v-model

  • v-model指令绑定组件,用于实现组件的双向绑定
<Popup v-model="show" />
  • v-model指令本质是以下写法的语法糖,帮我们简化了代码的书写
<Popup
  :modelValue="show"
  @update:modelValue="(newValue) => { show = newValue }"
/>

组件v-model参数

  • 可以通过给组件上的v-model指定一个参数来更改绑定的属性名和事件名
<Popup v-model:bool="show" />
<!-- 以上代码相当于如下写法 -->
<Popup :bool="show" @update:bool="(newValue) => { show = newValue }" />

组件v-model修饰符

  • 组件v-model不支持.trim.number.lazy这些修饰符,但允许我们自定义修饰符。
<MyComponent v-model.capitalize="msg" />

以上v-model修饰符最终在子组件中以modelModifiers属性访问。modelModifiers属性的值是{capitalize:true}对象

  • 如果v-model指令同时包含参数与修饰,如下:
<MyComponent v-model:text.capitalize="msg" />

以上v-model修饰符最终在子组件中以textModifiers属性访问

# 六、透传属性(Attributes)

TIP

深入浅出透传属性是什么,单根组件透传属性的继承、注意事项,禁用透传属性自动继承,$attr 属性、注意事项,多根组件透传属性的继承,深层组件继承等。

# 1、透传属性定义

TIP

当我们给一个组件传递的Propv-on事件监听器,没有出现在该组件的propsemits选项中,那这些Propv-on事件监听被称为透传属性(Attributes)。

  • App.vue根组件中调用<List />子组件,并传递了两个Prop和两个v-on事件监听器
<List :a="1" :b="2" @click="fn" @add-event="add" />
  • 在子组件<List />中通过propsemits选项来显示声明接受的属性与需要触发的事件
export default {
  props: ["a"],
  emits: ["addEvent"],
};

注:

上面代码中的b属性和@click事件监听器就可称为透传属性(Attributes)。

因为:

  • 传递给子组件<List>的 Prop 有ab两个,传递的v-on事件监听器有click,add-event两个。
  • 但在子组件的props选项中只声明了a,在emits选项中只声明了addEvent,没有声明的b属性和click事件就可以称为透传属性(Attributes)。

# 2、单根组件透传属性的继承

TIP

当组件是一个单根组件时,透传的属性(Attributes)会被自动被添加到组件的根元素上。相当于根元素自动继承组件的属性。

温馨提示: 透传属性常用来传递classstyleid这样的属性。

因为很多时候,我们希望把传递给组件的classstyleid能直接应用到组件的根元素上,在某此特殊的需求下,我们可以利用这个特性修改子组件最终呈现的样式。

代码演示

App.vue 根组件

<script>
  import List from "./src/components/List.vue";
  export default {
    data() {
      return {
        isActive: true,
        color: "red",
      };
    },
    components: {
      List,
    },
  };
</script>

<template>
  <List :class="{ active: isActive }" :style="{ color: color }" id="box" />
</template>

<style>
  #box {
    width: 100px;
    height: 100px;
  }

  .active {
    background-color: skyblue;
  }
</style>

List.vue 子组件

<script>
  export default {};
</script>
<template>
  <div>List组件,是一个单根组件</div>
</template>

以上代码最终被渲染的结果如下

image-20230629175017841

# 3、透传属性继承的注意事项

TIP

如果组件是一个单根组件,透传属性的自动继承有以下两个需要注意的点:

  • classstyle 的合并
  • v-on 监听器继承

classstyle 的合并

  • 如果组件的根元素上的属性与透传属性同时,则会以透传属性为主,覆盖根元素自身的属性
  • 但如果组件的根元素上已有属性为classstyle,它会和透传属性classstyle的值进行合并。
<!--父组件中调用子组件List-->
<List :class="{ active: true }" :style="{ color: color }" title="透传属性" />
<!--以下为List子组件根元素,在List组件的props选项中没有声明 class  style  title-->
<div class="list" style="{font-size:'30px'}" title="组件自身属性">
  List组件,是一个单根组件
</div>

以上代码最终渲染效果如下

<div class="list active" style="color: red;" title="透传属性">
  List组件,是一个单根组件
</div>

v-on 监听器继承

  • 如果组件的根元素自身通过v-on绑定了一个与透传的v-on同名的事件监听器,则最终两个事件监听器都会被触发。
<!--父组件中调用子组件List-->
<List @click="fn" />
<!--以下为List子组件根元素,在List组件的emits选项中没有声明 click-->
<div @click="add">List组件,是一个单根组件</div>

以上代码,在点击div元素时,两个事件监听都会触发,即fnadd两个方法都会被调用

代码演示

App.vue 根组件

<script>
  import List from "./src/components/List.vue";
  export default {
    components: {
      List,
    },
    methods: {
      fn() {
        console.log("父组件中的fn方法");
      },
    },
  };
</script>

<template>
  <List @click="fn" />
</template>

List.vue 子组件

<script>
  export default {
    methods: {
      add() {
        console.log("List子组件中的add方法");
      },
    },
  };
</script>

<template>
  <div @click="add">List组件,是一个单根组件</div>
</template>

以上代码最终渲染效果如下,点击 div 元素,fnadd方法都被调用了。

GIF2023-5-1218-11-40

# 4、禁用透传属性自动继承

TIP

在某些场景下我们并不希望透传属性自动继承到单根组件的根元素上,而是想把透传的属性按需要添加到根节点以外的其他元素上,这时我们就需要禁用透传属性的自动继承行为。

你只需要在子组件中添加以下inheritAttrs: false配置,这样透传属性就不会自动继承到根元素上。

<script>
  export default {
    inheritAttrs: false, // 禁用透传属性的自动继承
  };
</script>
<template>
  <div>List组件,是一个单根组件</div>
</template>

那我们如何访问到透传进来的所有属性呢 ?这就需要用到$attr属性,接下来我们就来学习下$attr属性。

# 5、$attr 属性

TIP

如果我们想在一个组件中访问到所有透传的属性,可以通过以下两种方式来实现

  • 在子组件的模板表达式中,你可以直接使用$attrs对象访问到所有透传的属性。
<!--通过 $attrs.class 和 $attrs.style 访问透传属性class与style-->
<div :class="$attrs.class" :style="$attrs.style"></div>
  • 在 JS 中你可以通过子组件实例的this.$attrs方法来访问所有透传的属性。
export default {
  beforeCreate() {
    console.log("class", this.$attrs.class); // 访问透传属性 class
    console.log("style", this.$attrs.style); // 访问透传属性 style
  },
};

温馨提示:

不管子组件中是否添加inheritAttrs: false选项,$attrs属性都可以访问到所有的透传属性

代码演示

App.vue根组件

<script>
  import List from "./components/List.vue";
  export default {
    components: {
      List,
    },
  };
</script>

<template>
  <List :class="['active']" :style="{ color: 'red' }" />
</template>

<style>
  .active {
    width: 300px;
    height: 40px;
    line-height: 40px;
    background-color: skyblue;
  }
</style>

List.vue子组件

<script>
  export default {
    // 禁用透传属性自动继承到根元素,此选项加与不加,都不影响$attrs的值
    inheritAttrs: false,
    // 生命周期函数
    beforeCreate() {
      console.log("$attrs属性值", this.$attrs);
      console.log("class透传属性值", this.$attrs.class);
      console.log("style透传属性值", this.$attrs.style);
    },
  };
</script>
<template>
  <div>
    <div>$attrs属性的值:{{ $attrs }}</div>
    <div :class="$attrs.class" :style="$attrs.style">访问所透传属性</div>
    <!--以下写法,为以上写法的简写形式-->
    <!-- <div v-bind="$attrs">访问所透传属性</div> -->
  </div>
</template>

image-20230512200709432

# 5.1、$attrs 属性的注意事项

TIP

  • props有所不同,透传属性在 JS 中保留了它们原始的大小写,所以在父组件中通过kebab-case方式命名的属性,需要通过$attrs["kebab-case"]形式来访问。
  • 所有v-on绑定的事件监听器,在$attrs对象下被暴露为一个函数。如果父组件中添加@add-event='add'事件监听听,在$attrs对象上要通过$attrs.onAdd形式来访问。

代码演示

App.vue根组件

<script>
  import List from "./components/List.vue";
  export default {
    data() {
      return {
        n: 0,
      };
    },
    components: {
      List,
    },
  };
</script>
<template>
  <div>n的值:{{ n }}</div>
  <List data-title="新闻标题" @click="n++" />
</template>

List.vue 子组件

<script>
  export default {
    inheritAttrs: false,
    beforeCreate() {
      console.log(this.$attrs);
    },
  };
</script>
<template>
  <div>
    <button @click="$attrs.onClick">n++</button>
    <div>$attrs---{{ $attrs }}</div>
    <div>正确访问方式: {{ $attrs['data-title'] }}</div>
    <div>错误访问方式: {{ $attrs['dataTitle'] }}</div>
  </div>
</template>

以上代码最终渲染效果如下:

GIF2023-5-1220-56-57

# 6、多根组件透传属性的继承

TIP

和单根组件有所不同,多根组件的透传属性不会自动继承到组件的根元素上,所以我们需要显示绑定$attrs

如果$attrs没有显示绑定,会在控制台将会抛出警告,因为根元素有多个,Vue 不知道要将透传属性放到那个元素上。

<List data-title="新闻标题" :data-num="1" />

如果<List>组件中的模板内容如下,则会在控制台抛出错误,因为没有显示绑定$attrs

<template>
  <h3>.....</h3>
  <div>.....</div>
</template>

如果<List>组件中的模板内容如下,则不会在控制台抛出错误提示,因为显示绑定了$attrs

<template>
  <h3>.....</h3>
  <div v-bind="$attrs">.....</div>
  <!--以上写法为以下写法的简写-->
  <!-- 
    <div :title="$attrs['data-title']" :num="$attrs['data-num']">
    .....
    </div> 
	-->
</template>

# 7、深层组件继承

有些情况下一个组件会在根节点上渲染另一个组件,如下:

<!--以为下为List组件,在List组件中,只是渲染另一个组件 <Item/>-->
<template>
  <List />
</template>

此时,<List>组件接收的透传属性会直接继承传给<Item/>组件

代码演示

App.vue根组件

<script>
  import List from "./components/List.vue";
  export default {
    data() {
      return {
        n: 0,
      };
    },
    components: {
      List,
    },
  };
</script>
<template>
  n的值: {{ n }}
  <List class="box" :num="n" @click="n++" />
</template>

List.vue子组件

<script>
  import Item from "./Item.vue";
  export default {
    props: ["num"],
    components: {
      Item,
    },
  };
</script>

<template>
  <!--能传递的透传属性有 class 与 click ,因为num在props中声明了-->
  <Item />
</template>

Item.vue 子组件

<script>
  export default {};
</script>
<template>
  <div>Item组件</div>
</template>

以上代码最终渲染效果如下,点击文字Item组件会触发 click 事件,更新 n 的值

GIF2023-5-1221-49-46

# 8、总结

TIP

透传属性定义

  • 当我们给一个组件传递的Propv-on事件监听器,没有出现在该组件的propsemits选项中,那这些Propv-on事件监听被称为透传属性(Attributes)。
  • 透传属性常用来传递classstyleid这样的属性。

透传属性的继承

  • 单组组件

在单根组件中,透传属性会自动传递给到组件的根元素。透传属性与根元素属性同名时,以透传属性为主,但以下两种情况除外:

  • 如果透传属性中包含classstyle属性,则会与根元素上的classstyle属性合并

  • 如果透传属性中v-on绑定的事件监听器与根元素自身的事件监听器同名,则两者都会被触发

  • 多根组件

    在多根组件中,透传属性不会自动传递到组件的根元素,因为根节点较多,Vue 不知道把透传属性传给谁。所以我们需要在模板中显示绑定$attrs,如果不绑定,控制台会抛出错误。

$attrs 属性

访问透传属性的两种方工作

  • 我们可以在子组件模板中,通过$attrs访问到所有的透传属性

  • 我们可以在 JS 中,通过组件实例this.$attrs属性访问到所有的透传属性

$attrs属性的注意事项

  • 在父组件中传递的属性名为kebab-case形式时,则需要通过$attrs["kebab-case"]形式来访问,这一点与 props 不同
  • v-on绑定的事件监听器,如@click,在$attrs对象下暴露为一个函数,同时需要通过$attrs.onClick形式来访问

禁用透传属性的自动继承

  • 在单组组件中,如果不想让透传属性自动传递到组件的根元素,可在组件选项中设置inheritAttrs: false

深层组件继承

如果单组组件的根元素为另一个组件,那该组件的透传属性会自动透传给到根元素组件。

上次更新时间: 6/30/2023, 11:02:22 PM

大厂最新技术学习分享群

大厂最新技术学习分享群

微信扫一扫进群,获取资料

X