vue组件通信方式及原理分析

# 简介及比较

Vue 组件常用几种种的通信方式:

# 数据是单向数据流;

  • props / $emit
  • $parent / $childrenrefs
  • $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 组件通信.png

上面展示的图片可以引入所有 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)
    }
  }
};
1
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;
      }   
    },
  };
1
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>
1
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>
1
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事件;

分析

  1. 使用 Vuex 来进行数据管理,但是使用的 vuex 的问题在于,如果项目比较小,组件间的共享状态比较少,那用 vuex 就好比杀鸡用牛刀
  2. 利用 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组件传递过来的值 => ",
    };
  },
};
1
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>
1
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
1
2

# provide / inject

父 => 子、父 => 孙组件通信

provide/inject 是 Vue2.2.0 新增 API,这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。

provideinject 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。

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>
1
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();
1
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
      })
    }
  }
};
1
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>
  `
});
1
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>
1
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

这也是一对成对出现的方法,不过只是在 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);
        }
    }
};
1
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 是向特定的子组件触发事件,本质上这种方式还是 onemit 的封装,在一些基础组件中都很实用。

# 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:74scr/core/instance/state:64src/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");
1
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;
}
1
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:74src/core/instance/events.js:12src/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>) {}
1
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:32src/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;
}
1
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
    );
  },
1
2
3
4
5
6
7
8
9
10
11

# provide&inject实现

就是一个响应式的数据;defineReactive

src/core/instance/init.js:74src/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
1
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;
  }
}
1
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:20src/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;
    }
  }
}
1
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);
    }
  }
1
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:215src/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
  }
}
1
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

# 相关链接

上次更新: 2022/04/15, 05:41:28
×