【Vue】Vue中的父子组件通讯以及使用sync同步父子组件数据

目录

 

正文

 

前言: 之前写过一篇文章《在不同场景下 Vue 组件间的数据交流》,但现在来看,其中关于“父子组件通信”的介绍仍有诸多缺漏或者不当之处, 正好这几天学习了关于用 sync 修饰符做父子组件数据双向绑定的的用法, 于是决定写一篇文章, 再次总结下“Vue 中的父子组件通信”。
前面提示:本文文字略少,代码略多


父子组件通讯,可分为两种情况:


1. 父组件子组件中传递数据
2. 子组件父组件中传递数据
一般情况下, 1 中情况可通过 props 解决数据传递的问题, 这里就不多赘述了。

 

回到顶部

子组件向父组件中传递数据

 

主要谈谈 2 中情景的实现,有三种方式:


一. 通过 props,父组件向子组件中传递数据和改变数据的函数,通过在子组件中调用父组件传过来的函数,达到更新父组件数据(向父组件传递数据)的作用(子组件中需要有相应的响应事件)
. 通过在子组件中触发一个 自定义事件(vm.$emit), 将数据作为 vm.$emit 方法的参数,回传给父组件用 v-on:[自定义事件] 监听的函数
.通过 ref 对子组件做标记,父组件可以通过 vm.$refs.[子组件的 ref].[子组件的属性 / 方法] 这种方式直接取得子组件的数据

 

下面我将一 一展示

一. 通过 props 从父向子组件传递函数, 调用函数改变父组件数据

这里就不做代码展示了
一来是因为相对比较简单
二来是因为这种方式显然不是 Vue 中的最佳实践(在 react 中倒比较常见)
想要看代码的话可以看这里:《【Vue】浅谈 Vue 不同场景下组件间的数据交流》http://www.cnblogs.com/penghuwan/p/7286912.html#_label1 (在兄弟组件的数据交流那一节)

 

二.  通过自定义事件从子组件向父组件中传递数据


我们可以在子组件中通过 $emit(event, [... 参数]) 触发一个自定义的事件,这样,父组件可以在使用子组件的地方直接用 v-on 来监听子组件触发的事件, 并且可以在监听函数中依次取得所有从子组件传来的参数
例如:
在子组件中某个部分写入:

this.emit('eventYouDefined', arg);


然后你就可以在父组件的子组件模板里监听:
// 这里是父组件的 Template:

<Son  v-on: eventYouDefined = "functionYours" />


下面是一个实例


父组件

<template>
  <div id="father">
    <div>
       我是父组件, 我接受到了:
      {{ text || '暂无数据'  }}
      <son v-on:sendData='getSonText'></son>
    </div>
  </div>
</template>

<script>
import son
from './son.vue'
export
default {
data: function () {
return {
text:
''
}
},
components: {
son: son
},
methods: {
getSonText (text) {
this.text = text
}
}
}

</script>

<style scoped>
#father div {
padding: 10px;
margin: 10px;
border: 1px solid grey;
overflow: hidden;
}
</style>

 


子组件:

<template>
  <div>
    <p> 我是子组件, 我所拥有的数据: {{text}}</p>
    <button @click="sendData">
      发送数据
    </button>
  </div>
</template>

<script>
export
default {
data () {
return {
text:
'来自子组件的数据'
}
},
methods: {
sendData () {
this.$emit('sendData', this.text)
}
}
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
button {
float: left }
</style>

 


在点击子组件中的“发送数据”按钮前, 父组件还没有接受到数据 (text 为空字符串), 则通过  {{text || '暂无数据' }} 将显示默认文本:‘暂无数据’


点击“发送数据”按钮后:

 

因为 sendData 自定义事件被触发,通过 

this.$emit('sendData', this.text)   //此处的 this 指向子组件实例)

子组件的 text 数据被父组件中:

 <son v-on:sendData='getSonText'></son>

中的 getSonText 函数作为参数接传参受到, 从而完成了从子组件向父组件中的传参过程

 

三. 通过 ref 属性在父组件中直接取得子组件的数据(data)


对于我们上面讲的一和二的处理情景来说,有个局限性就是它们都需要以事件机制为基础(无论是像 click 那样的原生事件还是自定义事件),而在事件发生的时候才能调用函数将数据传递过来
但如果子组件里没有类似“按钮”的东西因而无法制造原生事件同时也没办法找到一个触发自定义事件的时机的时候,怎么从子组件向父组件传递数据呢??
这个时候, 我们就只能从父组件中“直接取”子组件的数据了,借助 ref 属性
ref 是我们经常用到的 Vue 属性,利用它可以简单方便地从本组件的 template 中取得 DOM 实例,而实际上,如果你在父组件中为子组件设置 ref 的话, 就可以直接通过 vm.$refs.[子组件的 ref].[子组件的属性] 去拿到数据啦,例如:
父组件:

<template>
  <div id="father">
    <div>
       我是父组件, 我接受到了:
      {{ text || '暂无数据'  }}
      <button @click="getSonText()"> 接受数据 </button>
      <son ref='son'></son>
    </div>
  </div>
</template>

<script>
import son
from './son.vue'
export
default {
data: function () {
return {
text:
''
}
},
components: {
son: son
},
methods: {
getSonText () {
this.text = this.$refs.son.text
}
}
}

</script>

<style scoped>
#father div {
padding: 10px;
margin: 10px;
border: 1px solid grey;
overflow: hidden;
}
</style>

 


子组件:

<template>
  <div>
    <p> 我是子组件, 我所拥有的数据: {{text}}</p>
  </div>
</template>

<script>
export
default {
data () {
return {
text:
'来自子组件的数据'
}
}
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
button {
float: left }
</style>

 
demo:
尚未点击“接受数据”按钮前:

点击接受数据按钮后:

 

回到顶部

通过 sync 实现数据双向绑定, 从而同步父子组件数据


通过以上三种方式, 我想你应该能解决绝大多数父子组件通信的场景了,但让我们再仔细考虑一下上面的通信场景,就会发现它们还可能存在的问题:

从子组件向父组件传递数据时,父子组件中的数据仍不是每时每刻都同步的

 

在某些特殊的需求场景下,我们可能会希望父子组件中的数据时刻保持同步, 这时候你可能会像下面这样做:
这是父组件中的 template:

<son :foo="bar" v-on:update="val => bar = val"></son>


在子组件中, 我们通过 props 声明的方式接收 foo 并使用

props: {foo: [type]
}

 


同时每当子组件中数据改变的时候, 通过

 

this.$emit('update', newValue)

把参数 newValue 传递给父组件 template 中监听函数中的 "val"。然后通过

 

val => bar = val

 

这个表达式就实现了 bar = newValue. 这个时候,我们发现父组件中的关键数据 bar 被子组件改变(相等)了!
通过数据的双向绑定, 父(组件)可以修改子的数据, 子也可以修改父的数据
Vue 提供了 sync 修饰符简化上面的代码,例如:

<comp :foo.sync="bar"></comp>


会被扩展为:

<comp :foo="bar" @update:foo="val => bar = val"></comp>


然后你需要在子组件中改变父组件数据的时候, 需要触发以下的自定义事件:

this.$emit("update:foo", newValue)


【注意】你可能觉得这好像和我上面提到的二中的“通过自定义事件(emit)从子组件向父组件中传递数据”的那一节的内容似乎重叠了,。
然而并不是, 两者有着父子组件关系上的不同, 下面我通过一行关键的代码证明它们的区别所在
1.在我们讲解 sync 的这一小节里, 自定义事件发生时候运行的响应表达式是:
<son :foo="bar" v-on:update="val => bar = val"></son> 中的 "val => bar = val"
2.在二中的“通过自定义事件从子组件向父组件中传递数据” 里,自定义事件发生时候运行的响应表达式是:
<Son  v-on: eventYouDefined = "arg => functionYours(arg)" /> 中的 "arg => functionYours(arg)"
对前者, 表达式 val => bar = val 意味着强制让父组件的数据等于子组件传递过来的数据, 这个时候,我们发现父子组件的地位是平等的。 父可以改变子(数据), 子也可以改变父(数据)
对后者, 你的 functionYours 是在父组件中定义的, 在这个函数里, 你可以对从子组件接受来的 arg 数据做任意的操作或处理 决定权完全落在父组件中, 也就是:  父可以改变子(数据), 但子不能直接改变父(数据)!, 父中数据的变动只能由它自己决定
下面是一个展示 demo:
父组件:

<template>
  <div id="father">
    <div>
       我是父组件
      <son
        :wisdom.sync="wisdom"
        :magic.sync="magic"
        :attack.sync="attack"
        :defense.sync="defense">
      </son>
      <p> 智力: {{wisdom}}</p>
      <p> 膜法: {{magic}}</p>
      <p> 攻击: {{attack}}</p>
      <p> 防御: {{defense}}</p>
    </div>
  </div>
</template>

<script>
import son
from './son.vue'
export
default {
data: function () {
return {
wisdom:
90,
magic:
160,
attack:
100,
defense:
80
}
},
components: {
son: son
}
}

</script>

<style scoped>
#father div {
padding: 10px;
margin: 10px;
border: 1px solid grey;
overflow: hidden;
}
</style>

 


子组件:

<template>
  <div>
    <p> 我是子组件 </p>
    <p> 智力: {{wisdom}}</p>
    <p> 膜法: {{magic}}</p>
    <p> 攻击: {{attack}}</p>
    <p> 防御: {{defense}}</p>
    <button @click="increment('wisdom')"> 增加智力 </button>
    <button @click="increment('magic')"> 增加膜法 </button>
    <button @click="increment('attack')"> 增加攻击 </button>
    <button @click="increment('defense')"> 增加防御 </button>
  </div>
</template>

<script>
export
default {
props: {
wisdom: Number,
magic: Number,
attack: Number,
defense: Number
},

methods: {
increment (dataName) {
let newValue = this[dataName] + 1
this.$emit(update:${dataName}, newValue)
}
}
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
button {
float: left }
</style>

 


点击前:

 


点击增加子组件中“增加智力”按钮的时候, 父组件和子组件中的智力参数同时从 90 变为 91


点击增加子组件中“增加膜法”按钮的时候, 父组件和子组件中的智力参数同时从 160 变为 161

 

回到顶部

数据双向绑定是把双刃剑

从好处上看:
1.它实现了父子组件数据的“实时”同步, 在某些数据场景下可能会使用到这一点
2.sync 提供的语法糖使得双向绑定的代码变得很简单


从坏处上看:
它破环了单向数据流的简洁性, 这增加了分析数据时的难度

 

回到顶部

当 sync 修饰的 prop 是个对象


我们对上面的例子修改一下, 把数据包裹在一个对象中传递下来:
父组件

<template>
  <div id="father">
    <div>
       我是父组件
      <son :analysisData.sync="analysisData">
      </son>
      <p> 智力: {{analysisData.wisdom}}</p>
      <p> 膜法: {{analysisData.magic}}</p>
      <p> 攻击: {{analysisData.attack}}</p>
      <p> 防御: {{analysisData.defense}}</p>
    </div>
  </div>
</template>

<script>
import son
from './son.vue'
export
default {
data: function () {
return {
analysisData: {
wisdom:
90,
magic:
160,
attack:
100,
defense:
80
}
}
},
components: {
son: son
}
}

</script>

<style scoped>
#father div {
padding: 10px;
margin: 10px;
border: 1px solid grey;
overflow: hidden;
}
</style>

 


子组件:

<template>
  <div>
    <p> 我是子组件 </p>
    <p> 智力: {{analysisData.wisdom}}</p>
    <p> 膜法: {{analysisData.magic}}</p>
    <p> 攻击: {{analysisData.attack}}</p>
    <p> 防御: {{analysisData.defense}}</p>
    <button @click="increment('wisdom')"> 增加智力 </button>
    <button @click="increment('magic')"> 增加膜法 </button>
    <button @click="increment('attack')"> 增加攻击 </button>
    <button @click="increment('defense')"> 增加防御 </button>
  </div>
</template>

<script>
export
default {
props: {
analysisData: Object
},

methods: {
increment (dataName) {
let newObj = JSON.parse(JSON.stringify(this.analysisData))
newObj[dataName]
+= 1
this.$emit('update:analysisData', newObj)
}
}
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
button {
float: left }
</style>

 


 demo 同上

回到顶部

不要通过在子组件中修改引用类型 props 达到“父子组件数据同步”的需求!


父组件的数据传递给子组件, 一般通过 props 实现, 而在实现“父子组件数据同步”这一需求的时候, 小伙伴们可能会发现一点: 在子组件中修改引用类型的 props(如数组和对象)是可行的
1. 不仅可以达到同时修改父组件中的数据(因为本来引用的就是同一个数据)
2. 而且还不会被 Vue 的检测机制发现!(不会报错)
但千万不要这样做, 这样会让数据流变得更加难以分析,如果你尝试这样做, 上面的做法可能会更好一些
不要这样做,糟糕的做法:
父组件:

<template>
  <div id="father">
    <div>
       我是父组件
      <son :analysisData="analysisData">
      </son>
      <p> 智力: {{analysisData.wisdom}}</p>
      <p> 膜法: {{analysisData.magic}}</p>
      <p> 攻击: {{analysisData.attack}}</p>
      <p> 防御: {{analysisData.defense}}</p>
    </div>
  </div>
</template>

<script>
import son
from './son.vue'
export
default {
data: function () {
return {
analysisData: {
wisdom:
90,
magic:
160,
attack:
100,
defense:
80
}
}
},
components: {
son: son
}
}

</script>

<style scoped>
#father div {
padding: 10px;
margin: 10px;
border: 1px solid grey;
overflow: hidden;
}
</style>

 


子组件:

<template>
  <div>
    <p> 我是子组件 </p>
    <p> 智力: {{analysisData.wisdom}}</p>
    <p> 膜法: {{analysisData.magic}}</p>
    <p> 攻击: {{analysisData.attack}}</p>
    <p> 防御: {{analysisData.defense}}</p>
    <button @click="increment ('wisdom')"> 增加智力 </button>
    <button @click="increment ('magic')"> 增加膜法 </button>
    <button @click="increment ('attack')"> 增加攻击 </button>
    <button @click="increment ('defense')"> 增加防御 </button>
  </div>
</template>

<script>
export
default {
props: {
analysisData: Object
},

methods: {
increment (dataName) {
let obj = this.analysisData
obj[dataName]
+= 1
}
}
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
button {
float: left }
</style>

 


demo 同上, 但这并不是值得推荐的做法

 【完】