Vue 组件(二):父子组件通信

子组件是不能直接访问父组件中的数据的,但有时候父子组件之间需要进行数据交互,这就涉及到了父子组件通信的问题。简单来说,父组件向子组件通信是通过 props 进行的,而子组件向父组件通信则是通过自定义事件进行的。

我们用一个简易的 todolist 案例来理解这两个过程。

1.todolist 案例

1.1 父传子

假定我们现在有一个需求:在输入框中输入待办事项,点击添加按钮可以将事项展现在页面上。如下图所示:

分析:页面分为两个部分,一部分是操作区,一部分是展示区。展示区可以用 li,那么这些 li 就可以看作是可复用的子组件,而其它部分则看作是父组件,我们在父组件中操作,结果却是在子组件中显示的,所以这里是父组件向子组件通信的问题。

首先将根实例作为父组件,然后注册一个子组件,写好大概的结构:

<!--父组件模板-->
<div id="app">
  <input type="text" v-model="newvalue">
  <button @click="addItem">点击添加</button>
  <cpn></cpn>
</div>

<!--子组件模板-->
<template id="cpn">
  <div>
    <ul></ul>
  </div>
</template>
const cpn = {
  template:"#cpn"
}
const app  = new Vue({
  el:'#app',
  data:{
    newvalue:'',
    list:[]
  },
  methods:{
    addItem(){
      this.list.push(this.newvalue);
      this.newvalue = ''
    }
  },
  components:{
    cpn
  }
})

表单元素需要双向数据绑定,所以我们这里使用 v-model="newvalue"newvalue 初始化的时候是空字符串,后面就代表我们输入的待办事项,监听按钮的点击事件并把它 push 到空数组中,之后为了用户操作方便(不需要手动删除输入框内容),我们再把 newvalue 置空。这时候,父组件的操作已经完成了,接下来要把数据传递给子组件并显示出来。

list 是要传递的数据,首先把它交付给自定义属性 list2,对于子组件,它需要通过 props (可以是数组或者对象)去接收。之后,我们在子组件模板中进行列表的遍历,遍历的对象就是 list2 数组。

代码如下:

<!--父组件模板-->
<div id="app">
  <input type="text" v-model="newvalue">
  <button @click="addItem">点击添加</button>
  <cpn v-bind:list2="list"></cpn>
</div>

<!--子组件模板-->
<template id="cpn">
  <div>
    <ul>
      <li v-for="(item,index) in list2">{{item}}</li>
    </ul>
  </div>
</template>
const cpn = {
  template:"#cpn",
  props:["list2"]
}
const app  = new Vue({
  el:'#app',
  data:{
    newvalue:'',
    list:[]
  },
  methods:{
    addItem(){
      this.list.push(this.newvalue);
      this.newvalue = ''
    }
  },
  components:{
    cpn
  }
})

每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

1.2 子传父

作为一个 todolist,除了添加之外应该还可以删除,所以接下来的需求是点击待办事项可以进行删除。如下图所示:

分析:因为这里子组件只负责点击操作,实际的删除需要父组件自己去操作数据(类似于子组件打个电话告诉父组件该删除哪个东西了),所以这里涉及到了子组件向父组件通信的问题。

这里首先还是监听待办事项的点击事件,点击后调用函数,之后执行函数中的 this.$emit('eventName',args),作用是由实例向外触发一个自定义事件(参数可选),之后父组件再监听这个自定义事件,一旦监听到事件就调用父组件(即根实例)下挂载的方法,来删除待办事项。

代码如下:

<div id="app">
  <input type="text" v-model="newvalue">
  <button @click="addItem">点击添加</button>
  <!-- 3.父组件监听到自定义事件 receive 后,调用 deleteItem -->
  <cpn v-bind:list2="list" @receive="deleteItem"></cpn>
</div>
<template id="cpn">
  <div>
    <ul>
      <!--1.监听点击事件-->
      <li v-for="(item,index) in list2" @click="remove(index)">
        {{item}}
      </li>
    </ul>
  </div>
</template>
const cpn = {
  template:"#cpn",
  props:["list2"],
  methods:{
    remove(index){
      this.$emit("receive",index)  // 2.向外触发自定义事件 receive
    }
  }
}
const app  = new Vue({
  el:'#app',
  data:{
    newvalue:'',
    list:[]
  },
  methods:{
    addItem(){
      this.list.push(this.newvalue);
      this.newvalue = ''
    },
    // 4.执行 deleteItem 删除数组元素
    deleteItem(index){
      this.list.splice(index,1)
    }
  },
  components:{
    cpn
  }
})

2. props

前面使用的 props 是数组,实际开发中用的更多的其实是对象。作为对象的 props 可以配置高级选项,如类型检测、自定义校验和设置默认值等。

假定上面的子组件还接受了其它数据:

const cpn = {
  template:"#cpn",
  methods:{
    .......
  },
  props:{
    propA:Array, // 接受的 propA 类型必须是数组。也可以指定自定义类型
    propB:{String,Number}, // propB 必须是字符串或者数字
    propC:{
      type:String,
      required:true    // 必须接受 propC,否则报错
    },
    propD:{
      type:String,
      default:"demo"   // 没有接受到 propD 时使用这个默认值
    },
    propD:{
      type:Object,
      default:function(){   // 数组或对象指定默认值时必须是一个函数
        return {message:"Hello"}
      }
    },
    propE:{
      validator(value){
        // 这个值必须匹配下列字符串中的一个
        return {'aaa','bbb'}.indexOf(value) !== -1
      }
    }
  }
}

另外,还要注意一下 prop 的命名。引用官方文档的一段话:

HTML 中的特性名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名:

Vue.component('blog-post', {
  props: ['postTitle'],  // 在 JavaScript 中是 camelCase 的
  template: '<h3>{{ postTitle }}</h3>'
})
<!-- 在 HTML 中是 kebab-case 的 -->
<blog-post post-title="hello!"></blog-post>

重申一次,如果你使用字符串模板,那么这个限制就不存在了。

3. 在组件中使用 v-model

3.1 一般情况

首先要弄清楚一件事:v-model 其实是语法糖,本质上是 v-bind:valuev-on:input 的结合,也就是说:

<input type="text" v-model="test">
// 等同于
<input type="text" :value="test" @input="test=$event.target.value">

根据这点,v-model 除了实现双向数据绑定之外,也可以用在组件中,更方便地书写父子组件通信。

假如我们现在想要实现:点击父组件按钮,数据 +1;反过来,点击子组件按钮,数据 -1。 代码如下:

<div id="app">
  <cpn v-model="total"></cpn>
  <button @click="increase">+</button>
</div>
<template id="cpn">
  <div>
    <h2>{{value}}</h2>
    <button @click="decrease">-</button>
  </div>
</template>
const cpn = {
  template:"#cpn",
  props:["value"],
  methods: {
    decrease(){
      this.$emit("input", this.value-1)
    }
  }
}
const app  = new Vue({
  el:'#app',
  data:{
    total:0
  },
  methods: {
    increase() {
        this.total++
    }
  },
  components:{
    cpn
  }
})

前面我们说过,v-model 是语法糖,因此:

<cpn v-model="total"></cpn>
<!--相当于下面的-->
<cpn :value="total" @input="total=arguments[0]"></cpn>

分析: 我们把父组件数据直接绑定到 value 上(而不是自定义属性),之后子组件用 prop 接受。点击 -1 按钮后向外触发 input 事件(而不是自定义事件),同时传 -1 后的值,父组件监听到事件后调用函数完成赋值。 这里 arguments[0] 就是回调函数第一个参数,也就是前面 -1 后的值。

3.2 model 选项自定义

不过,组件的 v-model 默认会利用名为 valueprop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能不需要 value,需要的是 checked;不需要 oninput 事件,需要的是 onchange 事件。所以 Vue 提供了 model 选项让我们实现自定义:

假定父组件有一个数据 lovingVue 用于表示子组件的多选框是否勾选,那么可以这么写:

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})

其中:

<base-checkbox v-model="lovingVue"></base-checkbox>
<!--相当于下面的-->
<base-checkbox :checked="lovingVue" @change="lovingVue=arguments[0]">
</base-checkbox>

这里的 lovingVue 的值将会传入这个名为 checkedprop。同时当 <base-checkbox> 触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 将会被更新。

注意你仍然需要在组件的 props 选项里声明 checked 这个 prop

Tip: 上面这样写之后,看起来很像是子组件可以直接修改父组件数据,其实不是的,本质上还是 prop + $emit 的正常通信方式在,只是书写更加方便了而已。

要记住 Vue 是单向数据流的。所以上面这个例子,如果 this.value-1 写成 this.value-- ,实际上会报错,因为这样写是试图通过子组件直接修改 prop 的值,这是不允许的。详情可以看 Vue 组件(三):关于单向数据流的简单理解

4. 总结

到这里的话,父子组件之间的通信就已经结束了。使用 Vue 的时候应该避免直接去操作 dom,而是通过数据的改变让页面自动变化。

  • 父组件向子组件传值:在父组件中通过 v-on 绑定自定义属性以存储父组件数据,然后子组件通过 props 接收,这样就可以拿到父组件中的数据;
  • 子组件向父组件通信:子组件监听到事件后,通过 $emit 向外触发自定义事件,父组件监听到该事件后操作数据。

另外还要注意 v-model` 在组件中的使用。

参考: Vue.js中的组件以及父子组件间通信传值 Vue 进阶教程之:详解 v-model Vue.js - 自定义事件

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏sofu456

asp.net

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

5920
来自专栏前端达人

「vue基础」Vue Router 使用指南上篇(文末送漂亮的 Vue 站点源码)

大家好,今天的内容,我将和大家一起聊聊 Vue 路由相关的知识,如果你以前做过服务端相关的开发,那你一定会对程序的URL结构有所了解,我没记错的话也是路由映射的...

8040
来自专栏call_me_R

【译】Vue.set实际上是什么?

谈到Vue.set就要说响应式原理,所以得为你自己准备下这方面的理论知识。然而,一如即往,这并不难或者枯燥。准备点鳄梨和薯条,制作些鳄梨酱,然后我们再进入话题。

6220
来自专栏一番码客

vue.js学习(01)

之前尝试用electron做一些项目的时候,因为完全没有前端开发经验,做起来还是有点找不到头绪,思路非常乱,想到什么功能便搜什么,这样导致没有全局观,不找到整体...

7810
来自专栏青笔原创

从零开始构建 vue3

2019年10月5日凌晨,Vue 的作者尤雨溪公布了 Vue3 的源代码。当然,它暂时还不是完整的 Vue3,而是 pre-alpha 版,只完成了一些核心功能...

10810
来自专栏码客

Vue基本语法

当vue需要加载数据多或者网络慢时,加载数据时候会先出现vue模板(例如item.name),用户体验特别不好

10720
来自专栏码客

前端面试题

· 3.优化CSS(压缩合并css,如margin-top,margin-left…)

6510
来自专栏京程一灯

React VS Vue:2020年应该选哪个?[每日前端夜话0xD3]

Javascript 框架以及 HTML 和 CSS 已成为每个现代软件项目前端开发的重要组成部分。2020 年将会是为你的 Web 项目选择正确的 javas...

17510
来自专栏码客

Vue路由以及SEO配置

hash模式对应的路由是类似于这个样子的 http://localhost:8080/#/about

9520
来自专栏码客

Vue Cli3使用

Vue CLI 3 和旧版使用了相同的 vue 命令,所以 Vue CLI 2 (vue-cli) 被覆盖了。如果你仍然需要使用旧版本的 vue init 功能...

6830

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励