vue组件之间的通信

组件之间的通信

一、通过 p a r e n t 和 parent 和 parentroot 获取父组件或者根组件的实例

在子组件里想要获取到父组件、根组件的实例 直接通过 p a r e n t 、 parent、 parentroot 获取。

例如 父组件有以下数据:

//父组件中引入子组件
import ChildView from '../ChildView.vue'
//暴露
export default {
  //在父组件中注册子组件
  components: {
    ChildView
  },
  //父组件的data数据(组件因为要复用、所以必须写成函数返回值形式)
  data() {
    return {
      msg: 'Hello'
    }
  },
  provide:{
      get() {
      console.log("Hello world")
    }
  }
   //方法
  methods: {
      //父组件中的get()
    get() {
      console.log("Hello world")
    }
  }
}

在子组件ChildView中获取:

//子组件的模板
<template>
<div>{{$parent.msg}}</div>
</template>
 
//子组件的script
<script>
export default {
  //子组件在创建完毕后执行此函数
  created() {
      //this指向当前的子组件实例
      //.$parent.get() 获取父组件中的get方法
    this.$parent.get()
  }
}
</script>

父组件中的data数据也是可以读写的,可以直接修改其值

<template>
<!--通过$parent获取到父元素data中的msg -->
<div>{{$parent.msg}}</div>
<!-- 修改父组件的数据 -->
<button @click="changeMsg"></button>
</template>
 
<script>
export default {
  //用inject注入一下来自父组件的方法get
  inject: ['get'],
    
    //创建完毕后 获取到父组件的get方法
  created() {
    this.$parent.get()
  },
    
  methods: {
    changeMsg() {
        //修改获取到的父组件的data中的msg
      this.$parent.msg = "I'm a child"
    }
  }
}
</script>

$root:

所谓的根组件数据 就是在创建vue实例时 存入的data数据

所以它的所有子组件都能够用 $root去获取到根组件中的data中的属性

this: 当前实例

this.$parent :当前组件的父组件实例

this.$root:组件的根组件实例

二、通过$refs获取子组件的实例

尽管存在 prop 和事件 有时候仍然可能需要在js里直接访问一个子组件 为了达到这个目的 可以通过ref来给子组件赋予一个id进行引用 例如:

<base-input ref="userNameInput"></base-input>

上边已经定义了这个ref的组件里 你可以进行使用了:

this.$refs.userNameInput//此时获取到的是上边定义的标签元素

来访问base-input 实例 该组件也可以使用一个类似的ref提供对内部这个指定元素的访问 例如:

<input ref="input"><!-- 给input标签打上自定义id-->

通过父组件定义方法

methods: {
  // 用来从父级组件聚焦输入框
  focus: function () {
      //此时在父组件里使用 this.$refs.(name)来使用子组件中打上id的自定义id
    this.$refs.input.focus()
  }
}

** 在循环中使用ref

当ref和v-for 在一起使用时 你得到的ref将会是一个包含了对应数据源的这些子组件数组。

示例:

<template>
<ul>
  <li v-for="(item, key) in list" :key="key" ref="li">{{item}}</li>
</ul>
</template>
 
<script>
export default {
  mounted() {
    console.log(this.$refs.li)
  },
}
</script>

执行结果:获取到了 一个包含li标签的列表

img

所以 要取到其中的某个元素 就需要数组的操作方法 使用下标

this.$refs.li[0].innerText

三、通过prop向子组件传值

组件实例的作用域都是孤立的。这就意味着不能(也不应该)在子组件的模板中直接引用父组件中的数据,父组件的数据需要通过prop 才能下发到子组件中。

子组件要显式地用props选项声明它的预期的数据:

Vue.component('child', {
  // 声明 props
  props: ['message'],
  // 就像 data 一样,prop 也可以在模板中使用
  // 同样也可以在 vm 实例中通过 this.message 来使用
  template: '<span>{{ message }}</span>'
})

然后我们就可以这样向它传入一个普通字符串:

<child message="hello!"></child>

命名方式

html特性是不区分大小写的 所以,当使用的不是字符串模板时 camelCase(驼峰命名法) 的prop需要转换为相应的kebab-case(短横线分隔式命名)

Vue.component('child', {
  // 在 JavaScript 中使用 camelCase
  props: ['myMessage'],
  template: '<span>{{ myMessage }}</span>'
})
<!-- 在 HTML 中使用 kebab-case -->
<child my-message="hello!"></child>

如果你使用的是字符串模板 则没有这些限制。

动态属性

与绑定的任何普通的 html 特性相类似,我们可以用 v-bind 来动态地将prop绑定到父组件的数据中。每当父组件的数据变化时,该变化的也会传导给子组件;

<div>
    <input v-model='parentMsg'>
    <br>
    <child v-bind:my-message="parentMsg"><child>
</div>

你也可以使用v-bind的缩写语法 :

<child :my-message = "parentMsg"></child>

如果你想把一个对象的所有属性作为prop进行传递,可以使用不带任何参数的v-bind(即使用v-bind 而不是 v-bind:prop-name).例如,已经知道一个todo对象

todo{
    text:"Learn Vue",
    isComplate:false,
}

然后:

<todo-item v-bind="todo"></todo-item>

将等价于:

<todo-item :text="todo.text" :is-complete="todo.isComplete"></todo-item>

字面量语法 vs 动态语法

初学者常犯的一个错误是使用字面量语法传递数值:

<comp some-prop="1"></comp>

因为它是一个字面量 prop,它的值是字符串 “1” 而不是一个数值。如果想传递一个真正的 JavaScript 数值,则需要使用 v-bind,从而让它的值被当作 JavaScript 表达式计算:

<comp :some-prop="1"></comp>
单向数据流

Prop 是单向绑定的:当 **父组件的属性变化时,将传导给子组件,但是反过来不会。**这是为了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得难以理解。

另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop。如果你这么做了,Vue 会在控制台给出警告。

在两种情况下,我们很容易忍不住想去修改 prop 中数据:

​ 1、Prop 作为初始值传入后,子组件想把它当作局部数据来用;
​ 2、Prop 作为原始数据传入,由子组件处理成其它数据输出。

对这两种情况,正确的应对方式是:

1.定义一个局部变量,并用 prop 的值初始化它:

props: ['initialCounter'],
data: function () {
  return { counter: this.initialCounter }
}

2.定义一个计算属性,处理 prop 的值并返回:

props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。

属性验证

我们可以为组件的 prop 指定验证规则。如果传入的数据不符合要求,Vue 会发出警告。这对于开发给他人使用的组件非常有用。

要指定验证规则,需要用对象的形式来定义 prop,而不能用字符串数组:

Vue.component('example', {
  props: {
    // 基础类型检测 (`null` 指允许任何类型)
    propA: Number,
    // 可能是多种类型
    propB: [String, Number],
    // 必传且是字符串
    propC: {
      type: String,
      required: true
    },
    // 数值且有默认值
    propD: {
      type: Number,
      default: 100
    },
    // 数组/对象的默认值应当由一个工厂函数返回
    propE: {
      type: Object,
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        return value > 10
      }
    }
  }
})

type 可以是下面原生构造器:

String
Number
Boolean
Function
Object
Array
Symbol
type 也可以是一个自定义构造器函数,使用 instanceof 检测。

四、通过 emit 向父组件传值

父组件使用 prop 传递数据给子组件。但子组件怎么跟父组件通信呢?这个时候 Vue 的自定义事件系统就派得上用场了。

每个 Vue 实例都实现了事件接口,即:

使用 o n ( e v e n t N a m e ) ∗ ∗ 监 听 事 件 使 用 ∗ ∗ on(eventName)** 监听事件 使用 ** on(eventName)使emit(eventName) 触发事件
使用 v-on 绑定自定义事件
Vue 的事件系统与浏览器的 EventTarget API 有所不同。尽管它们的运行起来类似,但是 o n ∗ ∗ 和 ∗ ∗ on** 和 ** onemit 并不是addEventListener 和 dispatchEvent 的别名。

另外,父组件可以在使用子组件的地方直接用 v-on 来监听子组件触发的事件。

父组件:

<div id="counter-event-example">
  <p>{{ total }}</p>
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <button-counter v-on:increment="incrementTotal"></button-counter>
</div>
new Vue({
  el: '#counter-event-example',
  data: {
    total: 0
  },
  methods: {
    incrementTotal: function () {
      this.total += 1
    }
  }
})

子组件:

Vue.component('button-counter', {
  template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    incrementCounter: function () {
      this.counter += 1
      this.$emit('increment')
    }
  }
})

上例中,子组件使用 emit 向父组件传递一个 increment 的事件,父组件只需监听 increment 即可,

使用 v-on:increment 或 @increment 。

emit 传值

子组件中可以使用 emit 附带数据传递给父组件。

子组件:

Vue.component('counter', {
  template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    incrementCounter: function () {
      this.counter += 1
      this.$emit('increment', this.counter)
    }
  }
})

父组件:

<div id="counter-event-example">
  <p>{{ total }}</p>
  <counter @increment="incrementTotal"></counter>
</div>
new Vue({
  el: '#counter-event-example',
  data: {
    total: 0
  },
  methods: {
    incrementTotal (counter) {
      this.total = counter
    }
  }
})

上例中,子组件向父组件传递一个 increment 事件,并附带数据 ,在父组件中相应的事件监听方法中可以捕捉到传递的数据。

向自定义事件中传入额外参数

有的时候,在某些自定义组件中,通过 e m i t ∗ ∗ 本 身 就 暴 露 出 一 些 参 数 的 情 况 下 , 我 们 还 需 要 从 父 组 件 中 传 递 其 他 参 数 , 但 是 如 果 直 接 写 到 方 法 的 参 数 中 会 覆 盖 本 身 的 ∗ ∗ emit** 本身就暴露出一些参数的情况下,我们还需要从父组件中传递其他参数,但是如果直接写到方法的参数中会覆盖本身的 ** emitemit 返回的参数。这个时候可以在外面包裹一层箭头函数,在箭头函数体中调用方法并传递额外的参数。

举个例子,在一个组件中通过 $emit 返回了一些数据:

<template>
  <div
    v-for="(item, index, key) in list"
    :key="key"
    @click="clickItem(item, index)"
  >
    {{ item.name }}
  </div>
</template>
 
<script>
export default {
  props: {
    list: Array,
  },
  methods: {
    clickItem(item, index) {
      this.$emit("click", item, index);
    },
  },
};
</script>

如果在父组件中直接传入参数,则会覆盖掉从 $emit 中返回的值:

<template>
  <div>
    <TestView @click="clickItem('hello')" :list="list"></TestView>
  </div>
</template>
 
<script>
import TestView from "./components/TestView.vue";
 
export default {
  components: {
    TestView,
  },
  data() {
    return {
      list: [
        {
          name: "item1",
        },
        {
          name: "item2",
        },
      ],
    };
  },
  methods: {
    clickItem(item, index, data) {
      console.log(item, index, data); // hello undefined undefined
    },
  },
};
</script>

如果使用事件对象 e v e n t ∗ ∗ , 则 只 能 够 获 取 到 从 ∗ ∗ event** ,则只能够获取到从 ** eventemit 传出的第一个参数:

<template>
  <div>
    <TestView @click="clickItem($event, 'hello')" :list="list"></TestView>
  </div>
</template>

点击列表项后将打印出:

{...} "hello" undefined

改写为以下写法即可:

<template>
  <div>
    <TestView
      @click="
        (item, index) => {
          clickItem(item, index, 'hello');
        }
      "
      :list="list"
    ></TestView>
  </div>
</template>

点击列表项后将打印出:

{…} 0 “hello”

给组件绑定原生事件

有时候,你可能想在某个组件的根元素上监听一个原生事件。可以使用 v-on 的修饰符 .native。例如:

<my-component @click.native="doTheThing"></my-component>
总线通信

有时候,非父子关系的两个组件之间也需要通信。在简单的场景下,可以使用一个空的 Vue 实例作为事件总线:

let bus = new Vue()
 
// 触发组件 A 中的事件
bus.$emit('id-selected', 1)
 
// 在组件 B 创建的钩子中监听事件
bus.$on('id-selected', function (id) {
  // ...
})

在复杂的情况下,应该考虑使用专门的状态管理模式。

五、组件数据双向绑定(.sync 修饰符)

在一些情况下,我们可能会需要对一个 prop 进行“双向绑定”。事实上,这正是 Vue 1.x 中的 .sync 修饰符所提供的功能。当一个子组件改变了一个带 .sync 的 prop 的值时,这个变化也会同步到父组件中所绑定的值。这很方便,但也会导致问题,因为它破坏了单向数据流。由于子组件改变 prop 的代码和普通的状态改动代码毫无区别,当光看子组件的代码时,你完全不知道它何时悄悄地改变了父组件的状态。这在 debug 复杂结构的应用时会带来很高的维护成本。

从 Vue 2.3.0 起重新引入了 .sync 修饰符,但是这次它只是作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的 v-on 监听器。

如下代码:

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

会被扩展为:

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

当子组件需要更新 foo 的值时,它需要显式地触发一个更新事件:

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

相当于在子组件中更改了来自父组件传递的值,再通过 emit 将改变的值反馈给父组件,父组件拿到更改后的值后,再将传递给子组件的值更新。

举个例子:

父组件:

<template>
  <div id="app">
    <HelloWorld :msg.sync='msg'/>
  </div>
</template>
 
<script>
import HelloWorld from './components/HelloWorld.vue'
 
export default {
  components: {
    HelloWorld
  },
  data() {
    return {
      msg: 'Hello'
    }
  },
}
</script>

子组件:

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <button @click="modifyMsg">修改msg</button>
  </div>
</template>
 
<script>
export default {
  props: {
    msg: String
  },
  methods: {
    modifyMsg() {
      this.$emit('update:msg', 'Got msg')
    }
  }
}
</script>

效果如下:

img

六、通过 v-model 实现表单组件双向数据传递

同步子组件的表单输入
由于下面这种写法

<input v-model="something">

这不过是以下示例的语法糖:

<input
  v-bind:value="something"
  v-on:input="something = $event.target.value" />

所以在组件中使用时,它相当于下面的简写:

<custom-input
  v-bind:value="something"
  v-on:input="something = arguments[0]">
</custom-input>

所以要让组件的 v-model 生效,它应该 (从 2.2.0 起是可配置的):

1、接受一个 value 的 prop
2、在有新的值时触发 input 事件并将新值作为参数


举个例子:一个非常简单的货币输入的自定义控件。

父组件:

<template>
  <div>
    <div>${{ price }}</div>
    <currency-input v-model="price"></currency-input>
  </div>
</template>
 
<script>
import CurrencyInput from "./components/CurrencyInput.vue";
 
export default {
  components: {
    CurrencyInput,
  },
  data() {
    return {
      price: 10,
    };
  },
};
</script>

子组件:

<template>
  <span>
    <input
      ref="input"
      :value="value"
      @input="updateValue($event.target.value)"
    />
  </span>
</template>
 
<script>
export default {
  props: ["value"], // 显式接收一个 value 的 prop
  methods: {
    // 不直接更新值,而是使用此方法来对输入值进行格式化和位数限制
    updateValue: function (value) {
      let len =
        value.indexOf(".") === -1 ? value.length : value.indexOf(".") + 3;
      let formattedValue = value.trim().slice(0, len);
      // 如果值尚不合规,则手动覆盖为合规的值
      if (formattedValue !== value) {
        this.$refs.input.value = formattedValue;
      }
      // 通过 input 事件带出数值
      this.$emit("input", Number(formattedValue));
    },
  },
};
</script>

效果如下:

img

自定义组件的 v-model

默认情况下,一个组件的 v-model 会使用 value 属性input 事件。但是诸如单选框、复选框之类的输入类型可能把 value 用作了别的目的。model 选项 (从 2.2.0 起)可以避免这样的冲突:

Vue.component('my-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean,
    // 这样就允许拿 `value` 这个 prop 做其它事了
    value: String
  },
  // ...
})
<my-checkbox v-model="foo" value="some value"></my-checkbox>

上述代码等价于:

<my-checkbox
  :checked="foo"
  @change="val => { foo = val }"
  value="some value">
</my-checkbox>

注意你仍然需要显式声明 checked 这个 prop。


checkbox 示例:

父组件:

<template>
  <div>
    <div>checkbox: {{ checkbox1 }} {{ checkbox2 }}</div>
    <div>colors: {{ colors }}</div>
    <my-checkbox
      v-model="checkbox1"
      value="red"
      @change="updateVal"
    ></my-checkbox>
    <my-checkbox
      v-model="checkbox2"
      value="green"
      @change="updateVal"
    ></my-checkbox>
  </div>
</template>
 
<script>
import MyCheckbox from "./components/MyCheckbox.vue";
 
export default {
  components: { MyCheckbox },
  data() {
    return {
      checkbox1: false,
      checkbox2: false,
      colors: [],
    };
  },
  methods: {
    updateVal(checked, value) {
      console.log(checked);
      console.log(value);
      if (checked) {
        this.colors.push(value);
      } else {
        this.colors.splice(this.colors.indexOf(value), 1);
      }
    },
  },
};
</script>

子组件:

<template>
  <span>
    <input
      type="checkbox"
      ref="checkbox"
      @change="updateValue($event.target)"
      :value="value"
      :id="value"
    />
    <label :for="value">{{ value }}</label>
  </span>
</template>
 
<script>
export default {
  model: {
    prop: "checked",
    event: "change",
  },
  props: {
    checked: Boolean,
    value: String,
  },
  methods: {
    updateValue(val) {
      // 通过 change 事件带出数值
      this.$emit("change", val.checked, val.value);
    },
  },
};
</script>

效果如下:

img

七、依赖注入(provide/inject)

除了通过props向子组件中传值外,vue还可以通过peovide/inject 向子组件注入数据、方法。跟props不同的是,inject可以向子孙组件注入数据、方法、无视子孙组件的层级、而不光光只是子组件。

比如:

Root(provide) -> Child 1 -> Child 2 -> Child 3(inject)

在Root组件中注入(provide)数据,只是其子孙组件,都可以用inject取出注入的数据。

注入数据

举个例子,在祖先组件中注入数据msg:

<template>
  <div><InjectView /></div>
</template>
 
<script>
import InjectView from "../components-test/InjectView.vue";
export default {
  components: {
    InjectView,
  },
  provide: {
    msg: "Hello",
  },
};
</script>

其子组件(InjectView) 又包含了一个子组件 InjectChild :

<template>
  <div><InjectChild /></div>
</template>
 
<script>
import InjectChild from "./InjectChild.vue";
export default {
  components: {
    InjectChild,
  },
};
</script>

只要是父组件中注入了数据 就可以取

最后在孙组件中使用

<template>
  <div>{{ info }}</div>
</template>
 
<script>
export default {
  inject: {
    info: "msg",
  },
  created() {
    console.log(this.info); // Hello
  },
};
</script>

将注入的数据 msg 改名字为info。

要是使用props 则需要一层层传值才能达到相同的效果。

注入方法

跟data属性一样,provide 也可以返回一个函数, 其返回值甚至可以是组件中的方法。

还是以上面的祖先组件为例 下来写其 script部分

import InjectView from '../components-test/InjectView.vue'
export default {
  components: {
    InjectView
  },
  provide() {
    return {
      log: this.log
    }
  },
  methods: {
    log() {
      console.log("Hello world")
    }
  }
}

在儿子组件中取出此方法

export default {
  inject: {
    print: 'log',
  },
  created() {
    this.print() // Hello world
  }
}

Inject 也可以返回一个数组 跟props差不多

export default {
  inject: ['log'],
  created() {
    this.log()
  }
}

八、参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值