目录
一、通过 $parent 和 $root 获取父/根组件实例
一、通过 $parent 和 $root 获取父/根组件实例
在子组件中,想要获取到父组件、根组件的实例,可以直接通过 $parent 和 $root 获取。
比如在父组件中有如下数据:
import ChildView from '../ChildView.vue'
export default {
components: {
ChildView
},
data() {
return {
msg: 'Hello'
}
},
methods: {
get() {
console.log("Hello world")
}
}
}
在子组件 ChildView 中获取:
<template>
<div>{{$parent.msg}}</div>
</template>
<script>
export default {
created() {
this.$parent.get()
}
}
</script>
父组件中的数据也是可读写的
,可以直接修改其值:
<template>
<div>{{$parent.msg}}</div>
<!-- 修改父组件的数据 -->
<button @click="changeMsg"> </button>
</template>
<script>
export default {
inject: ['get'],
created() {
this.$parent.get()
},
methods: {
changeMsg() {
this.$parent.msg = "I'm a child"
}
}
}
</script>
而所谓的根组件,其实就是在创建Vue实例时创建的那一堆数据:
new Vue({
data: {
foo: 1
},
computed: {
bar: function () { /* ... */ }
},
methods: {
baz: function () { /* ... */ }
}
})
在任何子孙组件中都可访问:
// 获取根组件的数据
this.$root.foo
// 写入根组件的数据
this.$root.foo = 2
// 访问根组件的计算属性
this.$root.bar
// 调用根组件的方法
this.$root.baz()
可以归纳如下:
- this 代表当前组件的实例
- this.$parent 代表父组件的实例
- this.$root 代表根组件的实例
二、通过 $refs
获取子组件实例
尽管存在 prop 和事件,有的时候你仍可能需要在 JavaScript 里直接访问一个子组件。为了达到这个目的,你可以通过 ref
这个 attribute 为子组件赋予一个 ID 引用。例如:
<base-input ref="userNameInput"></base-input>
现在在你已经定义了这个 ref
的组件里,你可以使用:
this.$refs.userNameInput
来访问这个 <base-input>
实例,以便不时之需。比如程序化地从一个父级组件聚焦这个输入框。
在刚才那个例子中,该 <base-input>
组件也可以使用一个类似的 ref
提供对内部这个指定元素的访问,例如:
<input ref="input">
甚至可以通过其父级组件定义方法:
methods: {
// 用来从父级组件聚焦输入框
focus: function () {
this.$refs.input.focus()
}
}
这样就允许父级组件通过下面的代码聚焦 <base-input>
里的输入框:
this.$refs.usernameInput.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>
因此,要取出其中的某个元素,需要使用相应的下标:
this.$refs.li[0].innerText
上面的程序看到我是在 mounted
中取出 $refs
的,因为$refs
只会在组件渲染完成之后生效,并且它们不是响应式的。所以应该避免在模板或计算属性中访问 $refs
。
三、通过 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',
isComplete: false
}
然后:
<todo-item v-bind="todo"></todo-item>
将等价于:
<todo-item :text="todo.text" :is-complete="todo.isComplete"></todo-item>
字面量语法 vs 动态语法
初学者常犯的一个错误是使用字面量语法传递数值:
<!-- 传递了一个字符串 "1" -->
<comp some-prop="1"></comp>
因为它是一个字面量 prop,它的值是字符串 "1"
而不是一个数值。如果想传递一个真正的 JavaScript 数值,则需要使用 v-bind
,从而让它的值被当作 JavaScript 表达式计算:
<!-- 传递真正的数值 -->
<comp :some-prop="1"></comp>
单向数据流
Prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是反过来不会。这是为了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得难以理解。
另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop。如果你这么做了,Vue 会在控制台给出警告。
在两种情况下,我们很容易忍不住想去修改 prop 中数据:
- Prop 作为初始值传入后,子组件想把它当作局部数据来用;
- Prop 作为原始数据传入,由子组件处理成其它数据输出。
对这两种情况,正确的应对方式是:
- 定义一个局部变量,并用 prop 的值初始化它:
props: ['initialCounter'], data: function () { return { counter: this.initialCounter } }
- 定义一个计算属性,处理 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 实例都实现了事件接口,即:
- 使用
$on(eventName)
监听事件 - 使用
$emit(eventName)
触发事件
使用 v-on
绑定自定义事件
Vue 的事件系统与浏览器的 EventTarget API 有所不同。尽管它们的运行起来类似,但是 $on
和 $emit
并不是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 事件,并附带数据 ,在父组件中相应的事件监听方法中可以捕捉到传递的数据。
向自定义事件中传入额外参数
有的时候,在某些自定义组件中,通过 $emit
本身就暴露出一些参数的情况下,我们还需要从父组件中传递其他参数,但是如果直接写到方法的参数中会覆盖本身的 $emit
返回的参数。这个时候可以在外面包裹一层箭头函数,在箭头函数体中调用方法并传递额外的参数。
举个例子,在一个组件中通过 $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>
如果使用事件对象 $event
,则只能够获取到从 $emit
传出的第一个参数:
<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>
效果如下:
六、通过 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 起是可配置的):
- 接受一个
value
的 prop - 在有新的值时触发
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>
效果如下:
自定义组件的 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>
效果如下:
七、依赖注入(provide
/ inject
)
除了通过 props
向子组件传值之外,Vue还允许通过 provide
/ 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>
最后在 InjectChild
中取出注入的数据:
<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")
}
}
}
在 InjectView
中取出此方法:
export default {
inject: {
print: 'log',
},
created() {
this.print() // Hello world
}
}
inject
也可返回一个数组,跟 props
差不多:
export default {
inject: ['log'],
created() {
this.log()
}
}