父子传值
-
v-model
-
prop:父传子
-
$emit: 子触发父方法,并传参
-
style和class
父组件可以向子组件传递style和class,它们会合并到子组件的根元素中 -
.natvie修饰符
在注册事件时,父组件可以使用native
修饰符,将事件注册到子组件的根元素上 -
.sync:父子互传
-
$parent
可以在子组件中访问父实例的数据。 -
$children
可以在父组件中访问子实例的数据。 -
ref
可以在父组件中访问子实例的数据。
$refs 只会在组件渲染完成之后生效,并且它们不是响应式的
父子孙传值
-
$attrs
祖先组件传递数据给子孙组件时,可以利用$attrs
传递。
$attrs
的真正目的是撰写基础组件,将非Prop特性赋予某些DOM元素。 -
$listeners
可以在子孙组件中执行祖先组件的函数,从而实现数据传递。
$listeners的真正目的是将所有的事件监听器指向这个组件的某个特定的子元素。 -
$root
可以在子组件中访问根实例的数据。 -
provide & inject
祖先组件提供数据(provide),子孙组件按需注入(inject)。
可以通过子孙组件注入祖先组件方法的方式向祖先组件传参
跨组件传值
-
store
模式 -
eventbus
组件通知事件总线发生了某件事,事件总线通知其他监听该事件的所有组件运行某个函数 -
router
如果一个组件改变了地址栏,所有监听地址栏的组件都会做出相应反应
最常见的场景就是通过点击router-link
组件改变了地址,router-view
组件就渲染其他内容 -
vuex
适用于大型项目的数据仓库(专篇讲解)
Prop – 父向子单向传参
注意:在组件中,属性是只读的,绝不可以更改,这叫做单向数据流
命名规范
HTML 中的特性名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。故:当 传递的prop为 短横线分隔命名时,组件内 的props 应为 驼峰命名 。
如:
<div id="app">
<video-item
sub-title="hello!"
:poster="poster"
play="638000"
rank="1207"
></video-item>
</div>
Vue.component('video-item', {
props: ['subTitle', 'poster', 'play', 'rank'],
template: `<div>{{ title }}</div>`
})
传递一个对象的所有属性
如果你想要将一个对象的所有属性都作为 prop 传入,你可以使用不带参数的 v-bind 。例如,对于一个给定的对象 person:
person: {
name: 'shanshan',
age: 18
}
传递全部属性:
<my-component v-bind="person"></my-component>
上述代码等价于:
<my-component
:name="person.name"
:age="person.age"
></my-component>
对prop传参进行验证
Vue.component('my-component', {
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise
}
})
上述代码中,对prop进行了基础的类型检查,类型值可以为下列原生构造函数中的一种:String
、Number
、Boolean
、Array
、Object
、Date
、Function
、Symbol
、任何自定义构造函数、或上述内容组成的数组。
需要注意的是null
和 undefined
会通过任何类型验证。
除基础类型检查外,我们还可以配置高级选项,对prop进行其他验证,如:类型检测、自定义验证和设置默认值。
如:
Vue.component('my-component', {
props: {
title: {
type: String, // 检查 prop 是否为给定的类型
default: '杉杉最美', // 为该 prop 指定一个默认值,对象或数组的默认值必须从一个工厂函数返回,如:default () { return {a: 1, b: 10} },
required: true, // 定义该 prop 是否是必填项
validator (prop) { // 自定义验证函数,该prop的值会作为唯一的参数代入,若函数返回一个false的值,那么就代表验证失败
return prop.length < 140;
}
}
}
})
子组件中改变prop值常见操作:
- prop值只是作为一个初始值,子组件会在后续操作中改变改值。这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值:
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
- prop 值传入后,子组件需对其进行转换后再使用,这种情况下,最好使用这个 prop 的值来定义一个计算属性:
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
attribute
如果父组件传递了一些属性到子组件,但子组件并没有通过Prop声明这些属性,则它们称之为attribute
,这些属性会直接附着在子组件的根元素上
可以通过v-bind
实现父对子、父对孙… 的数据传递
不包括
style
和class
,它们会被特殊处理
// 父组件:
<template>
<div id="app">
<h1>App</h1>
<FirstLevel
data-a="1"
data-b="2"
msg="Hello world"></FirstLevel>
</div>
</template>
<script>
import FirstLevel from './components/FirstLevel.vue'
export default {
name: 'App',
components: {
FirstLevel
}
}
// 子组件:
<template>
<div class="first">
<h1>First</h1>
<second-level></second-level>
</div>
</template>
<script>
import SecondLevel from './SecondLevel.vue';
export default {
components: { SecondLevel },
name: 'FirstLevel',
props: [ 'dataA' ],
data() {
return {
};
},
created() {
console.log("$attrs", this.$attrs); // { "data-b": "2", msg: "Hello world" }
}
}
</script>
</script>
渲染结果:没有声明的属性附着在了根节点上
子组件可以通过inheritAttrs: false
配置,禁止将attribute
附着在子组件的根元素上,但不影响通过$attrs
获取
// 子组件
<template>
<div class="first">
<h1 v-bind="$attrs">First</h1>
<second-level></second-level>
</div>
</template>
<script>
import SecondLevel from './SecondLevel.vue';
export default {
name: 'FirstLevel',
components: { SecondLevel },
inheritAttrs: false,
}
</script>
渲染结果:
$listeners
$listeners
是vue
的一个实例属性,它用于获取父组件传过来的所有事件函数
一般用于当你想要在一个自定义组件的根元素上直接监听内部某个组件的原生事件时
我们可以配合v-on="\$listeners"
将所有的事件监听器指向组件的特定子元素
// 父组件
<template>
<div id="app">
<h1>App</h1>
<FirstLevel @focus="getFocus" @blur="blur"></FirstLevel>
</div>
</template>
<script>
import FirstLevel from './components/FirstLevel.vue'
export default {
name: 'App',
components: {
FirstLevel
},
methods: {
getFocus: function() {
console.log("getFocus");
},
blur: function() {
console.log("blur");
},
}
}
</script>
// 子组件
<template>
<div class="first">
<h1>First</h1>
<input type="text" v-on="$listeners"/>
<second-level></second-level>
</div>
</template>
<script>
import SecondLevel from './SecondLevel.vue';
export default {
name: 'FirstLevel',
components: { SecondLevel },
}
</script>
$emit
子组件可以向父组件方法中传参,使用 $emit 的第二个参数来提供这个值
不同于组件和prop,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所有的名称。
推荐始终使用 kebab-case 的事件名
// 父组件
<template>
<div id="app">
<h1>App</h1>
<div>{{val}}</div>
<FirstLevel @inputText="inputText"></FirstLevel>
</div>
</template>
<script>
import FirstLevel from './components/FirstLevel.vue'
export default {
name: 'App',
components: {
FirstLevel
},
data: function() {
return {
val: '',
}
},
methods: {
inputText: function (childValue) {
this.val = childValue;
},
}
}
</script>
// 子组件
<template>
<div class="first">
<h1>First</h1>
<input type="text" @input="input"/>
<second-level></second-level>
</div>
</template>
<script>
import SecondLevel from './SecondLevel.vue';
export default {
name: 'FirstLevel',
components: { SecondLevel },
methods: {
input: function() {
this.$emit('inputText', document.getElementsByTagName("input")[0].value);
}
}
}
</script>
子组件调用父组件传参后,希望得到父组件方法的返回值并做操作,可以传递回调函数
// 父组件
<template>
<div id="app">
<h1>App</h1>
<div>{{val}}</div>
<FirstLevel @inputText="inputText"></FirstLevel>
</div>
</template>
<script>
import FirstLevel from './components/FirstLevel.vue'
export default {
name: 'App',
components: {
FirstLevel
},
data: function() {
return {
val: '',
}
},
methods: {
inputText: function (childValue, callback) {
this.val = childValue;
callback(this.val.split('').join(" - "));
},
}
}
</script>
// 子组件
<template>
<div class="first">
<h1>First</h1>
<input type="text" @input="input"/>
<div>{{back}}</div>
<second-level></second-level>
</div>
</template>
<script>
import SecondLevel from './SecondLevel.vue';
export default {
name: 'FirstLevel',
components: { SecondLevel },
data() {
return {
back: '',
}
},
methods: {
input: function() {
this.$emit('inputText', document.getElementsByTagName("input")[0].value, (data) => {
this.back = data;
});
}
}
}
</script>
$emit
和$listeners
通信的异同
相同点:均可实现子组件向父组件传递消息
差异点:
$emit
更加符合单向数据流,子组件仅发出通知,由父组件监听做出改变;而$listeners
则是在子组件中直接使用了父组件的方法。- 调试工具可以监听到子组件
$emit
的事件,但无法监听到$listeners
中的方法调用。(想想为什么)- 由于
$listeners
中可以获得传递过来的方法,因此调用方法可以得到其返回值。但$emit
仅仅是向父组件发出通知,无法知晓父组件处理的结果
对于上述中的第三点,可以在$emit
中传递回调函数来解决
style
和class
父组件可以向子组件传递style
和class
,它们会合并到子组件的根元素中
示例
父组件
<template>
<div id="app">
<HelloWorld
style="color:red"
class="hello"
msg="Welcome to Your Vue.js App"
/>
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
export default {
components: {
HelloWorld,
},
};
</script>
子组件
<template>
<div class="world" style="text-align:center">
<h1>{{msg}}</h1>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String,
},
};
</script>
渲染结果:
<div id="app">
<div class="hello world" style="color:red; text-aling:center">
<h1>Welcome to Your Vue.js App</h1>
</div>
</div>
natvie
修饰符
在注册事件时,父组件可以使用native
修饰符,将事件注册到子组件的根元素上
父组件
<template>
<div id="app">
<HelloWorld @click.native="handleClick" />
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
export default {
components: {
HelloWorld,
},
methods: {
handleClick() {
console.log(1);
},
},
};
</script>
子组件
<template>
<div>
<h1>Hello World</h1>
</div>
</template>
渲染结果
<div id="app">
<!-- 点击该 div,会输出 1 -->
<div>
<h1>Hello World</h1>
</div>
</div>
sync
修饰符
和v-model
的作用类似,用于双向绑定,不同点在于v-model
只能针对一个数据进行双向绑定,而sync
修饰符没有限制
// 父组件
<template>
<div id="app">
<h1>App</h1>
<div>{{"n1:" + n1 + " n2:" + n2}}</div>
<FirstLevel :num1.sync="n1" :num2.sync="n2"></FirstLevel>
<!-- 等同于 -->
<!--<FirstLevel
:num1="n1"
@update:num1="n1 = $event"
:num2="n2"
@update:num2="n2 = $event"></FirstLevel>-->
</div>
</template>
<script>
import FirstLevel from './components/FirstLevel.vue'
export default {
name: 'App',
components: {
FirstLevel
},
data: function() {
return {
n1: 0,
n2: 0,
}
},
}
</script>
// 子组件
<template>
<div class="first">
<h1 @click="oClick">First</h1>
<div>{{"num1:" + num1 + " num2:" + num2}}</div>
<second-level></second-level>
</div>
</template>
<script>
import SecondLevel from './SecondLevel.vue';
export default {
name: 'FirstLevel',
components: { SecondLevel },
props: ['num1', 'num2'],
methods: {
oClick: function() {
this.$emit(`update:num1`, this.num1 + 1);
this.$emit(`update:num2`, this.num2 + 1)
}
}
}
</script>
$root
在每个子组件中,可以通过 $root 访问根实例。
// Vue 根实例
new Vue({
data: function() {
return {
n1: 0,
n2: 0,
}
},
computed: {
cpt1() {
return this.n1 +1;
}
},
methods: {
fun: function() {
console.log("fun");
}
},
render: h => h(App),
}).$mount('#app')
所有的子组件都可以将这个实例作为一个全局 store 来访问或使用。
// 获取根组件的数据
this.$root.n1
// 写入根组件的数据
this.$root.n2 = 2
// 访问根组件的计算属性
this.$root.cpt1
// 调用根组件的方法
this.$root.fun()
$parent 及 $children
$parent
在子组件中,可以通过 $parent 访问 父组件实例。这可以替代将数据以prop的方式传入子组件的方式。
若祖先组件需要共享一个属性 data,它的所有子元素都需要访问 data 属性,在这种情况下 其子节点可以通过层层套用 $parent 的方式来找到 data。
<template>
<div id="app">
<h1>App</h1>
<FirstLevel></FirstLevel>
</div>
</template>
<script>
import FirstLevel from './components/FirstLevel.vue'
export default {
name: 'App',
components: {
FirstLevel
},
data() {
return {
data: 'Hello world',
}
}
}
</script>
// 子节点
<template>
<div class="third">
<h1>Third</h1>
<div>{{msg}}</div>
</div>
</template>
<script>
export default {
name: 'ThirdLevel',
data() {
return {
msg:'',
}
},
created() {
this.msg = this.$parent.data || this.$parent.$parent.data || this.$parent.$parent.$parent.data;
}
}
</script>
这种方式会使应用更难调试和理解,且很难找出变更是从哪里发起的。
上述情况下,可以使用依赖注入解决。
依赖注入provide 和 inject
provide 选项允许我们指定想要提供给后代组件的数据/方法
然后再任何后代组件中,我们都可以使用 inject 选项来接受指定想要添加在实例上的属性。
// 祖先组件
<template>
<div id="app">
<h1>App</h1>
<FirstLevel></FirstLevel>
</div>
</template>
<script>
import FirstLevel from './components/FirstLevel.vue'
export default {
name: 'App',
components: {
FirstLevel
},
provide() {
return {
share: 'China',
abs: function(val) {
console.log(val);
}
}
}
}
</script>
// 子组件
<template>
<div class="third">
<h1>Third</h1>
<div>{{msg}}</div>
</div>
</template>
<script>
export default {
name: 'ThirdLevel',
data() {
return {
msg:'',
}
},
inject: ["share", "abs"],
created() {
this.msg = this.share;
this.abs(this.msg);
}
}
</script>
相比 $parent 来说,这个用法可以让我们在任意后代组件中访问share,而不需要暴露整个祖先组件的实例。同时这些组件之间的接口是始终明确定义的,就和 props 一样。
我们可以把依赖注入看作一部分“大范围有效的 prop”,除了:
- 祖先组件不需要知道哪些后代组件使用它提供的属性
- 后代组件不需要知道被注入的属性来自哪里
然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的属性是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用 $root做这件事都是不够好的。如果你想要共享的这个属性是你的应用特有的,而不是通用化的,或者如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像 Vuex 这样真正的状态管理方案了。
$refs
ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的
r
e
f
s
对
象
上
。
如
果
在
普
通
的
D
O
M
元
素
上
使
用
,
引
用
指
向
的
就
是
D
O
M
元
素
;
如
果
用
在
子
组
件
上
,
引
用
就
指
向
组
件
实
例
:
注
意
:
‘
refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例: 注意:`
refs对象上。如果在普通的DOM元素上使用,引用指向的就是DOM元素;如果用在子组件上,引用就指向组件实例:注意:‘refs只会在组件渲染完成之后生效,并且它们不是响应式的。应该避免在模板或计算属性中访问
$refs` 。
<template>
<div id="app">
<h1 ref="title">App</h1>
<FirstLevel ref="firstApp"></FirstLevel>
</div>
</template>
<script>
import FirstLevel from './components/FirstLevel.vue'
export default {
name: 'App',
components: {
FirstLevel
},
created() {
// 只能访问到当前组件中的ref,子组件中的访问不到
console.log(this.$refs);
}
}
当ref 和 v-for 一起使用时,得到的引用将会是一个包含了对应数据源的这些子组件的数组。
当遍历元素或组件时,如:
[1, 2, 3].map(item => <div ref="xx" key={ item }>{ item }</div>)
会发现从 this.$refs.xxx 中获取的并不是期望的数组值,此时就需要将refInFor属性设置为true了:
[1, 2, 3].map(item => <div ref="xx" refInFor={true} key={item}>{ item }</div>)
store
模式
适用于中小型项目的数据仓库
// store.js
const store = {
loginUser: ...,
setting: ...
}
// compA
const compA = {
data(){
return {
loginUser: store.loginUser
}
}
}
// compB
const compB = {
data(){
return {
setting: store.setting,
loginUser: store.loginUser
}
}
}
程序化的事件侦听器
除了 v-on 和 $emit 外, Vue 实例在其事件接口中还提供了其它的方法。我们可以:
- 通过 $on(eventName, eventHandler) 侦听一个事件
- 通过 $once(eventName, eventHandler) 一次性侦听一个事件
- 通过 $off(eventName, eventHandler) 停止侦听一个事件
这几个方法一般不会被用到,但是,当需要在一个组件实例上手动侦听事件时,他们是可以派的上用场的