vue组件通信方式及原理分析
# 简介及比较
Vue 组件常用几种种的通信方式:
# 数据是单向数据流;
- props /
$emit
$parent
/$children
与refs
$attrs
/$listeners
- provide / inject
- EventBus中央事件总线
- Vuex状态管理
$emit
/$on
$boradcast
和$dispatch
常见使用场景可以分为三类:
- 父子通信:props / $emit;$parent / $children/ ref;$attrs/$listeners;provide / inject API;
- 兄弟通信:eventBus; Vuex
- 跨级通信:
$emit
/$on
,Vuex;$attrs/$listeners;provide / inject API
组件关系
上面展示的图片可以引入所有 Vue 组件的关系形式:
- A 组件和 B 组件、B 组件和 C 组件、B 组件和 D 组件形成了父子关系
- C 组件和 D 组件形成了兄弟关系
- A 组件和 C 组件、A 组件和 D 组件形成了隔代关系(其中的层级可能是多级,即隔多代)
# 组件通信
# props / $emit
父子组件通信
父组件 A 通过 props 参数向子组件 B 传递数据,B 组件通过 $emit 向 A 组件发送一个事件(携带参数数据),A组件中监听 $emit 触发的事件得到 B 向 A 发送的数据。
- 父组件向子组件传值 (props)
- 子组件向父组件传值(通过事件方式)
// 父组件
<template>
<div>
<span>{{ desc }}</span>
<child1 :msg="msg" @getEmit="getEmit"></child1>
</div>
</template>
<script>
import child1 from "@/components/child1";
export default {
components: { child1 },
data() {
return {
msg: "我是父组件的msg",
desc:"default"
};
},
methods: {
getEmit(data) {
this.desc = data;// 这里的得到了子组件的值
}
}
};
</script>
// 子组件 => child1
<template>
<div>
<span>我是父组件传来的{{ msg }}</span>
<button @click="getEmit"></button>
</div>
</template>
<script>
export default {
props: {
msg: String
},
data() {
return {
//msg: "我是父组件的msg",
childMsg:"我是child1的msg"
};
},
methods: {
getEmit(data) {
this.$emit("getEmit",this.childMsg)
}
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# $parent
/ $children
与 $refs
父子组件通信
$parent
/$children
:访问父 / 子实例$children
:当前实例的子组件,它返回的是一个子组件的集合。如果想获取哪个组件属性和方法,可以通过 this.$children[index].子组件属性/f方法- ref:获取DOM 元素 和 组件实例来获取组件的属性和方法。
- 如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;
- 如果用在子组件上,引用就指向组件实例;
这两种方式都是直接得到组件实例,使用后可以直接调用组件的方法或访问数据。
$parent, $children, $refs示例
// 父组件
<template>
<div>
<child1 ref="sendMsg"></child1>
</div>
</template>
<script>
import child1 from "@/components/child1";
export default {
components: { child1 },
data() {
return {
msg: "我是父组件的msg",
};
},
mounted() {
this.refs.sendMsg.handleSendToChild1(this.msg)
},
methods: {
handle() {
//this.$children是个数组存储所有的子组件
this.$children[0].flag = !this.$children[0].flag;
//通过 $children 来获取 子组件的属性和方法
this.$children[0].getValue(); // 我是 Test1
this.$children[1].getTest2(); //我是 Test2
this.$children[1].datas}; //我是Test2
//可通过给子组件标签加ref快速找到目标子组件
//<childrenbtn ref='button'></childrenbtn> //this.$refs['button']
this.$refs.button.flag = !this.$refs.button.flag;
}
}
};
</script>
// 子组件 => child1
<template>
<div>
<span>{{ msg }} </span>
</div>
</template>
<script>
export default {
data() {
return {
msg: "",
};
},
//子组件 通过$parent来获取父组件实例的属性和方法
created(){
console.log( this.$parent.obj )
this.$parent.getQuery()
},
methods: {
handleSendToChild1(data) {
this.msg = data;
}
},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# ref/refs
官方文档描述小结:
- $ref:注册引用信息属性,用来给元素或子组件注册引用信息。// 目前没有用到
- $refs:一个对象,持有已经注册过ref的所有子组件。
通俗理解:$ref:注册引用信息属性;$refs:注册引用信息对象
注意:refs只会在组件渲染完成之后生效,并且它们不是响应式的。这只意味着一个直接的子组件封装的“逃生舱”——你应该避免在模板或计算属性中访问 $refs 。
(1)普通DOM元素用法;
新版做法:用ref绑定之后,不需要在获取dom节点了,直接在上面的input上绑定ref , 然后通过ref,然后通过ref,然后通过refs.$ref即可调用,这样就可以减少获取dom节点的消耗。
<!-- html代码 -->
<div id="arry">
<!-- 1.注册引用 -->
<div ref="one">蜡笔小新</div>
<button @click="change">点击加背景</button>
</div>
<script>
var arry= new Vue({
el:"#arry",
data:{},
methods:{
change(){
//console.log(this)
// 2.利用refs找到此元素
this.$refs.one.style.backgroundColor='blue'
}
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(2)组件component用法;
案例:利用组件,实现计数器count效果
<!-- html代码 -->
<div id="arry">
<!-- 1.注册引用 -->
<my-count ref="one" @pass-parent="changeparent"></my-count>
<my-count ref="two" @pass-parent="changeparent"></my-count>
{{zong}}
</div>
<script>
Vue.component('my-count', {
data() {
return {
childCount: 0
}
},
template: `<button @click=addCount>{{childCount}}</button>`,
methods: {
addCount() {
this.childCount++;
this.$emit('pass-parent')
}
}
})
var arry = new Vue({
el: "#arry",
data: {
samy: 0
},
methods: {
changeparent() {
console.log(this.$refs.one.childCount)
console.log(this.$refs.two.childCount) //this.$refs['current']
this.samy = this.$refs.one.childCount + this.$refs.two.childCount
}
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# $attrs
/ $listeners
父 => 子 => 孙组件通信
$attrs
和$listeners
获取父组件实例属性和方法(组件嵌套情况下使用); 两者结合使用做到透明组件功能;
其中子组件只做数据传递 多级组件嵌套需要传递数据时,如果仅仅是传递数据,而不做中间处理,可以使用这种方法;
- $attrs:没有被子组件非属性prop;
- $listeners:子组件可以触发父组件的 非.native事件;
分析
- 使用 Vuex 来进行数据管理,但是使用的 vuex 的问题在于,如果项目比较小,组件间的共享状态比较少,那用 vuex 就好比杀鸡用牛刀。
- 利用 B 组件做中转站,当 A 组件需要把信息传给 C 组件时,B 接受 A 组件的信息,然后用 props 传给 C 组件, 但是如果嵌套的组件过多,会导致代码繁琐,代码维护比较困难;如果 C 中状态的改变需要传递给 A, 还要使用事件系统一级级往上传递 。
在 Vue2.4 中,为了解决该需求,引入了attrs 和listeners , 新增了 inheritAttrs 选项; ($attrs 的作用,某些情况下需要结合 inheritAttrs 一起使用)
$attrs
:包含了父作用域中不被认为 (且不预期为)props
的特性绑定 (class 和 style 除外),并且可以通过 v-bind=”$attrs
” 传入内部组件。当一个组件没有声明任何props
时,它包含所有父作用域的绑定 (class 和 style 除外)。$listeners
:包含了父作用域中的 (不含 .native 修饰符) v-on 事件监听器。它可以通过 v-on=”$listeners
” 传入内部组件。它是一个对象,里面包含了作用在这个组件上的所有事件监听器,相当于子组件继承了父组件的事件。
使用场景
多层嵌套组件的情况下使用,可以避免使用Vuex来做数据处理, **使用v-on="$listeners" v-bind="$attrs"
**很方便达到业务数据传递。
// 父组件
<template>
<div>
<span>{{ desc }}</span>
<child1 :msg="msg" @getEmit="getEmit" v-on="$listeners"></child1>
</div>
</template>
<script>
import child1 from "@/components/child1";
export default {
components: { child1 },
data() {
return {
msg: "我是父组件的msg",
desc:""
};
},
methods: {
getEmit(data) {
this.desc = data;
}
}
};
</script>
// 子组件child1 => 相对于child2组件,该组件为兄弟组件
<template>
<div>
<child2 v-bind="$attrs"></child2>
</div>
</template>
<script>
import child2 from "@/components/child2
export default {
// inheritAttrs:false,不会把未被注册的 prop 呈现为普通的 HTML 属性 ,也就是:prop="data" prop不会作为html的新属性
inheritAttrs: false,
components: { child2 },
data() {
return {
msg: "我是父组件的msg",
childMsg:"我是child1的msg"
};
},
mounted(){
this.$emit("getEmit",this.childMsg);
}
};
// 子组件child2 => 相对比child1组件,该组件为兄弟组件
<template>
<div>
<span>{{ msg }} {{ $attrs.msg }}</span>
</div>
</template>
<script>
export default {
data() {
return {
msg: "该组件拿到父组件通过child1组件传递过来的值 => ",
};
},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
而$listeners: 官方文档说的是:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用;示范【推荐】
<template>
<div class="child1">
<child2 v-on:upRocket="reciveRocket"></child2>
</div>
</template>
<script>
const child2 = () => import("./child2.vue");
export default {
components: {
child2
},
methods: {
reciveRocket() {
console.log("reciveRocket success");
}
}
};
</script>
// child2.vue
<template>
<div class="child2">
<child3 v-bind="$attrs" v-on="$listeners"></child3>
</div>
</template>
<script>
const child3 = () => import("./child3.vue");
export default {
components: {
child3
},
created() {
this.$emit('child2', 'child2-data');
}
};
</script>
// child3.vue
<template>
<div class="child3">
<p @click="startUpRocket">child3</p>
</div>
</template>
<script>
export default {
methods: {
startUpRocket() {
this.$emit("upRocket");
console.log("startUpRocket");
}
}
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
这里的结果是,当我们点击 child3 组件的 child3 文字,触发 startUpRocket 事件,child1 组件就可以接收到,并触发 reciveRocket 打印结果如下:
> reciveRocket success
> startUpRocket
2
# provide / inject
父 => 子、父 => 孙组件通信
provide/inject 是 Vue2.2.0 新增 API,这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。
provide
和 inject
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
provide
:
- 是一个对象或返回一个对象的函数
- 该对象包含可注入其子孙的属性。
inject
:
- 是一个字符串数组 或者是一个对象
- 用来在子组件或者子孙组件中注入
provide
提供的父组件属性。
使用场景
provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。
provide/inject可以轻松实现跨级访问父组件的数据;
$attrs
是为了解决什么问题出现的?应用场景有哪些?provide/inject 不能解决它能解决的问题吗?
$attrs主要的作用就是实现批量传递数据。provide/inject更适合应用在插件中,主要是实现跨级数据传递
// 父组件
<template>
<div>
<span>{{ desc }}</span>
<child1 :msg="msg"></child1>
</div>
</template>
<script>
import child1 from "@/components/child1";
export default {
provide () {
return {
desc:this.desc //这里不接收动态传参,但是以直接传this过去,实现动态响应式
titleFather: '父组件的值'
reload: this.reload,
}
},
components: { child1 },
data() {
return {
msg: "我是父组件的msg",
desc:"我是父组件的desc"
};
},
methods: {
reload () {
this.isRouterAlive = false
this.$nextTick(function () {
this.isRouterAlive = true
})
}
}
};
</script>
// child1部分代码上面已经赘述过多,此处省略
// 孙组件 => grandson1
<template>
<div>
<span>{{ desc }}</span>
</div>
</template>
<script>
export default {
inject:['desc','titleFather'],
created(){
console.log(`${this.titleFather}----`)
},
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# EventBus中央事件总线
兄弟组件通信
// 准备条件可以分为两种方式
// 方式一: npm install vue-bus / main.js中引用
import Vue from 'vue';
import VueBus from 'vue-bus';
Vue.use(VueBus);
//组件中直接使用this.$bus.on()和this.$bus.emit()来做相应的处理
// 方式二:2.创建bus.js,该文件内写入
import Vue from "vue";
export default new Vue();
2
3
4
5
6
7
8
9
// 子组件 => child2 采用了第二种创建bus.js的方法,然后在该组件内引用
<template>
<div>
<button @click="handleSendToChild3">{{ msg }}</button>
</div>
</template>
<script>
import Bus from "@/components/bus.js";
export default {
data() {
return {
msg: "点我向child3组件传参",
childMsg:"我是child1的msg"
};
},
methods: {
handleSendToChild3(data) {
Bus.$emit("getData",this.childMsg)
}
}
};
// 子组件 => child3 采用了第二种创建bus.js的方法,然后在该组件内引用
<template>
<div>
<span>{{ msg }}</span>
</div>
</template>
<script>
import Bus from "@/components/bus.js";
export default {
data() {
return {
msg: "",
};
},
methods: {
handleSendToChild3(data) {// 在mounted中处理监听
Bus.$on("getData",(data)=>{
this.msg = data
})
}
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
其他示例: 直接使用Vue.component
注册组件;
// 组件 A
Vue.component('A', {
template: `
<div>
<p>this is A component!</p>
<input type="text" v-model="mymessage" @input="passData(mymessage)">
</div>
`,
data() {
return {
mymessage: 'hello brother1'
}
},
methods: {
passData(val) {
this.$EventBus.$emit('globalEvent', val) //触发全局事件globalEvent
}
}
});
// 组件 B
Vue.component('B', {
template:`
<div>
<p>this is B component!</p>
<p>组件A 传递过来的数据:{{brothermessage}}</p>
</div>
`,
data() {
return {
mymessage: 'hello brother2',
brothermessage: ''
}
},
mounted() {
//绑定全局事件globalEvent
this.$EventBus.$on('globalEvent', (val) => {
this.brothermessage = val;
});
}
});
//定义中央事件总线
const EventBus = new Vue();
// 将中央事件总线赋值到 Vue.prototype 上,这样所有组件都能访问到了
Vue.prototype.$EventBus = EventBus;
const app = new Vue({
el: '#app',
template: `
<div>
<A />
<B />
</div>
`
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# $emit
/ $on
和EventBus类似;这种方式是通过一个类似 App.vue 的实例作为一个模块的事件中心,用它来触发和监听事件,如果把它放在 App.vue 中,就可以很好的实现任何组件中的通信,但是这种方法在项目比较大的时候不太好维护。
假设现在有 4 个组件,Home.vue 和 A/B/C 组件,AB 这三个组件是兄弟组件,Home.vue 相当于父组件 建立一个空的 Vue 实例,将通信事件挂载在该实例上 -
//D.js
import Vue from 'vue'
export default new Vue()
// 我们可以在router-view中监听change事件,也可以在mounted方法中监听
// home.vue
<template>
<div>
<child-a />
<child-b />
<child-c />
</div>
</template>
// A组件
<template>
<p @click="dataA">将A组件的数据发送给C组件 - {{name}}</p>
</template>
<script>
import Event from "./D";
export default {
data() {
return {
name: 'Echo'
}
},
components: { Event },
methods: {
dataA() {
Event.$emit('data-a', this.name);
}
}
}
</script>
// B组件
<template>
<p @click="dataB">将B组件的数据发送给C组件 - {{age}}</p>
</template>
<script>
import Event from "./D";
export default {
data() {
return {
age: '18'
}
},
components: { Event },
methods: {
dataB() {
Event.$emit('data-b', this.age);
}
}
}
</script>
// C组件
<template>
<p>C组件得到的数据 {{name}} {{age}}</p>
</template>
<script>
import Event from "./D";
export default {
data() {
return {
name: '',
age: ''
}
},
components: { Event },
//在 C 组件的 mounted 事件中监听了 A/B 的 $emit 事件,并获取了它传递过来的参数(由于不确定事件什么时候触发,所以一般在 mounted / created 中监听)
mounted() {
Event.$on('data-a', name => {
this.name = name;
})
Event.$on('data-b', age => {
this.age = age;
})
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# $boradcast
和 $dispatch
$boradcast
和 $dispatch
这也是一对成对出现的方法,不过只是在 Vue1.0
中提供了,而 Vue2.0
被废弃了,但是还是有很多开源软件都自己封装了这种组件通信的方式,比如 Mint UI (opens new window)、Element UI (opens new window) 和 iView (opens new window) 等。
// broadcast 方法的主逻辑处理方法
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
const name = child.$options.componentName;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat(params));
}
});
}
export default {
methods: {
// 定义 dispatch 方法
dispatch(componentName, eventName, params) {
let parent = this.$parent;
let name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
// 定义 broadcast 方法
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
上面所示的代码,一般都作为一个 mixins
去混入使用, broadcast
是向特定的父组件触发事件,dispatch
是向特定的子组件触发事件,本质上这种方式还是 on
和 emit
的封装,在一些基础组件中都很实用。
# Vuex状态管理
Vuex 是一个状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 应用的核心是 store(仓库,一个容器),store 包含着应用中大部分的状态 (state);
Vuex
是一个专门为Vue.js
开发状态管理软件。 Vuex中有几个核心的概念
- State 存储状态,类似于data属性
- Getter 从
state
中派生出一些属性,类似于computed - Mutation 更改
state
的属性,state
属性是无法直接通过赋值进行更改的,需要通过Mutation
定义的函数来进行赋值,提交的是state
- Actions
Mutation
必须是同步的函数,所以在Actions中处理异步的操作,并且提交的是Mutation` - Module 当项目很大的时候,需要根据不同的功能分割成不同的模块,每次需要的时候,只需要引用需要的模块即可
# 原理分析
# props实现
src/core/instance/init.js:74
、scr/core/instance/state:64
、src/core/vdom/create-component.js:101
initLifecycle(vm);
/*初始化事件*/
initEvents(vm);
/*初始化render*/
initRender(vm);
/*调用beforeCreate钩子函数并且触发beforeCreate钩子事件*/
callHook(vm, "beforeCreate");
initInjections(vm); // resolve injections before data/props
/*初始化props、methods、data、computed与watch*/
initState(vm);
initProvide(vm); // resolve provide after data/props
/*调用created钩子函数并且触发created钩子事件*/
callHook(vm, "created");
2
3
4
5
6
7
8
9
10
11
12
13
export function initState(vm: Component) {
vm._watchers = [];
const opts = vm.$options;
/*初始化props*/
if (opts.props) initProps(vm, opts.props);
}
/*初始化props*/
function initProps(vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {};
const props = (vm._props = {});
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
/*缓存属性的key,使得将来能直接使用数组的索引值来更新props来替代动态地枚举对象*/
const keys = (vm.$options._propKeys = []);
/*根据$parent是否存在来判断当前是否是根结点*/
const isRoot = !vm.$parent;
// root instance props should be converted
/*根结点会给shouldConvert赋true,根结点的props应该被转换*/
observerState.shouldConvert = isRoot;
for (const key in propsOptions) {
/*props的key值存入keys(_propKeys)中*/
keys.push(key);
/*验证prop,不存在用默认值替换,类型为bool则声称true或false,当使用default中的默认值的时候会将默认值的副本进行observe*/
const value = validateProp(key, propsOptions, propsData, vm);
/* istanbul ignore else */
if (process.env.NODE_ENV !== "production") {
/*判断是否是保留字段,如果是则发出warning*/
if (isReservedProp[key] || config.isReservedAttr(key)) {
warn(
`"${key}" is a reserved attribute and cannot be used as component prop.`,
vm
);
}
defineReactive(props, key, value, () => {
/*
由于父组件重新渲染的时候会重写prop的值,所以应该直接使用prop来作为一个data或者计算属性的依赖
https://cn.vuejs.org/v2/guide/components.html#字面量语法-vs-动态语法
*/
if (vm.$parent && !observerState.isSettingProps) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
);
}
});
} else {
defineReactive(props, key, value);
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
/*Vue.extend()期间,静态prop已经在组件原型上代理了,我们只需要在这里进行代理prop*/
if (!(key in vm)) {
proxy(vm, `_props`, key);
}
}
observerState.shouldConvert = true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# emit事件机制实现
src/core/instance/init.js:74
、src/core/instance/events.js:12
、src/core/vdom/create-component.js:101
/*初始化事件*/
export function initEvents(vm: Component) {
/*在vm上创建一个_events对象,用来存放事件。*/
vm._events = Object.create(null);
/*这个bool标志位来表明是否存在钩子,而不需要通过哈希表的方法来查找是否有钩子,这样做可以减少不必要的开销,优化性能。*/
vm._hasHookEvent = false;
// init parent attached events
/*初始化父组件attach的事件*/
const listeners = vm.$options._parentListeners;
if (listeners) {
updateComponentListeners(vm, listeners);
}
}
/*更新组件的监听事件*/
export function updateComponentListeners(
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm;
updateListeners(listeners, oldListeners || {}, add, remove, vm);
}
/*为Vue原型加入操作事件的方法*/
export function eventsMixin(Vue: Class<Component>) {}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# parent&children实现
src/core/instance/lifecycle.js:32
、src/core/vdom/create-component.js:47
、
/*初始化生命周期*/
export function initLifecycle(vm: Component) {
const options = vm.$options;
// locate first non-abstract parent
/* 将vm对象存储到parent组件中(保证parent组件是非抽象组件,比如keep-alive) */
let parent = options.parent;
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent;
}
parent.$children.push(vm);
}
vm.$parent = parent;
vm.$root = parent ? parent.$root : vm;
vm.$children = [];
vm.$refs = {};
vm._watcher = null;
vm._inactive = null;
vm._directInactive = false;
vm._isMounted = false;
vm._isDestroyed = false;
vm._isBeingDestroyed = false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions;
const child = (vnode.componentInstance = oldVnode.componentInstance);
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
);
},
2
3
4
5
6
7
8
9
10
11
# provide&inject实现
就是一个响应式的数据;defineReactive
src/core/instance/init.js:74
、src/core/instance/inject.js:7
initInjections(vm) // resolve injections before data/props
/*初始化props、methods、data、computed与watch*/
initState(vm)
initProvide(vm) // resolve provide after data/props
2
3
4
export function initProvide(vm: Component) {
const provide = vm.$options.provide;
if (provide) {
vm._provided = typeof provide === "function" ? provide.call(vm) : provide;
}
}
export function initInjections(vm: Component) {
const result = resolveInject(vm.$options.inject, vm);
if (result) {
Object.keys(result).forEach((key) => {
/* istanbul ignore else */
/*为对象defineProperty上在变化时通知的属性*/
if (process.env.NODE_ENV !== "production") {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
);
});
} else {
defineReactive(vm, key, result[key]);
}
});
}
}
export function resolveInject(inject: any, vm: Component): ?Object {
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
// isArray here
const isArray = Array.isArray(inject);
const result = Object.create(null);
const keys = isArray
? inject
: hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const provideKey = isArray ? key : inject[key];
let source = vm;
while (source) {
if (source._provided && provideKey in source._provided) {
result[key] = source._provided[provideKey];
break;
}
source = source.$parent; // 指定$parent
}
}
return result;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# $refs
实现
src/core/vdom/modules/ref.js:20
、src/core/vdom/create-component.js:47
、
将真实DOM
或者组件实例
挂载在当前实例的$refs属性上
export default {
create(_: any, vnode: VNodeWithData) {
registerRef(vnode);
},
update(oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (oldVnode.data.ref !== vnode.data.ref) {
registerRef(oldVnode, true);
registerRef(vnode);
}
},
destroy(vnode: VNodeWithData) {
registerRef(vnode, true);
},
};
/*注册一个ref(即在$refs中添加或者删除对应的Dom实例),isRemoval代表是增加还是移除,*/
export function registerRef(vnode: VNodeWithData, isRemoval: ?boolean) {
const key = vnode.data.ref;
if (!key) return;
const vm = vnode.context;
const ref = vnode.componentInstance || vnode.elm;
const refs = vm.$refs;
if (isRemoval) {
/*移除一个ref*/
if (Array.isArray(refs[key])) {
remove(refs[key], ref);
} else if (refs[key] === ref) {
refs[key] = undefined;
}
} else {
/*增加一个ref*/
if (vnode.data.refInFor) {
/*如果是在一个for循环中,则refs中key对应的是一个数组,里面存放了所有ref指向的Dom实例*/
if (Array.isArray(refs[key]) && refs[key].indexOf(ref) < 0) {
refs[key].push(ref);
} else {
refs[key] = [ref];
}
} else {
/*不在一个for循环中则直接放入refs即可,ref指向Dom实例*/
refs[key] = ref;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*初始化组件*/
function initComponent(vnode, insertedVnodeQueue) {
/*把之前已经存在的VNode队列合并进去*/
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(
insertedVnodeQueue,
vnode.data.pendingInsert
);
}
vnode.elm = vnode.componentInstance.$el;
if (isPatchable(vnode)) {
/*调用create钩子*/
invokeCreateHooks(vnode, insertedVnodeQueue);
/*为scoped CSS 设置scoped id*/
setScope(vnode);
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
/*注册ref*/
registerRef(vnode);
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# $attrs&$listener实现
src/core/instance/lifecycle.js:215
、src/core/instance/render.js:49
、
export function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
if (process.env.NODE_ENV !== 'production') {
isUpdatingChildComponent = true
}
// determine whether component has slot children
// we need to do this before overwriting $options._renderChildren.
// check if there are dynamic scopedSlots (hand-written or compiled but with
// dynamic slot names). Static scoped slots compiled from template has the
// "$stable" marker.
const newScopedSlots = parentVnode.data.scopedSlots
const oldScopedSlots = vm.$scopedSlots
const hasDynamicScopedSlot = !!(
(newScopedSlots && !newScopedSlots.$stable) ||
(oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
(newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key)
)
// Any static slot children from the parent may have changed during parent's
// update. Dynamic scoped slots may also have changed. In such cases, a forced
// update is necessary to ensure correctness.
const needsForceUpdate = !!(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
hasDynamicScopedSlot
)
vm.$options._parentVnode = parentVnode
vm.$vnode = parentVnode // update vm's placeholder node without re-render
if (vm._vnode) { // update child tree's parent
vm._vnode.parent = parentVnode
}
vm.$options._renderChildren = renderChildren
// update $attrs and $listeners hash
// these are also reactive so they may trigger child update if the child
// used them during render
vm.$attrs = parentVnode.data.attrs || emptyObject // 一直查找
vm.$listeners = listeners || emptyObject
//<child3 v-bind="$attrs" v-on="$listeners"></child3>
// update props
if (propsData && vm.$options.props) {
toggleObserving(false)
const props = vm._props
const propKeys = vm.$options._propKeys || []
for (let i = 0; i < propKeys.length; i++) {
const key = propKeys[i]
const propOptions: any = vm.$options.props // wtf flow?
props[key] = validateProp(key, propOptions, propsData, vm)
}
toggleObserving(true)
// keep a copy of raw propsData
vm.$options.propsData = propsData
}
// update listeners
listeners = listeners || emptyObject
const oldListeners = vm.$options._parentListeners
vm.$options._parentListeners = listeners
updateComponentListeners(vm, listeners, oldListeners)
// resolve slots + force update if has children
if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
if (process.env.NODE_ENV !== 'production') {
isUpdatingChildComponent = false
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80