# 组件通信

通常一个单页应用会以一棵嵌套的组件树的形式来组织:

component-3

  • 页面首先分成了顶部导航、左侧内容区、右侧边栏 3 部分

  • 左侧内容区又分为上下 2 个组件

  • 右侧边栏中又包含了 3 个子组件

各个组件之间以嵌套的关系组合在一起,那么这个时候不可避免的会有组件间通信的需求。

# 1. 父组件向子组件传递数据

# props

标准的 HTML 元素有属性,例如 <input type="" id="" name="">,组件也可以有。

子组件的属性,就是使用它的元素(即,其父元素)向它传递数据的途径

  1. 父组件使用子组件时,对子元素的『自定义属性』赋值。子组件的自定义属性名任意,属性值为要传递的数据

  2. 子组件通过 props 接收父组件传递来的值。

在下例中,父组件使用了子组件,并通过子组件的自定义属性 title,向子组件传递了数据。

<div id="app">
  <h1>打个招呼:</h1>
  <!-- 使用子组件,同时传递 title 属性 -->
  <introduce title="Hello,World"/>
</div>

<script type="text/javascript">
  Vue.component("introduce", {
    // 直接使用 props 接收到的属性来渲染页面
    template:'<h1>{{title}}</h1>',
    props:['title'] // 通过 props 来接收一个父组件传递的属性
  });

  var app = new Vue({
    el:"#app"
  });
</script>

# props 验证

子元素在自定义属性时,可以提出更多的限定条件,要求父元素在通过这个自定义属性向它传递数据时,必须传递符合某些限定条件的值。

例如,我们定义一个子组件,并接受复杂数据:

const myList = {

  // 这个子组件可以对 items 进行迭代,并输出到页面。
  template: '\
    <ul>\
      <li v-for="item in items" v-bind:key="item.id">{{item.id}} : {{item.name}}</li>\
    </ul>\
  ',
  props: {
    items: {          // 属性名为 items
      type: Array,    // 属性类型为数组
      default: [],    // 默认值为 []
      required: true  // 该属性为必须
    },
    { ... },  // 其它自定义属性
    { ... }
  }
};

当 prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告。

我们在父组件中使用它:

<div id="app">
    <h2>课程有:</h2>
    <!-- 使用子组件的同时,传递属性,这里使用了v-bind,指向了父组件自己的属性 lessons -->
    <my-list v-bind:items="lessons"/>
</div>
var app = new Vue({
  el: "#app",
  components: {
    myList // 当 key 和 value 一样时,可以只写一个
  },
  data: {
    lessons: [
      { id:1, name: 'java' },
      { id:2, name: '测试' },
      { id:3, name: '前端' },
    ]
  }
})

type 类型,可以有:

# 类型值
1 String
2 Number
3 Boolean
4 Array
5 Object
6 Date
7 Function
8 Symbol

# 动态静态传递

  • 给 prop 传入一个静态的值:

    <introduce title="Hello,World"/>
    
  • prop 传入一个动态的值:

    通过 v-bind 从父组件的 data 中获取值来为子元素的 title 属性赋值

    <introduce v-bind:title="title"/>
    

另外,有可能这两种传值的写法,看起来会很像,例如:

<son likes="42"></son>
<son v-bind:likes="42"></son>
  • likes="42" 是静态传值

    在静态传值中,父元素传递给子元素的属性的值的类型都是『字符串』。这里的 42 实际上是字符串 "42",即,子元素的 likes 属性的类型是 String 。

  • v-bind:likes="42" 是动态传值

    如果是传常量的话,上例并无必要,也不常见,但是语法上是完全正确的。

    在这里,传递给子元素的 42 是一个 JavaScript 表达式,(42)是一个 Number 而非 String 。即,子元素的 likes 属性的类型是 Number 。

# 2. 子组件向父组件传递数据

一个典型的子组件向父组件传递数据的场景是这样的:子组件中有一个按钮,当按钮被点击时,父组件的某个元素的值要发生变化。

想当然的代码可以写成如下形式:

<div id="app">
  <h2>num: {{num}}</h2>
  <!-- 使用子组件的时候,传递 num 到子组件中 -->
  <counter v-bind:num="num"></counter>
</div>

<script type="text/javascript">
  Vue.component("counter", { // 子组件,定义了两个按钮,点击数字 num 会加或减
    template:'\
      <div>\
        <button @click="num++">加</button>  \
        <button @click="num--">减</button>  \
      </div>',
    props:['num'] // count是从父组件获取的。
  })
  var app = new Vue({
    el:"#app",
    data:{
      num:0
    }
  })
</script>
  • 子组件接收父组件的 num 属性

  • 子组件定义点击按钮,点击后对 num 进行加或减操作

我们尝试运行,好像没问题,点击按钮试试:

1525859093172

子组件接收到父组件属性后,『默认是不允许修改的』。怎么办?

# 自定义事件

子组件向父组件传递数据要通过『在父组件上自定义事件』来实现。

核心关键点有 2 处:

  1. 子组件自定义事件,父组件定义该事件的处理函数。即『提前』做好准备:当发生子组件身上发生 xxx 事件时就执行父组件的 xxx 方法。该方法中去修改父组件的)某个、某些数据。

  2. 子组件通过 $emit() 函数发出『通知』:我(子组件)身上发生了 xxx 事件。


最终要变动的是父元素的属性,那么加和减的操作一定是放在父组件:

var app = new Vue({
  el: "#app",
  data: {
    num:0
  },
  methods: { // 父组件中定义操作 num 的方法
    increment() { this.num++; },
    decrement() { this.num--; }
  }
})

但是,点击按钮是在子组件中,那就是说需要子组件来调用父组件的函数,怎么做?

我们可以『通过 v-on 指令将父组件的函数绑定到子组件的自定义事件』上:

<div id="app">
  <h2>num: {{num}}</h2>
  <counter v-bind:count="num" v-on:inc="increment" v-on:dec="decrement"></counter>
</div>
  • 当子组件<counter>上发生 inc 事件时,调用、触发(父组件的) increment() 方法。

  • 当子组件<counter>上发生 dec 事件时,调用、触发(父组件的) decrement() 方法。

简单来说就是,子组件通过 $emit() 上报给父组件自己身上发生了 xxx 事件,而父组件因为提前就已经做好了绑定,当子组件上发生 xxx 事件时,就执行父组件的某某方法,而父组件的某某方法去修改父组件的某某数据。从而实现子组件『间接』修改父组件的数据。


在子组件中定义函数,函数的具体实现调用父组件的实现,并在子组件中调用这些函数。当子组件中按钮被点击时,调用绑定的函数:

Vue.component("counter", {
  template:'\
    <div>\
      <button v-on:click="plus">加</button>  \
      <button v-on:click="reduce">减</button>  \
    </div>',
  props:['count'],
  methods: {
    plus() {
      this.$emit("inc");
    },
    reduce(){
      this.$emit("dec");
    }
  }
});

逻辑上,子组件中做了一个『包装』:将 click 事件包装成了自定义的 incdec 事件。

效果:

# 传递数据

上面的例子中,子组件只需要向父组件『上报』一个提前预定好的自定义事件即可,但是有些情况下,子组件还需要向父组件传递数据。

例如,子组件中是一个 input 输入框,要求其中的输入的数据要传递给父组件,在父组件中展示。

这种情况下,子组件在使用 $emit 方法向父组件上报事件时,需要/可以多传入一个参数,将输入框中的当前值传递给父组件。

  • 子组件核心代码

    <input type="text" v-model="msg">
    
    data() {
      return {
        msg: ''
      }
    },
    watch: {
      msg(newVal, oldVal) {
        this.$emit('xxx', newVal);
      }
    }
    
  • 父组件核心代码

    父组件的 xxx 事件处理函数,就需要多一个参数来接收子组件传递的数据。

    <Son @xxx="msgChange"></Son>
    
    methods: {
      msgChange(msg) {
        this.msg = msg;
      }
    },
    

『The End』