v-model
是用来在表单控件或者组件上创建双向绑定的,他的本质是 v-bind
和 v-on
的语法糖,在一个组件上使用 v-model
,默认会为组件绑定名为 value
的 prop
和名为 input
的事件。
语法糖
解析
简写
当我们组件中的某一个 prop
需要实现上面所说的”双向绑定“时,v-model
就能大显身手了。有了它,就不需要自己手动在组件上绑定监听当前实例上的自定义事件,会使代码更简洁。
下面以一个 input 组件实现的核心代码,简单介绍下 v-model 的在组件中实现动态传值:
父组件代码如下:
<template>
<div>
父组件:<span>{{message}}</span>
<Child v-model="message"></Child>
</div>
</template>
<script>
import Child from '../Child'
export default {
components:{Child},
data() {
return {
message: ''
}
},
}
</script>
子组件代码如下:
<template>
<div>
子组件:
<input type="text" :value="currentValue" @input="handleInput">
</div>
</template>
<script>
export default {
data() {
return {
currentValue: this.value === undefined || this.value === null || ''
}
},
props: {
value: [String, Number],
},
methods: {
handleInput(event) {
const value = event.target.value;
this.$emit('input', value);
},
},
}
</script>
我们可以通过上面代码,子组件修改父组件v-model绑定的值!
以上这种方式实现的父子组件的v-model通信,限制了popos接收的属性名必须为value和emit触发的必须为input,这样容易有冲突,特别是在表单里面。Vue2.2.0+ 新增自定义组件的model选项,可以自定义属性(prop)和事件(event)名称,这样更加灵活也可以避免默认的 value 属性值有其它的用处
子组件代码修改如下:
<template>
<div>
子组件:
<input type="text" :value="currentValue" @input="handleInput">
</div>
</template>
<script>
export default {
data() {
return {
currentValue: this.val === undefined || this.val === null || ''
}
},
// model选项用来避免冲突,prop属性用来指定props属性中的哪个值用来接收父组件v-model传递的值,
// 例如:这里用props中的flag1来接收父组件传递的v-model值;event属性可以理解为父组件@input的别名,从而避免冲突,即emit时要提交的事件名。
model: {
prop: 'val',
event: 'AnyName'
},
props: {
//定义和model的prop相同的props来接收
val: [String, Number],
},
methods: {
handleInput(event) {
const value = event.target.value;
this.$emit('AnyName', value);
},
},
}
</script>
在创建类似复选框或者单选框或者下拉框的常见组件时,v-model就不好用了。
以checkbox为例的解决办法:
可以这样解决
父组件:
<template>
<div>
父组件:<span>{{whyrooms}}</span>
<Child v-model="whyrooms"></Child>
</div>
</template>
<script>
import Child from '../Child'
export default {
components:{Child},
data() {
return {
whyrooms: false
}
},
}
</script>
子组件:
<template>
<div>
子组件:
<!--这里就不用 input 了,而是 任意名称-->
<input type="checkbox"
@change="handleChange"
:checked="checked"
/>
</div>
</template>
<script>
export default {
data() {
return {
}
},
model: {
prop: 'checked',
event: 'AnyName'
},
props: {
checked: Boolean,
},
methods: {
handleChange(event) {
const checked = event.target.checked;
this.$emit('AnyName', checked)
},
},
}
</script>
.sync
修饰符在 vue 1.x 的版本中就已经提供,1.x 版本中,当子组件改变了一个带有 .sync 的 prop 的值时,会将这个值同步到父组件中的值。这样使用起来十分方便,但问题也十分明显,这样破坏了单向数据流,当应用复杂时,debug 的成本会非常高。
Vue2.3.0+
新增,默认以 update:prop
模式触发事件,注意.sync 绑定的 prop 不能是表达式(v-bind:title.sync=“message + ‘!’”)这种绑定,只能是属性名
新的 .sync 修饰符所实现的已经不再是真正的双向绑定,它的本质和 v-model 类似,只是一种缩写。
<text-document v-bind:title.sync="doc.title"></text-document>
<!-- 是以下的简写: -->
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>
父组件代码:
<template>
<div>
<h2>父组件的值num-->{{num}}---num1-->{{num1}}</h2>
<Child :a.sync="num " :b.sync='num1'></Child>
</div>
</template>
<script>
import Child from "../Child";
export default {
data() {
return {
num: 10,
num1:20
};
},
components: {
Child,
},
};
</script>
子组件代码:
<template>
<div>
<h3>子组件接收的值a:{{a}}</h3>
<h3>子组件接收的值b:{{b}}</h3>
<button @click='change'>点击修改</button>
</div>
</template>
<script>
export default {
props:{
a:{
type:Number,
required:true,
},
b:{
type:Number,
required:true,
}
},
methods: {
change(){
// $emit触发update事件修改接收的值 父组件中跟着变化
this.$emit('update:a',this.a+1)
this.$emit('update:b',this.b+1)
}
}
}
</script>
当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:
这样会把 doc
对象中的每一个 property (如 title
) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on
监听器。
父子组件中使用.sync通信,子组件用props接收传递的值(绑定的属性名自己决定) 在子组件中通过$emit方法触发’update:属性名’事件 实现父组件子组件值同时变化
$emit('update:属性名',要修改的值)
v-model 和 .sync 区别
都是为了实现数据的“双向绑定”,本质上,也都不是真正的双向绑定,而是语法糖。
1、v-model只能用一次;.sync可以有多个。
2、格式不同。 v-model=“num”, :num.sync=“num”
个人理解
语义区别:prop.sync表示这个子组件会修改父组件的值,v-model表示这是个表单类型的组件。
v-model一般是表单组件,绑定的是value属性,这个值的双向绑定也不是父组件和子组件的关系,而是view和model的对应关系,因为表单组件的值的变化来自于用户输入
而.sync是指父子组件之间的通信
父组件通过 props 向子组件传递数据,子组件通过 $emit 和父组件通信
props的特点:
父组件代码:
<template>
<div>
<Child :msg="msgData" :fn="myFunction"></Child>
</div>
</template>
<script>
import Child from "../Child";
export default {
name: "parent",
data() {
return {
msgData: "父组件数据"
};
},
components: {
Child,
},
methods:{
myFunction() {
console.log("vue");
}
}
};
</script>
子组件代码:
<template>
<div>
<h3>{{msg}}</h3>
<button @click='fn'>点击修改</button>
</div>
</template>
<script>
export default {
name: "child",
props:["msg", "fn"],
}
</script>
第一:不应该在一个子组件内部改变 prop,这样会破坏单向的数据绑定,导致数据流难以理解。如果有这样的需要,可以通过 data 属性接收或使用 computed 属性进行转换。
第二:如果 props 传递的是引用类型(对象或者数组),在子组件中改变这个对象或数组,父组件的状态会也会做相应的更新,利用这一点就能够实现父子组件数据的“双向绑定”,虽然这样实现能够节省代码,但会牺牲数据流向的简洁性,令人难以理解,最好不要这样去做。想要实现父子组件的数据“双向绑定”,可以使用 v-model 或 > .sync。
更多有关props相关内容
$emit的特点:
父组件代码:
<template>
<div>
<Child :fruits="fruitList" @onEmitIndex="onEmitIndex"></Child>
<p>{{currentIndex}}</p>
</div>
</template>
<script>
import Child from "../Child";
export default {
name: "parent",
data() {
return {
currentIndex: 0,
fruitList: ['草莓', '蓝莓', '火龙果']
};
},
components: {
Child,
},
methods:{
onEmitIndex(idx) {
this.currentIndex = idx
}
}
};
</script>
子组件代码:
<template>
<div>
<div v-for="(item, index) in fruits" :key="index" @click="emitIndex(index)">{{item}}</div>
</div>
</template>
<script>
export default {
name: "child",
props:["fruits"],
methods: {
emitIndex(index) {
this.$emit('onEmitIndex', index) // 触发父组件的方法,并传递参数index
}
}
}
</script>
ref 给元素或子组件注册引用信息,通过这个引用信息可以直接访问这个子组件的实例。
当父组件中需要主动获取子组件中的数据或者方法时,可以使用 $ref 来获取。
换一种说法:
1、如果ref用在子组件上,指向的是组件实例,可以理解为对子组件的索引,通过$ref可能获取到在子组件里定义的属性和方法。
2、如果ref在普通的 DOM 元素上使用,引用指向的就是 DOM 元素,通过$ref可能获取到该DOM 的属性集合,轻松访问到DOM元素,作用与JQ选择器类似。
父组件代码:
<template>
<div>
<Child ref="child"></Child>
</div>
</template>
<script>
import Child from "../Child";
export default {
name: "parent",
components: {
Child,
},
mounted(){
console.log(this.$refs.child.name); // JavaScript
this.$refs.child.sayHello(); // hello
}
};
</script>
子组件代码:
export default {
name: "child",
data(){
return {
name: 'JavaScript'
}
},
methods: {
sayHello () {
console.log('hello')
}
}
}
</script>
1、$refs 是作为渲染结果被创建的,所以在初始渲染的时候它还不存在,此时无法无法访问。
2、$refs 不是响应式的,只能拿到获取它的那一刻子组件实例的状态,所以要避免在模板和计算属性中使用它。
父组件代码:
<template>
<div>
<div>{{msg}}</div>
<Child></Child>
<button @click="change">点击改变子组件值</button>
</div>
</template>
<script>
import Child from "../Child";
export default {
name: "parent",
components: {
Child,
},
data() {
return {
msg: 'Welcome'
}
},
methods: {
change() {
// 获取到子组件
this.$children[0].message = 'JavaScript'
}
}
};
</script>
子组件代码:
<template>
<div>
<span>{{message}}</span>
<p>获取父组件的值为: {{parentVal}}</p>
</div>
</template>
<script>
export default {
name: "child",
data(){
return {
message: 'Vue'
}
},
computed:{
parentVal(){
return this.$parent.msg;
}
}
}
</script>
1、通过 $parent 访问到的是上一级父组件的实例,可以使用 $root 来访问根组件的实例
2、在组件中使用$children拿到的是所有的子组件的实例,它是一个数组,并且是无序的
3、在根组件 #app 上拿 $parent 得到的是 new Vue()的实例,在这实例上再拿 $parent 得到的是undefined,而在最底层的子组件拿 $children 是个空数组
4、$children 的值是数组,是直接儿子的集合,关于具体是第几个儿子,那么儿子里面有个 _uid 属性,可以知道他是第几个元素,是元素的唯一标识符,根据这个属性,我们可以进行其他的操作,而 $parent是个对象
注意: 是根组件,不是父组件。$root只对根组件有用。
<div id="app">
<child></child>
</div>
Vue.component('child', {
mounted() {
this.$root.fun('子组件的参数')
},
template:``
})
new Vue({
el: '#app',
data: {
msg: '我是app内的msg'
},
methods: {
fun (str) {
console.log(this.msg + str) // 我是app内的msg 子组件的参数
}
}
})
对于比较小型的项目,没有必要引入 vuex 的情况下,可以使用 eventBus。相比我们上面说的所有通信方式,eventBus 可以实现任意两个组件间的通信。事件总线 ,即 EventBus
来通信。
初始化(首先需要创建事件总线并将其导出,以便其它模块可以使用或者监听它。)
// utils/event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
// main.js
Vue.prototype.$EventBus = new Vue()
注意,这种方式(2)初始化的EventBus是一个全局的事件总线。
假设你有两个Vue页面需要通信: A 和 B ,A页面 在按钮上面绑定了点击事件,发送一则消息,通知 B页面。
PageA.vue
<template>
<button @click="sendMsg()">-</button>
</template>
<script>
import { EventBus } from "../../utils/event-bus";
export default {
methods: {
sendMsg() {
EventBus.$emit("message", '来自A页面的消息');
// EventBus.$emit('message',data)
// 这个message是一个自定义的事件名称,data就是你要传递的数据。
}
}
};
</script>
接下来,我们需要在 B页面 中接收这则消息。
PageB.vue
<template>
<p>{{msg}}</p>
</template>
<script>
import { EventBus } from "../../utils/event-bus";
export default {
data(){
return {
msg: ''
}
},
mounted() {
EventBus.$on("message", (msg) => {
// A发送来的消息
this.msg = msg;
});
}
};
</script>
注意:发送和监听的事件名称必须一致,msg就是获取的数据。第一个参数是事件名,第二个参数是方法
同理我们也可以在 B页面 向 A页面 发送消息。这里主要用到的两个方法:
// 发送消息
EventBus.$emit(channel: string, callback(payload1,…))
// 监听接收消息
EventBus.$on(channel: string, callback(payload1,…))
如果想移除事件的监听,可以像下面这样操作:
import { EventBus } from "../../utils/event-bus";
beforeDestroy(){
EventBus.$off('message',{})
}
在vue生命周期beforeDestroy或者destroyed中用vue实例的$off方法清除eventBus
它的工作原理是发布/订阅方法,通常称为 Pub/Sub 。
// main.js
import Vue from 'vue'
// 创建事件总线 就相当于创建了一个新的vue实例
const bus = new Vue()
// 把bus挂载到了Vue的原型上, 保证所有的组件都能通过 this.$bus访问到事件总线
Vue.prototype.$bus = bus
// this.$bus.$emit('事件名', 额外参数)
this.$bus.$emit('sendMsg', 'hello')
// 1. 在created中订阅
// 2. 回调函数需要写成箭头函数
// this.$bus.$on('事件名', 事件回调函数)
this.$bus.$on('sendMsg', msg => {
console.log(msg)
})
同时也可以使用this. b u s . bus. bus.off(‘sendMsg’)来移除事件监听。
参考:Vue事件总线(EventBus)、 o n 、 on、 on、emit、$off
使用EventBus的好处在于:
相对于状态管理,缺点也很明显
中大型项目都不推荐用EventBus,建议用vuex做状态管理,方便日后维护。
小型项目,涉及到多处跨组件通信的情况,可以考虑使用。
先让我们来想一种情况,就是组件A跟组件C怎么通信,我们可以有多少中解决方案?
正常情况下:父组件通过v-bind绑定一个数据传递给子组件,子组件通过props接收到就可以在子组件的html中使用了。但是,如果父组件v-bind传递给子组件,子组件没有用props接收呢?
注意:这个时候,父组件传递过来的数据就会被挂载(赋值)到这个子组件自带的对象$attrs上面,
所以: $attrs就是一个容器对象,这个容器对象会存放父组件传过来的且子组件未使用props声明接收的数据
爷组件代码
在爷组件中,我们给父组件传递4个数据,msg1、msg2、msg3、msg4,其数据类型分别是字符串、字符串、数组、对象
<template>
<div id="app">
我是爷组件
<fu
:msg1="msg1"
:msg2="msg2"
:msg3="msg3"
:msg4="msg4"
></fu>
</div>
</template>
<script>
import fu from "./views/fu.vue";
export default {
components: {
fu,
},
data() {
return {
msg1: "孙悟空",
msg2: "猪八戒",
msg3: ["白骨精", "玉兔精", "狐狸精"],
msg4: {
name: "炎帝萧炎",
book: "斗破苍穹",
},
};
},
};
</script>
<style scoped>
#app {
width: 950px;
height: 600px;
box-sizing: border-box;
border: 3px dashed #e9e9e9;
background-color: #cde;
margin: 50px;
}
</style>
父组件代码
在父组件中我们只在props中接收msg1,另外三个我们不在props中接收。于是另外三个未在props中接收的,会自动被存放在 a t t r s 这个容器对象中去。同时,我们通过 attrs 这个容器对象中去。同时,我们通过 attrs这个容器对象中去。同时,我们通过attrs对象也可以拿到对应的爷组件中传递过来的,未在props中接收的数据值,也可以在html中使用。
<template>
<div class="fatherClass">
我是父组件
<h2>{{ msg1 }}</h2>
<h2>{{ $attrs.msg2}}</h2>
<h2>{{ $attrs.msg3}}</h2>
<h2>{{ $attrs.msg4}}</h2>
</div>
</template>
<script>
export default {
name: "DemoFather",
props: {
msg1: {
type: String,
default: "",
},
},
mounted() {
console.log('fu组件实例',this);
},
};
</script>
<style lang="less" scoped>
.fatherClass {
width: 850px;
height: 400px;
background-color: #baf;
margin-left: 50px;
margin-top: 50px;
}
</style>
祖孙之间的数据传递,需要通过中间的父组件$attrs做一个桥梁。
孙组件代码
<template>
<div class="sunClass">
我是孙子组件
<h2>接收爷组件数据:-->{{ msg2 }}</h2>
<h2>接收爷组件数据:-->{{ msg3 }}</h2>
<h2>接收爷组件数据:-->{{ msg4 }}</h2>
</div>
</template>
<script>
export default {
// $attrs一般搭配interitAttrs 一块使用
inheritAttrs: false,
// 默认会继承在html标签上传递过来的数据,类似href属性的继承
/*
孙子组件通过props,就能接收到父组件传递过来的$attrs了,就能拿到里面的数据了,也就是:
爷传父、父传子。即:祖孙之间的数据传递。
*/
props: {
msg2: {
type: String,
default: "",
},
msg3: {
type: Array,
default: () => {
return [];
},
},
msg4: {
type: Object,
default: () => {
return {};
},
},
},
name: "DemoSun",
};
</script>
<style lang="less" scoped>
.sunClass {
width: 750px;
height: 180px;
background-color: #bfa;
margin-top: 80px;
margin-left: 50px;
}
</style>
使用$listeners
可以实现孙组件的数据传递到爷组件中去,逻辑的话,也是用在中间的桥梁父组件上面去,我的理解就是$listeners
可以将子组件emit的方法通知到爷组件。代码如下:
第一步,在中间的父组件中加上$listenners
<sun v-bind="$attrs" v-on="$listeners"></sun>
第二步,爷组件中定义事件方法
<template>
<div id="app">
我是爷组件
<h3>{{ fromSunData }}</h3>
<fu :msg1="msg1" :msg2="msg2" :msg3="msg3" :msg4="msg4"
@fromSun="fromSun">
</fu>
</div>
</template>
<script>
import fu from "./views/fu.vue";
export default {
components: {
fu,
},
data() {
return {
msg1: "孙悟空",
msg2: "猪八戒",
msg3: ["白骨精", "玉兔精", "狐狸精"],
msg4: {
name: "炎帝萧炎",
book: "斗破苍穹",
},
fromSunData: "",
};
},
methods: {
fromSun(payload) {
console.log("孙传祖", payload);
this.fromSunData = payload;
},
},
};
</script>
第三步,孙组件去触发爷组件的事件方法即可
<template>
<div class="sunClass">
我是孙子组件
<h2>接收爷组件:-->{{ msg2 }}</h2>
<h2>接收爷组件:-->{{ msg3 }}</h2>
<h2>接收爷组件:-->{{ msg4 }}</h2>
<el-button size="small" type="primary" plain @click="sendToZu">孙传祖</el-button>
</div>
</template>
<script>
export default {
// $attrs一般搭配interitAttrs 一块使用
inheritAttrs: false, // 默认会继承在html标签上传递过来的数据,类似href属性的继承
props: {
msg2: {
type: String,
default: "",
},
msg3: {
type: Array,
default: () => {
return [];
},
},
msg4: {
type: Object,
default: () => {
return {};
},
},
},
name: "DemoSun",
data() {
return {
data: "来自孙组件的数据",
};
},
methods: {
sendToZu() {
// 孙组件能够触发爷组件的fromSun方法的原因还是因为父组件中有一个$listeners作为中间人,去转发这个事件的触发
this.$emit("fromSun", this.data);
},
},
};
</script>
参考:详细讲解vue中祖孙组件间的通信之使用$attrs
和$listeners
的方式
provide 和 inject 需要在一起使用(成对使用)
,它可以使一个祖先组件向其所有子孙后代注入一个依赖,可以指定想要提供给后代组件的数据/方法,不论组件层次有多深,都能够使用。
简单的来说就是在父组件中通过provider来提供变量,然后在子组件中通过inject来注入变量
<!--祖先组件-->
<script>
export default {
provide () {
return {
author: 'yushihu',
}
},
data() {},
}
</script>
<!--子孙组件-->
<script>
export default {
inject: ['author'],
created() {
console.log('author', this.author) // => yushihu
},
}
</script>
provide 和 inject 绑定不是响应的,它被设计是为组件库和高阶组件服务的,平常业务中的代码不建议使用。 因为数据追踪比较困难,不知道那一层级声明了 provide 又或是哪些层级使用了 inject 。造成比较大的维护成本。
Vue $dispatch 和 $broadcast 详解
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
Vuex各个模块:
vuex官方文档