浅谈Vue组件传递数据与通信

1,878 阅读4分钟

对于使用Vue的新手来说,组件之间的数据传递一直是一个比较头疼的问题,在实际开发中我也踩了些坑,简单的做一个总结:

父子组件之间的数据传递

  1. 父组件向子组件传递数据
    由于组件实例的作用域是孤立(scope)的,所以组件之间无法相互访问到对方的数据,所以这里我们需要在子组件中使用 props 选项去接受来自父组件传递进来的动态数据,并且在父组件的标签上绑定v-bind该数据,这样一来,我们就把父组件中的数据传递给了子组件中。
Vue.component('child', {
// declare the props
  props: ['message'],
// just like data, the prop can be used inside templates
// and is also made available in the vm as this.message
  template: '<span>{{ message }}</span>'
})

<child :message="data"></child>
  1. props数据单向传递
prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是不会反过来。这是为了防止子组件无意修改了父组件的状态——这会让应用的数据流难以理解。
另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop。如果你这么做了,Vue 会在控制台给出警告。

所以,当我们的控制台出现这样的报错时,那就说明你的prop被你改变了:

[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "message"

警告告诉我们需要使用data或者computed选项将prop所接受的数据处理一下即可,所以我们有如下两种方式:

  • 定义一个局部变量,并用 prop 的值初始化它:
props: ['initialCounter'],
data: function () {
return { counter: this.initialCounter }
}
  • 定义一个计算属性,处理 prop 的值并返回。
props: ['size'],
computed: {
  normalizedSize: function () {
return this.size.trim().toLowerCase()
  }
}
  1. 子组件向父组件传递事件
    上面提到了prop是单向数据流的,所以prop接受的数据是无法双向绑定的,那么我们如何才能改变父组件的数据呢?这里我们就用到了vue的自定义事件。
    子组件中我们可以通过$emit(eventName)来触发事件。
    父组件中我们可以通过$on(eventName)来监听事件,如果对事件发布订阅模式比较熟悉的同学应该会比较容易理解。
  • v-on监听事件
<!-- html -->
<div id="app1">
<input type="text" v-model="message">
//v-on 监听子组件 传递的input事件
<child1 :value="message" v-on:input="setVal"></child1>
</div>

//javascript
Vue.component('child1', {
  props: ['value'],
//定义一个计算属性,处理 prop 的值并返回
  computed: {
    newVal: {
      get() { //用于获取计算属性
return this.value
      },
      set(v) { //用于设置计算属性
return v
      }
    }
  },
  template: '<input type="text" :value="newVal" @input="onInput">',
  methods: {
    onInput: function(e) {
this.newVal = e.target.value
this.$emit('input', e.target.value)
    }
  }
})
var app1 = new Vue({
  el: '#app1',
  data: function() {
return {
      message: ''
    }
  },
  methods: {
    setVal: function(v){
this.message = v
    }
  }
})

这样一来,我们便实现了父子组件数据的双向绑定。不过看起来比较麻烦,下面再介绍几种语法糖,让父子组件通信更简洁

  • v-model
    我们都知道,v-model是一个语法糖,当我们在input里面写了v-model="xxx"时相当于写了 <input :value="xxx" @input="xxx = $event.target.value">,所以我们可以利用这个特性简化一下:
<div id="app">
<input type="text" v-model="message">
<child v-model="message"></child>
</div>

Vue.component('child', {
  props: ['value'],
  template: '<input type="text" :value="value" @input="onInput">',
  methods: {
    onInput: function(e) {
this.$emit('input', e.target.value)
    }
  }
})
var app = new Vue({
    el: '#app',
    data: function() {
return {
        message: ''
      }
    }
  })
  • .sync修饰符
    文档中这样说道:
在一些情况下,我们可能会需要对一个 prop 进行『双向绑定』。事实上,这正是 Vue 1.x 中的 .sync修饰符所提供的功能。当一个子组件改变了一个 prop 的值时,这个变化也会同步到父组件中所绑定的值。这很方便,但也会导致问题,因为它破坏了『单向数据流』的假设。由于子组件改变 prop 的代码和普通的状态改动代码毫无区别,当光看子组件的代码时,你完全不知道它何时悄悄地改变了父组件的状态。这在 debug 复杂结构的应用时会带来很高的维护成本。

.sync 修饰符目的就是为了让父子组件的数据双向绑定更加简单。例

<div id="app2">
<input type="text" v-model="message">
<child2 :message.sync="message"></child2>
</div>

Vue.component('child2', {
  props: ['message'],
  template: '<input type="text" :value="message" @input="onInput">',
  methods: {
    onInput: function(e) {
this.$emit('update:message', e.target.value)
    }
  }
})
var app2 = new Vue({
  el: '#app2',
  data: {
    message: ''
  }
})

注意 子组件emit事件的时候,需要加上update

  • 利用引用类型进行双向绑定
    因为JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。代码更加简单了。
<div id="app3">
<input type="text" v-model="message.val">
<child3 :message="message"></child3>
</div>

Vue.component('child3', {
  props: ['message'],
  template: '<input type="text" v-model="message.val">'
})
var app3 = new Vue({
  el: '#app3',
  data: {
    message: {
      val: ''
    }
  }
})

这样写是更简单,但同时你的数据更不可控。

非父子组件之间的数据传递

对于非父子组件通信,在简单的场景下,可以使用一个空的 Vue 实例作为中央事件总线:

var bus = new Vue()

// 触发组件 A 中的事件
bus.$emit('id-selected', 1)

// 在组件 B 创建的钩子中监听事件
bus.$on('id-selected', function (id) {
// ...
})

这个空的实例就好像一辆汽车,将两个组件紧紧的联系起来,所以又称 eventBus。下面在具体的例子中去使用它:

<div id="app4">
<child4></child4>
<child5></child5>
</div>
<script>
//创建空的实例
var Bus = new Vue()
Vue.component('child4', {
  data: function() {
return {
      message: ''
    }
  },
  template: '<input type="text" :value="message" @input="onInput">',
  methods: {
    onInput(e) {
      Bus.$emit('send', e.target.value)
    }
  }
})
Vue.component('child5', {
  data() {
return {
        message: ''
      }
    },
    created: function() {
      Bus.$on('send', function(value) {
this.message = value
      }.bind(this)) //若不bind this,回调中的this指向Bus
    },
    template: '<span>{{message}}</span>'
})
var app4 = new Vue({
  el: '#app4'
})
</script>

当然,eventBus也可以在父子组件的通信中使用,不过略为繁琐,这里不做介绍。如果非父子组件通信比较复杂时,我们可以通过Vuex来完美解决。

PS:所有的示例可以在 js.jirengu.com/dosef/1/ 看到
完~

更多交流请加饥人谷9群:542597149。暗号:知乎

或者你也可以在微信搜索「饥人谷」订阅我们。