解锁 vue2.0 通信的各种姿势

$refs 、 $parent 和 $children

最开始,ref 是什么?这是 Vue 实现的一个特殊属性,用来给一个元素(就是 HTML 定义的元素)或子组件注册的一个引用。这个属性被添加在父组件$refs 对象中。如果用在一个普通的 DOM 元素,ref 指的就是这个这个元素;如果用在一个子组件上,ref 指的是这个组件的一个 instance。

官方解释

从上面的描述中,我们可以得到一些信息:

  • ref 可以单纯地对一个 DOM 元素或一个组件进行标识。
  • ref 不是 DOM 的属性,是 Vue 实现的,也不是一个指令
  • ref 的目的是单纯地让方便快捷地处理 DOM 元素或子组件
  • $refs 就是父组件上用来保存这些 ref 的一个对象集合

举个例子:

父组件 - Parent.vue,使用 ref 标识了一个 h1 标签和一个子组件 child。但父组件还有另一个子组件 another-child 未被 ref 标识。

<template>
  <div class="hello">
    <h1 ref="maybeATitle">{{ parentMsg }}</h1>
    <button @click="clickParent">I am a FATHER</button>
    <child ref="myChild"></child>
    <another-child></another-child>
  </div>
</template>

<script>
import Child from './Child'
import AnotherChild from './AnotherChild'
export default {
  components: {
    Child,
    AnotherChild
  },
  name: 'HelloWorld',
  data () {
    return {
      parentMsg: 'I am a Father'
    }
  },
  methods: {
    clickParent () {
      console.log('this.$refs', this.$refs)
      console.log('this.$refs.myChild', this.$refs.myChild.childMsg)
      console.log('this.$children', this.$children)
    }
  }
}
</script>

我们将 this.$refs 打印出来。结果如下:

this.$refs

结果对第一条做出了解释: (1) ref 是元素或子组件标识,且被添加到父组件的 this.$refs 对象中。 (2) 如果用在一个普通的 DOM 元素,ref 指的就是这个这个元素;如果用在一个子组件上,ref 指的是这个组件的一个 instance。 (3) 如果想访问子组件 child 就可以通过 this.$refs.myChild 访问了

我们先看一下子组件 Child.vue 的内容,只包含一个 button 元素,有一个 click 事件,还有一个 data 属性 childMeg

<template>
  <button @click="clickSon">I am a SON</button>
</template>
<script>
export default {
  data () {
    return {
      childMsg: 'I am a Child'
    }
  },
  methods: {
    clickSon () {
      console.log('this.$parent.parentMsg', this.$parent.parentMsg)
    }
  }
}
</script>

那么,刚才说可以通过 this.$refs.myChild 访问到子组件 child,这是真的吗?我们展开看一下 this.$refs.myChild 的内容。

this.$refs.myChild

$children 是什么?我么在父组件中将 this.$children 打印出来。

很明白地看得出来 $children 是当前父组件实例的直接子组件实例的组合(没有确定的顺序关系,也不是响应式的)。

那么,this.parent 就是当前子组件的实例的直接父组件实例的引用。Father 有多个 Sons,而 Son 只有一个 Father

所以,通过 ref$refs$parent 或者 $children$parent 就可以构建父子组件间的通信。

比如上面的父组件 Parent.vue,通过 ref 对子组件构成唯一标识,使用 $refs.myChild 对子组件进行数据访问。子组件使用 $refs.parent 对父组件进行数据访问。

HTML 代码:

<!--Parent.vue-->
<child ref="myChild"></child>

JavaScirpt 代码:

// Parent.vue
clickParent () {
  console.log('this.$refs.myChild', this.$refs.myChild.childMsg)
}
// Child.vue
clickSon () {
  console.log('this.$parents.parentMsg', this.$parent.parentMsg)
}

需要注意的是,$refsref 是在初始 render 之后才存在的,并且也不是响应式的。最好不要在模板或者计算属性中使用它,也不能用于数据绑定。

Github demo 地址


props、$emit 和 .sync

props父组件向子组件传递数据的一种方式,这是一种单向的数据传播方式。使用起来也非常简单:

<!--Ancestor.vue-->
<template>
  <div class="hello">
    <h1>I am a father of child, and an ancestor of grandson</h1>
    <child :ancestor-message="grandsonMessage" v-on:descendant="getMeg"></child>
  </div>
</template>

<script>
import Child from './Child'
export default {
  components: {
    Child
  },
  data () {
    return {
      grandsonMessage: 'Message from an acenstor'
    }
  },
  methods: {
    getMeg (msg) {
      console.log(msg)
    }
  },
}
</script>

<style scoped>
  .hello {
    border: 1px solid black;
    height: 400px;
    background: whitesmoke
  }
</style>

父组件 <Ancestor /> 向下传递数据 ancestorMessage为啥不写成 ancestor-message)。并使用 v-on 监听了子组件自定义的 descendant 事件。

下面是子组件的代码:

<!-- Child.vue -->
<template>
  <div class="child">
    <h2> {{ ancestorMessage }} </h2>
  </div>
</template>
<script>
export default {
  props: {
    ancestorMessage: {
      type: String,
      required: true
    }
  },
  mounted () {
    this.$emit('descendant', 'hello Wall·E')
  }
}
</script>
<style scoped>
  .child {
    border: 2px dotted red;
    height: 250px;
    margin: 0 20px;
    background: rgb(214, 206, 206);
  }
</style>

这都很简单。需要强调的是,不要试图在子组件修改 props,所以试图修改 props 时,可以使用 data 或 computd 属性拷贝一份 props

TIP

总结一下:父组件通过 props 向子组件传值,子组件可通过 $emit 一个自定义事件向父组件传值。 另外,通过 $emit 传递消息 还有其他的写法,比如:

<!--Ancestor.vue-->
<child :ancestor-message="grandsonMessage" v-on:descendant="grandsonMessage += $event"></child>

<!-- Child.vue -->
<button :click="$emit('descendant', ' Hello Wall·E')">

但是,很多情况我们会有 双向绑定 props 的需求。这种情况下,我们可以使用 .sync Modifier

比如有个例子,父组件传递一个 isFold 判断子组件的内容是否展开,同时子组件也会试图修改 props 的值,来决定自身是否被展开。

<!-- SubMenu.vue -->
<nav-sub-menu :is-fold.sync="defaultOpen">
  <item-menu></item-menu>
</nav-sub-menu>

<script>
  export default {
    data () {
      return {
        defaultOpen: true
      }
    }
  }
</script>

:is-fold.sync="defaultOpen" 是一种简写方式,等同于:

<nav-sub-menu
  v-bind:is-fold="defaultOpen"
  v-on:update:is-fold="defaultOpen = $event"
>

子组件的代码:

<!-- ItemMenu.vue -->
<div
  @click="toggleSubMenu">
</div>
<script>
export default {
  props: {
    isFold: {
      type: Boolean,
      default: true
    }
  },
  methods: {
    toggleSubMenu () {
      this.$emit('update:isFold', !this.isFold)
    }
  }
}
</script>

Github demo 地址


$attrs 和 $listeners

在高阶组件中,多层级组件通信可以是通过 props$emit 进行,从而达到跨层级组件通信。

当然,多层级组件通信还可以通过 Vuex 或全局 Event Bus 的方式。

但是,以 props$emit 这种方式进行通信有一个很大的缺点,以下图为例:

A 是祖先组件,如果需要将信息传递到子孙组件 D,组件 BC 需要作为中转站处理 props,这样会导致组件维护难度加大,props 传递也非常不明朗。

$attrs$listeners 就是为了解决这一问题。两者并不是一类新的组件通信方式,而是 props$emit 通信方式的一种补充。

注意: $attrs 是在 Vue 2.4 才引入的。

祖先组件 A:Ancestor.vue:

<!--Ancestor.vue-->
<template>
  <div class="hello">
    <h1>A: I am a father of child, and an ancestor of grandson</h1>
    <child :message="grandsonMessage" v-on:descendant="getMeg"></child>
  </div>
</template>

<script>
import Child from './Child'
export default {
  components: {
    Child
  },
  data () {
    return {
      grandsonMessage: 'Message from an acenstor'
    }
  },
  methods: {
    getMeg () {
      console.log('message from descendant--')
    }
  },
  mounted () {
    console.log('acenstor $attrs----', this.$attrs)
  }
}
</script>

在组件 Ancestor.vue(A) 试图将一个 messageprops 传递给 子孙组件 GrandgrandSon.vue(D)。而 A 的其他子孙组件 Child.vue(B) 和 GrandSon.vue(C) 并不需要这个 props。同时,A 还监听了 D 传递的 descentdant 事件。

如何,我们先来看一下 子孙组件 D:GrandgrandSon.vue 的代码:

<!--GrandgrandSon.vue-->
<template>
  <div class="grandgrandson">
    D: This is a <span>{{ message }}</span>
  </div>
</template>
<script>
export default {
  props: ['message'],
  mounted () {
    this.$emit('descendant')
    console.log('sending a msg to ancestor')
    console.log('grandgrandson $attrs----', this.$attrs)
  }
}
</script>

祖先组件 A 和子孙组件 D 通信的方式就像普通的父子组件通信的方式一样。

之后,组件 B 和 C 通过 $attrs$listeners 搭建“数据中转站”。如下:

<!--Child.vue-->
<template>
  <div class="child">
    <h2>B: I am a child of A</h2>
    <grand-son v-bind="$attrs" v-on="$listeners"></grand-son>
  </div>
</template>
<script>
import GrandSon from './GrandSon'
export default {
  components: {
    GrandSon
  },
  inheritAttrs: false,
  mounted () {
    console.log('child $attr----', this.$attrs)
    console.log('child $listener----', this.$listeners)
  }
}
</script>
<!--GrandSon.vue-->
<template>
  <div class="grandson">
    <h3>C: I am a child of B, and a father of D</h3>
    <grandgrand-son v-bind="$attrs" v-on="$listeners"></grandgrand-son>
  </div>
</template>
<script>
import GrandgrandSon from './GrandgrandSon'
export default {
  components: {
    GrandgrandSon
  },
  data () {
    return {}
  },
  inheritAttrs: false,  
  mounted () {
    console.log('grandson $attrs----', this.$attrs)
    console.log('grandson $listens---', this.$listeners)
  }
}
</script>

可以看到这两行的代码:

<grand-son v-bind="$attrs" v-on="$listeners"></grand-son>
<grandgrand-son v-bind="$attrs" v-on="$listeners"></grandgrand-son>

架起了沟通的桥梁!

有一个地方需要说明一下,我们使用了 inheritAttrs: false。这个属性的介绍在这里。该属性的默认值是 true,当父组件的传递过来的 props 未被子组件视为 props 时,就会作为子组件根元素普通的 HTML 属性存在。

比如,该属性为 true 渲染的结果。

false 时:

最后,看一下项目打印出来的信息。

Github demo 地址


v-model

v-model 是 Vue 实现的一种双向绑定。

比如:

<input v-model="text" />

<!-- 等价于 -->
<input
  :value="text"
  @input="text = $event.target.value">

相对而言是一种比较简单的操作,绑定 <input>name 属性,监听 input 事件。

如果我们想要对自定义的组件使用 v-model 指令。当用在自定义组件中,比如:

<my-input v-model="text" />
<!-- 等价于 -->
<my-input
  :value="text"
  @input="text = $event" />

举个例子:自定义一个 MyInput 组件。

<!-- Input.vue -->
<template>
  <input
    :value="value"
    @input="$emit('input', $event.target.value)" />
</template>

<script>
export default {
  name: 'MyInput',
  props: ['value']
}
</script>

然后使用 Vue.component 封装一下:

// index.js
import Input from './Input.vue'
import Vue from 'vue'

export default Vue.component(Input.name, Input)

可以看到,我们需要做两件事:

  • <input>value 属性绑定到 propsvalue 中来
  • $emit 自定义的 event 事件

Github Demo

上次更新: 1/3/2019, 5:21:25 PM