代码改变世界

vue系列---Vue组件化的实现原理(八)

2019-10-29 20:19  龙恩0707  阅读(10886)  评论(0编辑  收藏  举报

阅读目录

在Vue中,组件是一个很强大的功能,组件可以扩展HTML元素,封装可重用的代码。比如在页面当中的某一个部分需要在多个场景中使用,那么我们可以将其抽出为一个组件来进行复用。组件可以大大提高了代码的复用率。

所有的Vue组件都是Vue的实列,因此它可以接受Vue中的所有的生命周期钩子。Vue又分为全局注册和局部注册两种方式来注册组件。全局组件它可以在任何(根)实列中使用的组件,局部组件只能在某一实列中使用的组件。

1.1 全局注册组件

 全局注册有下面两种方式注册:
 1. 通过 Vue.component 直接注册

全局组件可以使用 Vue.component(tagName, option); 来注册组件。 tagName 是自定义的组件名称, option是组件的一些选项, 比如可以在option对象中添加如下一些选项:
1) template 表示组件模板。
2) methods 表示组件里的方法。
3) data 表示组件里的数据。

在Vue中, 定义组件名的方式也是有规范的。定义组件名的方式有两种:

1) 使用kebab-case

Vue.component('my-component-name', {}); 

kebab-case 的含义是: "短横线分隔命名" 来定义一个组件, 因此我们在使用该组件的时候, 就如这样使用: <my-component-name>

2) 使用 PascalCase

Vue.component('MyComponentName', {});

PascalCase 的含义是: "首字母大写命名" 来定义一个组件, 我们在使用该组件时,可以通过如下两种方式来使用:<my-component-name>或<MyComponentName>都是可以的。
那么一般的情况下,我们习惯使用 第一种方式来使用组件的。

下面以官方列子来举例如何注册一个全局组件,简单的代码如下:

<!DOCTYPE html>
<html>
<head>
  <title>vue组件测试</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <!-- 组件复用如下 -->
    <button-counter></button-counter><br/><br/>
    <button-counter></button-counter><br/><br/>
    <button-counter></button-counter>
  </div>
  <script type="text/javascript">
    Vue.component('button-counter', {
      data: function() {
        return {
          count: 0
        }
      },
      template: '<button @click="count++">You clicked me {{ count }} times.</button>'
    });
    new Vue({
      el: '#app'
    });
  </script>
</body>
</html>

如上调用了三次组件,对第一个按钮点击了一次,因此 count = 1了,第二个按钮点击2次,因此count=2了,对第三个按钮点击了3次,因此count=3了。如下图所示:

注意:当我们定义 button-counter 组件的时候,data 必须为一个函数,不能是一个对象,比如不能是如下的对象:

data: {
  count: 0
};

因为每个实列可以维护一份被返回对象的独立拷贝。如果是一个对象的话,那么它每次调用的是共享的实列,因此改变的时候会同时改变值。

官方文档是这么说的:当一个组件被定义时,data必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实列。如果data仍然是一个纯粹的对象,则所有的实列将共享引用同一个数据对象。通过提供data函数,每次创建一个新实列后,我们能够调用data函数,从而返回初始数据的一个全新副本的数据对象。

2. 通过Vue.extend来注册

Vue.extend(options); Vue.extend 返回的是一个 "扩展实列构造器", 不是具体的组件实列, 它一般是通过 Vue.component 来生成组件。

简单的代码如下测试:

<!DOCTYPE html>
<html>
<head>
  <title>vue组件测试</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <!-- 组件复用如下 -->
    <button-counter></button-counter><br/><br/>
    <button-counter></button-counter><br/><br/>
    <button-counter></button-counter>
  </div>
  <!-- 全局组件在 id为app2下也能使用的 -->
  <div id="app2">
    <button-counter></button-counter>
  </div>
  <script type="text/javascript">
    var buttonCounter = Vue.extend({
      name: 'button-counter',
      data: function() {
        return {
          count: 0
        }
      },
      template: '<button @click="count++">You clicked me {{ count }} times.</button>'
    });
    /*
      Vue.component 是用来全局注册组件的方法, 其作用是将通过 Vue.extend 生成的扩展实列构造器注册为一个组件 
    */
    Vue.component('button-counter', buttonCounter);
    
    // 初始化实列
    new Vue({
      el: '#app'
    });
    new Vue({
      el: '#app2'
    });
  </script>
</body>
</html>

效果和上面也是一样的。如上我们可以看到, 全局组件不仅仅在 '#app' 域下可以使用, 还可以在 '#app2' 域下也能使用。这就是全局组件和局部组件的区别, 局部组件我们可以看如下的demo。

1.2 局部注册组件

<div id="app">
  <child-component></child-component>
</div>
<script type="text/javascript">
  var child = {
    template: "<h1>我是局部组件</h1>"
  };
  new Vue({
    el: '#app',
    components: {
      "child-component": child
    }
  })
</script>

在浏览器中会被渲染成html代码如下:

"<div id='app'><h1>我是局部组件</h1></div>";

如上代码是局部组件的一个列子, 局部组件只能在 id 为 'app' 域下才能使用, 在其他id 下是无法访问的。如下如下代码:

<div id="app">
  <child-component></child-component>
</div>
<div id="app2">
  <child-component></child-component>
</div>
<script type="text/javascript">
  var child = {
    template: "<h1>我是局部组件</h1>"
  };
  new Vue({
    el: '#app',
    components: {
      "child-component": child
    }
  });
  new Vue({
    el: '#app2'
  })
</script>

如上代码, 我们在 id 为 '#app2' 的域下使用 child-component 组件是使用不了的, 并且在控制台中会报错的。因为该组件是局部组件, 只能在 '#app' 域下才能使用。

1) props

在Vue中, 组件之间的通信有父子组件、兄弟组件、祖先后代组件等之间通信。

1. 父子组件通信

父组件想把数据传递给子组件是通过 props 来传递的, 子组件想把数据传递给父组件是通过事件 emit 来触发的。

在vue中,子组件向父组件传递数据, 子组件使用 $emit 触发事件, 在父组件中我们使用 v-on / @ 自定义事件进行监听即可。

我们可以使用如下图来解释他们是如何传递数据的, 如下图所示:

子组件的props选项能够接收来自父组件的数据。 我们可以使用一个比方说, 父子组件之间的数据传递相当于自上而下的下水管子, 只能从上往下流,不能逆流。这也正是Vue的设计理念之单向数据流。
而Props可以理解为管道与管道之间的一个衔接口。这样水才能往下流。

如下代码演示:

<!DOCTYPE html>
<html>
<head>
  <title>父子组件通信</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <!-- 父组件把message值传递给子组件 -->
    <child-component :content="message"></child-component>
  </div>
  <script type="text/javascript">
    var childComponent = Vue.extend({
      template: '<div>{{ content }}</div>',
      // 使用props接收父组件传递过来的数据
      props: {
        content: {
          type: String,
          default: 'I am is childComponent'
        }
      }
    });
    new Vue({
      el: '#app',
      data: {
        message: 'I am is parentComponent'
      },
      components: {
        childComponent
      }
    });
  </script>
</body>
</html>

在页面渲染出HTML如下:

<div id="app">
  <div>I am is parentComponent</div>
</div>

2) $emit

子组件想把数据传递给父组件的话, 那么可以通过事件触发的方式来传递数据, 父组件使用 v-on / @ 自定义事件进行监听即可。

如下基本代码演示:

<!DOCTYPE html>
<html>
<head>
  <title>父子组件通信</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <child-component @event="eventFunc"></child-component>
  </div>
  <script type="text/javascript">
    var childComponent = Vue.extend({
      template: '<div @click="triggerClick">子组件使用事件把数据传递给父组件</div>',
      data() {
        return {
          content: '我是子组件把数据传递给父组件'
        }
      },
      methods: {
        triggerClick() {
          this.$emit('event', this.content);
        }
      } 
    });
    new Vue({
      el: '#app',
      components: {
        childComponent
      },
      methods: {
        eventFunc(value) {
          console.log('打印出数据:' + value);
        }
      }
    });
  </script>
</body>
</html>

如上代码, 在子组件childComponent中, 我们定义了一个点击事件, 当我们点击后会触发 triggerClick 这个函数, 该函数内部使用 $emit 来派发一个事件, 事件名为 "event", 然后在父组件中我们使用 @ 或 v-on 来监听 'event' 这个事件名称, 在父组件中使用 @event="eventFunc";  因此当我们点击后, 会触发父组件中的 "eventFunc" 这个方法, 该方法有一个参数值, 就是子组件传递过来的数据。 

3) 使用$ref实现通信

ref它有下面两点用处: 

1. 如果ref用在子组件上, 那么它指向的就是子组件的实列, 可以理解为对子组件的索引的引用, 我们可以通过$ref就可以获取到在子组件定义的属性和方法。
2. 如果ref使用在普通的DOM元素上使用的话, 引用指向的就是DOM元素, 通过$ref就可以获取到该DOM的属性集合, 我们轻松就可以获取到DOM元素。作用和jquery中的选择器是一样的。

那么我们如何通过使用 $ref 来实现父子组件之间通信呢? 我们将上面使用 props 实现的功能, 下面我们使用 $ref 来实现一下:

如下代码演示:

<!DOCTYPE html>
  <html>
  <head>
    <title>父子组件通信</title>
    <meta charset="utf-8">
    <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <child-component ref="child"></child-component>
    </div>
    <script type="text/javascript">
      var childComponent = Vue.extend({
        template: '<div>{{message}}</div>',
        data() {
          return {
            message: ''
          }
        },
        methods: {
          getMsg(value) {
            this.message = value;
          }
        }
      });
      new Vue({
        el: '#app',
        components: {
          childComponent
        },
        mounted() {
          this.$refs.child.getMsg('父组件给子组件传递数据的');
        }
      });
    </script>
  </body>
  </html>  

如上代码, 我们子组件childComponent 默认 的 message值为 空字符串, 但是在父组件中的 mounted 生命周期中, 我们使用 $refs 获取到 子组件的方法, 把值传递给子组件, 最后子组件就能获取值, 重新渲染页面了。 因此最后页面被渲染成为:<div id="app"><div>父组件给子组件传递数据的</div></div>; 如上我们在父组件中调用子组件的方法,当然我们也可以改变子组件的属性值也是可以实现的.
props 和 $refs 之间的区别: 

props 着重与数据传递, 父组件向子组件传递数据, 但是他并不能调用子组件里面的属性和方法。

$refs 着重与索引,主要用于调用子组件里面的属性和方法。并且当ref使用在DOM元素的时候, 能起到选择器的作用, 我们可以通过它能获取到DOM元素的节点, 可以对DOM元素进行操作等。

4) $attrs 和 $listeners 及 inheritAttrs

$attrs 是vue2.4+版本添加的内容, 目的是解决props 在组件 "隔代" 传值的问题的。
如下图所示:

如上图我们可以看到, A组件与B组件是父子组件, A组件传递给B组件的数据可以使用props传递数据, B组件作为A组件的子组件, 传递数据只需要通过事件 $emit 触发事件即可。 但是A组件它与C组件要如何通信呢? 我们可以通过如下方案解决:

1) 我们首先会想到的是使用Vuex来对数据进行管理, 但是如果项目是非常小的话, 或者说全局状态比较少, 如果我们使用Vuex来解决的话, 感觉大材小用了。
2) 我们可以把B组件当做中转站, 比如说我们使用A组件把数据先传递给B组件, B组件拿到数据后在传递给C组件, 这虽然算是一种方案, 但是并不好, 比如说组件传递的数据非常多, 会导致代码的繁琐, 或导致代码以后更加的难以维护。
3) 自定义一个Vue中央数据总线, 但是这个方法适合组件跨级传递消息。

因此为了解决这个需求, 在vue2.4+ 的版本后, 引入了 $attrs 和 $listeners, 新增了 inheritAttrs选项。

inheritAttrs、attrs和listeners的使用场景: 组件之间传值, 特别对于祖孙组件有跨度的传值。

inheritAttrs 默认值为true, 意思是说会将父组件中除了props以外的属性会添加到子组件的根节点上。但是我们的子组件仍然可以通过 $attrs 获取到 props 以外的属性。

上面的含义可能会有点理解不清晰, 我们换句话说吧, 就是说我们的父组件传了两个属性值给子组件,但是子组件只获取到其中一个属性了, 那么另外一个属性会被当做子组件的根节点上的一个普通属性。
如下代码演示下:

<!DOCTYPE html>
<html>
<head>
  <title>父子组件通信</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <father-component
      :foo="foo"
      :bar="bar"
      @event="reciveChildFunc"
    >
    </father-component>
  </div>
  <script type="text/javascript">
    var str = `
      <div>
        <div>attrs: {{$attrs}}</div>
        <div>foo: {{foo}}</div>
      </div>
    `;
    // 这是父组件
    var fatherComponent = Vue.extend({
      template: str,
      name: 'father-component',
      data() {
        return {
          
        }
      },
      props: {
        foo: {
          type: String,
          default: ''
        }
      },
      components: {
        
      },
    });

    // 这是祖先组件
    new Vue({
      el: '#app',
      components: {
        fatherComponent
      },
      data() {
        return {
          foo: 'hello world',
          bar: 'kongzhi'
        }
      },
      methods: {
        reciveChildFunc(value) {
          console.log('接收孙子组件的数据' + value);
        }
      }
    });
  </script>
</body>
</html>

如上代码我们定义了一个祖先组件和一个父组件, 在祖先组件里面, 我们引用了父组件, 并且给父组件传递了两个属性数据, 分别为 'foo' 和 'bar'; 如下代码可以看得到:

<father-component
  :foo="foo"
  :bar="bar"
  @event="reciveChildFunc"
>
</father-component>

但是在我们的父组件中只接收了一个属性为 'foo'; 'bar' 属性值并没有接收, 因此该bar默认会把该属性放入到父组件的根元素上, 如下父组件接收祖先组件的代码如下:

var str = `
  <div>
    <div>attrs: {{$attrs}}</div>
    <div>foo: {{foo}}</div>
  </div>
`;
// 这是父组件
var fatherComponent = Vue.extend({
  template: str,
  name: 'father-component',
  data() {
    return {
      
    }
  },
  props: {
    foo: {
      type: String,
      default: ''
    }
  },
  components: {
    
  },
});

然后代码执行的结果如下所示:

如上效果可以看到, 我们可以使用 $attrs 来接收 祖先组件传递给父组件中未使用的数据。

同时bar参数默认会把属性放入到我们父组件的根元素上当做一个普通属性, 如下图所示:

如果我们不想让未使用的属性放入到父组件的根元素上当做普通属性的话, 我们可以在父组件上把 inheritAttrs 设置为false即可。如下父组件代码添加 inheritAttrs: false 

// 这是父组件
var fatherComponent = Vue.extend({
  template: '.....',
  name: 'father-component',
  data() {
    return {
      
    }
  },

  // 这是新增的代码
  inheritAttrs: false,

  props: {
    foo: {
      type: String,
      default: ''
    }
  },
  components: {
    
  },
});

效果如下图所示:

如上是祖先组件和父组件之间的交互, 现在我们又来了一个子组件, 我们现在要考虑的问题是我们要如何让祖先组件的数据直接传递给子组件呢? 或者说我们的子组件的数据如何能直接传递给祖先组件呢?

如上父组件我们可以看到,我们使用 $attrs 就可以拿到祖先组件的未使用的属性, 也就是 {"bar": 'kongzhi'} 这样的值, 如果我们在父组件中把该 $attrs 传递给子组件的话, 那么子组件不就可以直接拿到 bar 的值了吗? 因此我们在父组件中可以使用 v-bind = "$attrs" 这样的就可以把数据传递给子组件了。如下代码演示:

<!DOCTYPE html>
<html>
<head>
  <title>父子组件通信</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <father-component
      :foo="foo"
      :bar="bar"
      @event="reciveChildFunc"
    >
    </father-component>
  </div>
  <script type="text/javascript">

    var str2 = `<div>
                  <div>bar: {{bar}}</div>
                  <button @click="childFunc">点击子节点</button>
               </div>`;
    // 这是子组件
    var childComponent = Vue.extend({
      name: 'child-component',
      template: str2,
      data() {
        return {
          
        }
      },
      props: {
        bar: {
          type: String,
          default: ''
        }
      },
      methods: {
        childFunc() {
          this.$emit('event', '32');
        }
      }
    });

    var str = `
      <div>
        <div>attrs: {{$attrs}}</div>
        <div>foo: {{foo}}</div>
        <child-component v-bind="$attrs" v-on="$listeners"></child-component>
      </div>
    `;
    // 这是父组件
    var fatherComponent = Vue.extend({
      template: str,
      name: 'father-component',
      data() {
        return {
          
        }
      },
      props: {
        foo: {
          type: String,
          default: ''
        }
      },
      components: {
        childComponent
      },
    });

    // 这是祖先组件
    new Vue({
      el: '#app',
      components: {
        fatherComponent
      },
      data() {
        return {
          foo: 'hello world',
          bar: 'kongzhi'
        }
      },
      methods: {
        reciveChildFunc(value) {
          console.log('接收孙子组件的数据' + value);
        }
      }
    });
  </script>
</body>
</html>

上面的执行结果如下所示:

如上我们可以看到,在父组件里面,我们传递数据给子组件, 我们通过 v-bind="$atts" 这样的就可以把数据传递给孙子组件了, 同样孙子组件 emit的事件可以在中间组件中通过$listeners属性来传递。 在子组件里面我们可以使用 props 来接收参数 'bar' 的值了。同样在子组件我们想把数据传递给 祖先组件的话, 我们通过事件的方式:
@click="childFunc"; 然后在该函数代码里面执行: this.$emit('event', '32'); 和我们以前父子组件传递数据没有什么区别, 无非就是在中间组件之间添加了 v-on="$listeners";
这样就可以实现祖先组件和孙子组件的数据交互了, 如上我们在孙子组件点击 按钮后会调用 emit 触发事件; 如代码:this.$emit('event', '32'); 然后我们在 祖先组件中通过 @event="reciveChildFunc" 来监听该事件了, 因此我们在祖先组件中 编写reciveChildFunc函数来接收数据即可。

我们下面可以看下中间组件(也就是父组件是如何使用 $listeners 和 $attrs的了)。 如下代码:

var str = `
<div>
  <div>attrs: {{$attrs}}</div>
  <div>foo: {{foo}}</div>
  <!-- 使用 v-bind="$attrs" 把数据传递给孙子组件, 通过v-on="$listeners"这样可以实现祖先组件和孙子组件数据交互 -->
  <child-component v-bind="$attrs" v-on="$listeners"></child-component>
</div>
`;
// 这是父组件
var fatherComponent = Vue.extend({
  template: str,
  name: 'father-component',
  data() {
    return {
      
    }
  },
  props: {
    foo: {
      type: String,
      default: ''
    }
  },
  components: {
    childComponent
  }
});

5) 理解 provide 和 inject 用法

provide 和 inject 主要是为高阶插件/组件库提供用例, 在应用程序代码中并不推荐使用。
注意: 该两个属性是一起使用的。它允许一个祖先组件向其所有子孙后代组件注入一个依赖, 不论组件层次有多深。也就是说, 在我们的项目当中会有很多很多组件,并且嵌套的很深的组件, 我们的子孙组件想要获取到祖先组件更多的属性的话,那么要怎么办呢? 我们总不可能通过父组件一级一级的往下传递吧, 那如果真这样做的话, 那么随着项目越来越大, 我们的项目会越来越难以维护, 因此 provide 和 inject 出现了, 他们两个就是来解决这个事情的。

provide: 它是一个对象, 或者是一个返回对象的函数。里面它可以包含所有要传递给子孙组件的属性或属性值。
inject: 它可以是一个字符串数组或者是一个对象。属性值也可以是一个对象。

简单的来说, 父组件通过provide来提供变量, 然后在子组件中可以通过 inject来注入变量。

下面我们可以来看下一个简单的demo来理解下 provide/inject 的使用吧:

<!DOCTYPE html>
<html>
<head>
  <title>父子组件通信</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <parent-component></parent-component>
  </div>
  <script type="text/javascript">
    // 定义一个子组件
    var childComponent = Vue.extend({
      template: '<div>子组件获取祖先组件的age值为: {{age2}}</div>',
      inject: ['age'],
      data() {
        return {
          age2: this.age
        }
      }
    });
    // 定义一个父组件
    var str = `<div>
                <div>父组件获取name属性为: {{name2}}</div>
                <child-component></child-component>
              </div>`;
    var parentComponent = Vue.extend({
      template: str,
      inject: ['name'],
      data() {
        return {
          name2: this.name
        }
      },
      components: {
        childComponent
      }
    });
    // 初始化祖先组件
    new Vue({
      el: '#app',
      components: {
        parentComponent
      },
      // 祖先组件提供所有要传递的属性变量给子组件
      provide: {
        name: 'kongzhi',
        age: 31,
        marriage: 'single'
      },
      data() {
        return {
          
        }
      },
      methods: {
        
      }
    });
  </script>
</body>
</html>

如上代码运行效果如下所示:

如上我们可以看得到, 我们在祖先组件中 使用 provide 提供了所有需要传递给子组件甚至孙子组件的数据, 然后在我们的祖先组件中调用了父组件, 并且没有通过props传递任何数据给父组件, 代码如下看得到:

<div id="app">
  <parent-component></parent-component>
</div>

然后我们在父组件中使用 inject: ['name'] 这样的, 通过inject这个来注入name属性进来, 因此在我们data中使用 this.name 就可以获取到我们祖先组件中的属性了, 然后在父组件中使用该属性即可。
同样的道理在我们的父组件中调用子组件, 也没有传递任何属性过去, 如下的代码可以看得到:

<child-component></child-component>

在我们子组件中, 也一样可以通过 inject 来注入祖先组件的属性; 比如代码: inject: ['age'], 因此在页面上我们一样通过 this.age 值就可以拿到祖先的属性值了。

6) 理解使用bus总线

bus总线可用于解决跨级和兄弟组件通信的问题,它可以为一个简单的组件传递数据。我们可以使用一个空的Vue实列作为中央事件总线。

我们可以封装Bus成Vue的一个方法; 如下代码:

const Bus = new Vue({
    methods: {
      emit(event, ...args) {
        this.$emit(event, ...args);
      },
      on(event, callback) {
        this.$on(event, callback);
      },
      off(event, callback) {
        this.$off(event, callback);
      }
    }
  });
  // 把该Bus放在原型上
  Vue.prototype.$bus = Bus;

因此我们可以把Bus抽离出来当做一个文件, 然后在入口文件引用进去即可; 假如我们现在项目结构如下:

|--- app
| |--- index
| | |--- common
| | | |--- bus.js
| | |--- views
| | | |--- c1.vue
| | | |--- c2.vue
| | | |--- index.vue
| | |--- app.js
| | |--- package.json
| | |--- webpack.config.js
| | |--- .babelrc

app/app.js 代码如下:

import Vue from 'vue/dist/vue.esm.js';

import Index from './views/index';
import Bus from './common/bus';

Vue.use(Bus);

new Vue({
  el: '#app',
  render: h => h(Index)
});

app/index/common/bus.js 源码如下:

import Vue from 'vue/dist/vue.esm.js';

const install = function(Vue) {
  const Bus = new Vue({
    methods: {
      emit(event, ...args) {
        this.$emit(event, ...args);
      },
      on(event, callback) {
        this.$on(event, callback);
      },
      off(event, callback) {
        this.$off(event, callback);
      }
    }
  });
  // 注册给Vue对象的原型上的全局属性
  Vue.prototype.$bus = Bus;
};

export default install;

app/index/common/index.vue

<template>
  <div>
    <c1></c1>
    <c2></c2>
  </div>
</template>

<script>
  import c1 from './c1';
  import c2 from './c2';
  export default {
    components: {
      c1,
      c2
    }
  }
</script>

app/index/common/c1.vue

<template>
  <div>
    <div>{{msg}}</div>
  </div>
</template>

<script>
  export default {
    name: 'c1',
    data: function() {
      return {
        msg: 'I am kongzhi'
      }
    },
    mounted() {
      this.$bus.$on('setMsg', function(c) {
        console.log(c);
        this.msg = c;
      })
    }
  }
</script>

app/index/common/c2.vue

<template>
  <button @click="sendEvent">Say Hi</button>
</template>

<script>
  export default {
    name: 'c2',
    methods: {
      sendEvent() {
        this.$bus.$emit('setMsg', '我是来测试Bus总线的');
      }
    }
  }
</script> 

如上代码, 我们把Bus代码抽离出来 封装到 app/index/common/bus.js 中, 然后在app/app.js 中入口文件中使用 Vue.use(Bus);
接着在 app/index/common/c2.vue c2组件中, 点击按钮 Say Hi 触发函数 sendEvent; 调用 this.$bus.$emit('setMsg', '我是来测试Bus总线的');事件, 然后在c1组件中使用

this.$bus.$on('setMsg', function(c) { console.log(c); } 来监听该事件. 如上代码我们可以看到我们 打印 console.log(c); 能打印新值出来。

注意: 使用Bus存在的问题是: 如上代码, 我们在 this.$bus.$on 回调函数中 this.msg = c; 改变值后,视图并不会重新渲染更新,之所以会出现这种原因: 是因为我们接收的bus 是 $bus.on触发的。而不会重新渲染页面, 这也有可能是Vue中官方的一个缺陷吧。

因此Bus总线会存在一些问题, 所以在Vue组件通信的时候, 我们可以综合考虑来使用哪种方法来进行通信。

三:在vue源码中注册组件是如何实现的呢?

3.1 全局注册组件 

上面已经介绍过, 全局注册组件有2种方式; 第一种方式是通过Vue.component 直接注册。第二种方式是通过Vue.extend来注册。

Vue.component 注册组件

比如如下代码:

<!DOCTYPE html>
<html>
<head>
  <title>vue组件测试</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <!-- 组件调用如下 -->
    <button-counter></button-counter><br/><br/>
  </div>
  <script type="text/javascript">
    Vue.component('button-counter', {
      data: function() {
        return {
          count: 0
        }
      },
      template: '<button @click="count++">You clicked me {{ count }} times.</button>'
    });
    new Vue({
      el: '#app'
    });
  </script>
</body>
</html>

如上组件注册是通过 Vue.component来注册的, Vue注册组件初始化的时候, 首先会在 vue/src/core/global-api/index.js 初始化代码如下:

import { initAssetRegisters } from './assets'
initAssetRegisters(Vue);

因此会调用 vue/src/core/global-api/assets.js 代码如下:

/* @flow */

import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'

export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

如上代码中的 'shared/constants' 中的代码在 vue/src/shared/constants.js 代码如下:

export const SSR_ATTR = 'data-server-rendered'

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]

因此 ASSET_TYPES = ['component', 'directive', 'filter']; 然后上面代码遍历:

ASSET_TYPES.forEach(type => {
  Vue[type] = function (
    id: string,
    definition: Function | Object
  ): Function | Object | void {
    if (!definition) {
      return this.options[type + 's'][id]
    } else {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && type === 'component') {
        validateComponentName(id)
      }
      if (type === 'component' && isPlainObject(definition)) {
        definition.name = definition.name || id
        definition = this.options._base.extend(definition)
      }
      if (type === 'directive' && typeof definition === 'function') {
        definition = { bind: definition, update: definition }
      }
      this.options[type + 's'][id] = definition
      return definition
    }
  }
});

从上面源码中我们可知: Vue全局中挂载有 Vue['component'], Vue['directive'] 及 Vue['filter']; 有全局组件, 指令和过滤器。在Vue.component注册组件的时候, 我们是如下调用的:

Vue.component('button-counter', {
  data: function() {
    return {
      count: 0
    }
  },
  template: '<button @click="count++">You clicked me {{ count }} times.</button>'
});

因此在源码中我们可以看到: 

id = 'button-counter'; 
definition = {
  template: '<button @click="count++">You clicked me {{ count }} times.</button>',
  data: function() {
    return {
      count: 0
    }
  }
};

如上代码, 首先我们判断如果 definition 未定义的话,就返回 this.options 中内的types 和id对应的值。this.options 有如下值:

this.options = {
  base: function(Vue),
  components: {
    KeepAlive: {},
    Transition: {},
    TransitionGroup: {}
  },
  directives: {
    mode: {},
    show: {}
  },
  filters: {

  }
};

如上我们知道type的可取值分别为: 'component', 'directive', 'filter'; id为: 'button-counter'; 因此如果 definition 未定义的话, 就返回: return this.options[type + 's'][id]; 因此如果type为 'component' 的话, 那么就返回 this.options['components']['button-counter']; 从上面我们的 this.options 的值可知; this.options['components'] 的值为:

this.options['components'] = {
    KeepAlive: {},
    Transition: {},
    TransitionGroup: {}
  };

因此如果 definition 值为未定义的话, 则返回 return this.options['components']['button-counter']; 的值为 undefined;

如果definition定义了的话, 如果不是正式环境的话, 就调用 validateComponentName(id); 方法, 该方法的作用是验证我们组件名的合法性; 该方法代码如下:

// 验证组件名称的合法性
function validateComponentName (name) {
  if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) {
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'should conform to valid custom element name in html5 specification.'
    );
  }
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    );
  }
}

如果是component(组件)方法,并且definition是对象, 源码如下:

if (type === 'component' && isPlainObject(definition)) {
  definition.name = definition.name || id = 'button-counter';
  definition = this.options._base.extend(definition)
}

我们可以打印下 this.options._base 的值如下:

如上我们可以看到 this.options._base.extend 就是指向了 Vue.extend(definition); 作用是将定义的对象转成了构造器。

Vue.extend 代码在 vue/src/core/global-api/extend.js中, 代码如下:

/*
 @param {extendOptions} Object
 extendOptions = {
   name: 'button-counter',
   template: '<button @click="count++">You clicked me {{ count }} times.</button>',
   data: function() {
    return {
      count: 0
    }
  }
 };
 */
Vue.cid = 0;
var cid = 1;
Vue.extend = function (extendOptions: Object): Function {
  extendOptions = extendOptions || {}
  const Super = this
  const SuperId = Super.cid
  // 如果组件已经被缓存到extendOptions, 则直接取出组件
  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
  }
  /*
    获取 extendOptions.name 因此 name = 'button-counter'; 
    如果有name属性值的话, 并且不是正式环境的话,验证下组件名称是否合法
   */
  const name = extendOptions.name || Super.options.name
  if (process.env.NODE_ENV !== 'production' && name) {
    validateComponentName(name)
  }
  
  const Sub = function VueComponent (options) {
    this._init(options)
  }
  /*
    将Vue原型上的方法挂载到 Sub.prototype 中。
    因此Sub的实列会继承了Vue原型中的所有属性和方法。
   */
  Sub.prototype = Object.create(Super.prototype)
  // Sub原型重新指向Sub构造函数
  Sub.prototype.constructor = Sub
  Sub.cid = cid++
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  Sub['super'] = Super

  // For props and computed properties, we define the proxy getters on
  // the Vue instances at extension time, on the extended prototype. This
  // avoids Object.defineProperty calls for each instance created.
  if (Sub.options.props) {
    initProps(Sub)
  }
  if (Sub.options.computed) {
    initComputed(Sub)
  }

  // allow further extension/mixin/plugin usage
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use

  // create asset registers, so extended classes
  // can have their private assets too.
  ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type]
  })
  // enable recursive self-lookup
  if (name) {
    Sub.options.components[name] = Sub
  }

  // keep a reference to the super options at extension time.
  // later at instantiation we can check if Super's options have
  // been updated.
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  // cache constructor
  cachedCtors[SuperId] = Sub
  return Sub
}

如上代码中会调用 mergeOptions 函数, 该函数的作用是: 用于合并对象, 将两个对象合并成为一个。如上代码: 

Sub.options = mergeOptions(
  Super.options,
  extendOptions
);

如上函数代码,我们可以看到 mergeOptions 有两个参数分别为: Super.options 和 extendOptions。他们的值可以看如下所示:

 

如上我们可以看到, Super.options 和 extendOptions 值分别为如下:

Super.options = {
  _base: function Vue(options),
  components: {},
  directives: {},
  filters: {}
};
extendOptions = {
  name: 'button-counter',
  template: '<button @click="count++">You clicked me {{ count }} times.</button>',
  data: function() {
    return {
      count: 0
    }
  },
  _Ctor: {}
};

该mergeOptions函数的代码在 src/core/util/options.js 中, 基本代码如下:

/* 
  参数 parent, child, 及 vm的值分别为如下:
  parent = {
    _base: function Vue(options),
    components: {},
    directives: {},
    filters: {}
  };
  child = {
    name: 'button-counter',
    template: '<button @click="count++">You clicked me {{ count }} times.</button>',
    data: function() {
      return {
        count: 0
      }
    },
    _Ctor: {}
  }
  vm: undefined
*/
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

如上代码, 首先该函数接收3个参数, 分别为: parent, child, vm ,值分别如上注释所示。第三个参数是可选的, 在这里第三个参数值为undefined; 第三个参数vm的作用是: 会根据vm参数是实列化合并还是继承合并。从而会做不同的操作。

首先源码从上往下执行, 会判断是否是正式环境, 如果不是正式环境, 会对组件名称进行合法性校验。如下基本代码:

export function validateComponentName (name: string) {
  if (!new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$`).test(name)) {
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'should conform to valid custom element name in html5 specification.'
    )
  }
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    )
  }
}
function checkComponents (options: Object) {
  for (const key in options.components) {
    validateComponentName(key)
  }
}
if (process.env.NODE_ENV !== 'production') {
  checkComponents(child)
}

接下来会判断传入的参数child是否为一个函数,如果是的话, 则获取它的options的值重新赋值给child。也就是说child的值可以是普通对象, 也可以是通过Vue.extend继承的子类构造函数或是Vue的构造函数。基本代码如下:

if (typeof child === 'function') {
  child = child.options
}

接下来会执行如下三个函数:

normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)

它们的作用是使数据能规范化, 比如我们之前的组件之间的传递数据中的props或inject, 它既可以是字符串数组, 也可以是对象。指令directives既可以是一个函数, 也可以是对象。在vue源码中对外提供了便捷, 但是在代码内部做了相应的处理。 因此该三个函数的作用是将数据转换成对象的形式。

normalizeProps 函数代码如下:

/*
 @param {options} Object 
 options = {
   name: 'button-counter',
    template: '<button @click="count++">You clicked me {{ count }} times.</button>',
    data: function() {
      return {
        count: 0
      }
    },
    _Ctor: {}
 };
 vm = undefined
 */
function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}

该函数的作用对组件传递的 props 数据进行处理。在这里我们的props为undefined,因此会直接return, 但是我们之前父子组件之间的数据传递使用到了props, 比如如下代码:

var childComponent = Vue.extend({
  template: '<div>{{ content }}</div>',
  // 使用props接收父组件传递过来的数据
  props: {
    content: {
      type: String,
      default: 'I am is childComponent'
    }
  }
});

因此如上代码的第一行: const props = options.props; 因此props的值为如下:

props = {
  content: {
    type: String,
    default: 'I am is childComponent'
  }
};

如上props也可以是数组的形式, 比如 props = ['x-content', 'name']; 这样的形式, 因此在代码内部分了2种情况进行判断, 第一种是处理数组的情况, 第二种是处理对象的情况。

首先是数组的情况, 如下代码:

export function cached<F: Function> (fn: F): F {
  const cache = Object.create(null)
  return (function cachedFn (str: string) {
    const hit = cache[str]
    return hit || (cache[str] = fn(str))
  }: any)
}

const camelizeRE = /-(\w)/g;
/*
 该函数的作用是把组件中的 '-' 字符中的第一个字母转为大写形式。
 比如如下代码:
 'a-b'.replace(/-(\w)/g, (_, c) => c ? c.toUpperCase() : ''); 
  最后打印出 'aB';
 */
export const camelize = cached((str: string): string => {
  return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
})

const res = {}
let i, val, name
if (Array.isArray(props)) {
  i = props.length
  while (i--) {
    val = props[i]
    if (typeof val === 'string') {
      name = camelize(val)
      res[name] = { type: null }
    } else if (process.env.NODE_ENV !== 'production') {
      warn('props must be strings when using array syntax.')
    }
  }
}

我们可以假设props是数组, props = ['x-content', 'name']; 这样的值。 因此 i = props.length = 2; 因此就会进入while循环代码, 最后会转换成如下的形式:

res = {
  'xContent': { type: null },
  'name': { type: null }
};

同理如果我们假设我们的props是一个对象形式的话, 比如值为如下:

props: {
  'x-content': String,
  'name': Number
};

因此会执行else语句代码; 代码如下所示:

const _toString = Object.prototype.toString;

function isPlainObject (obj: any): boolean {
  return _toString.call(obj) === '[object Object]'
}

else if (isPlainObject(props)) {
  for (const key in props) {
    val = props[key]
    name = camelize(key)
    res[name] = isPlainObject(val)
      ? val
      : { type: val }
  }
}

因此最后 res的值变为如下:

res = {
  'xContent': {
    type: function Number() { ... }
  },
  'name': {
    type: function String() { ... }
  }
};

当然如上代码, 如果某一个key本身是一个对象的话, 就直接返回该对象, 比如 props 值如下:

props: {
  'x-content': String,
  'name': Number,
  'kk': {'name': 'kongzhi11'}
}

那么最后kk的键就不会进行转换, 最后返回的值res变为如下:

res = {
  'xContent': {
    type: function Number() { ... }
  },
  'name': {
    type: function String() { ... }
  },
  'kk': {'name': 'kongzhi11'}
};

因此最后我们的child的值就变为如下值了:

child = {
  name: 'button-counter',
  template: '<button @click="count++">You clicked me {{ count }} times.</button>',
  data: function() {
    return {
      count: 0
    }
  },
  _Ctor: {},
  props: {
    'xContent': {
      type: function Number() { ... }
    },
    'name': {
      type: function String() { ... }
    },
    'kk': {'name': 'kongzhi11'}
    }
  }
};

normalizeInject 函数, 该函数的作用一样是使数据能够规范化, 代码如下:

function normalizeInject (options: Object, vm: ?Component) {
  const inject = options.inject
  if (!inject) return
  const normalized = options.inject = {}
  if (Array.isArray(inject)) {
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] }
    }
  } else if (isPlainObject(inject)) {
    for (const key in inject) {
      const val = inject[key]
      normalized[key] = isPlainObject(val)
        ? extend({ from: key }, val)
        : { from: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "inject": expected an Array or an Object, ` +
      `but got ${toRawType(inject)}.`,
      vm
    )
  }
}

同理, options的值可以为对象或数组。options值为如下:

options = {
  name: 'button-counter',
  template: '<button @click="count++">You clicked me {{ count }} times.</button>',
  data: function() {
    return {
      count: 0
    }
  },
  inject: ['name'],
  _Ctor: {}
};

同理依次执行代码; const inject = options.inject = ['name'];

1: inject数组情况下:

inject是数组的话, 会进入if语句内, 代码如下所示:

var normalized = {};

if (Array.isArray(inject)) {
  for (let i = 0; i < inject.length; i++) {
    normalized[inject[i]] = { from: inject[i] }
  }
}

因此最后 normalized 的值变为如下:

normalized = {
  'name': {
    from: 'name'
  }
};

因此child值继续变为如下值:

child = {
  name: 'button-counter',
  template: '<button @click="count++">You clicked me {{ count }} times.</button>',
  data: function() {
    return {
      count: 0
    }
  },
  _Ctor: {},
  props: {
    'xContent': {
      type: function Number() { ... }
    },
    'name': {
      type: function String() { ... }
    },
    'kk': {'name': 'kongzhi11'}
    }
  },
  inject: {
    'name': {
      form: 'name'
    }
  }
};

2. inject为对象的情况下:

比如现在options的值为如下:

options = {
  name: 'button-counter',
  template: '<button @click="count++">You clicked me {{ count }} times.</button>',
  data: function() {
    return {
      count: 0
    }
  },
  inject: {
    foo: {
      from: 'bar',
      default: 'foo'
    }
  },
  _Ctor: {}
};

如上inject配置中的 from表示在可用的注入内容中搜索用的 key,default当然就是默认值。默认是 'foo', 现在我们把它重置为 'bar'. 因此就会执行else if语句代码,基本代码如下所示:

/**
 * Mix properties into target object.
 */
export function extend (to: Object, _from: ?Object): Object {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}

else if (isPlainObject(inject)) {
  for (const key in inject) {
    const val = inject[key]
    normalized[key] = isPlainObject(val)
      ? extend({ from: key }, val)
      : { from: val }
  }
} 

由上可知; inject值为如下:

inject = {
  foo: {
    from: 'bar',
    default: 'foo'
  }
};

如上代码, 使用for in 遍历 inject对象。执行代码 const val = inject['foo'] = { from: 'bar', default: 'foo' }; 可以看到val是一个对象。因此会调用 extend函数方法, 该方法在代码 vue/src/shared/util.js 中。
代码如下:

/*
  @param {to}
  to = {
    from: 'foo'
  }
  @param {_from}
  _form = {
    from: 'bar',
    default: 'foo'
  }
 */
export function extend (to: Object, _from: ?Object): Object {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}

如上执行代码后, 因此最后 normalized 值变为如下:

normalized = {
  foo: {
    from: 'bar',
    default: 'foo'
  }
};

因此我们通过格式化 inject后,最后我们的child的值变为如下数据了:

child = {
  name: 'button-counter',
  template: '<button @click="count++">You clicked me {{ count }} times.</button>',
  data: function() {
    return {
      count: 0
    }
  },
  _Ctor: {},
  props: {
    'xContent': {
      type: function Number() { ... }
    },
    'name': {
      type: function String() { ... }
    },
    'kk': {'name': 'kongzhi11'}
    }
  },
  inject: {
    'foo': {
      default: 'foo',
      from: 'bar'
    }
  }
};

现在我们继续执行 normalizeDirectives(child); 函数了。 该函数的代码在 vue/src/core/util/options.js中,代码如下:

/*
 * Normalize raw function directives into object format.
 * 遍历对象, 如果key值对应的是函数。则把他修改成对象的形式。
 * 因此从下面的代码可以看出, 如果vue中只传递了函数的话, 就相当于这样的 {bind: func, unpdate: func}
 */
function normalizeDirectives (options: Object) {
  const dirs = options.directives
  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key]
      if (typeof def === 'function') {
        dirs[key] = { bind: def, update: def }
      }
    }
  }
}

现在我们再回到 vue/src/core/util/options.js中 export function mergeOptions () 函数中接下来的代码:

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component) {
  : Object {
    // ...  代码省略
    if (!child._base) {
      if (child.extends) {
        parent = mergeOptions(parent, child.extends, vm)
      }
      if (child.mixins) {
        for (let i = 0, l = child.mixins.length; i < l; i++) {
          parent = mergeOptions(parent, child.mixins[i], vm)
        }
      }
    }

    const options = {}
    let key
    for (key in parent) {
      mergeField(key)
    }
    for (key in child) {
      if (!hasOwn(parent, key)) {
        mergeField(key)
      }
    }
    function mergeField (key) {
      const strat = strats[key] || defaultStrat
      options[key] = strat(parent[key], child[key], vm, key)
    }
    return options
  }
}

从上面可知, 我们的child的值为如下:

child = {
  name: 'button-counter',
  template: '<button @click="count++">You clicked me {{ count }} times.</button>',
  data: function() {
    return {
      count: 0
    }
  },
  _Ctor: {},
  props: {
    'xContent': {
      type: function Number() { ... }
    },
    'name': {
      type: function String() { ... }
    },
    'kk': {'name': 'kongzhi11'}
    }
  },
  inject: {
    'foo': {
      default: 'foo',
      from: 'bar'
    }
  }
};

因此 child._base 为undefined, 只有合并过的选项才会有 child._base 的值。这里判断就是过滤掉已经合并过的对象。 因此会继续进入if语句代码判断是否有 child.extends 这个值,如果有该值, 会继续调用mergeOptions方法来对数据进行合并。最后会把结果赋值给parent。
继续执行代码 child.mixins, 如果有该值的话, 比如 mixins = [xxx, yyy]; 这样的,因此就会遍历该数组,递归调用mergeOptions函数,最后把结果还是返回给parent。

接着继续执行代码;

 

// 定义一个空对象, 最后把结果返回给空对象
const options = {}
let key
/*
 遍历parent, 然后调用下面的mergeField函数
 parent的值为如下:
 parent = {
   _base: function Vue(options),
  components: {},
  directives: {},
  filters: {}
 };
 因此就会把components, directives, filters 等值当作key传递给mergeField函数。
*/
for (key in parent) {
  mergeField(key)
}
for (key in child) {
  if (!hasOwn(parent, key)) {
    mergeField(key)
  }
}
/*
 该函数主要的作用是通过key获取到对应的合并策略函数, 然后执行合并, 然后把结果赋值给options[key下面的starts的值,在源码中
 的初始化中已经定义了该值为如下:
 const strats = config.optionMergeStrategies;
 starts = {
   activated: func,
   beforeCreate: func,
   beforeDestroy: func,
   beforeMount: func,
   beforeUpdate: func,
   components: func,
   computed: func,
   created: func,
   data: func,
   deactivated: func,
   destroyed: func,
   directives: func,
   filters: func
   ......
 };
 如下代码: const strat = strats[key] || defaultStrat; 
 就能获取到对应中的函数, 比如key为 'components', 
 因此 start = starts['components'] = function mergeAssets(){};
*/
function mergeField (key) {
  const strat = strats[key] || defaultStrat
  options[key] = strat(parent[key], child[key], vm, key)
}
return options

mergeAssets函数代码如下:

function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  const res = Object.create(parentVal || null)
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

最后返回的options的值变为如下了:

options = {
  name: 'button-counter',
  template: '<button @click="count++">You clicked me {{ count }} times.</button>',
  data: function() {
    return {
      count: 0
    }
  },
  _Ctor: {},
  props: {
    'xContent': {
      type: function Number() { ... }
    },
    'name': {
      type: function String() { ... }
    },
    'kk': {'name': 'kongzhi11'}
    }
  },
  inject: {
    'foo': {
      default: 'foo',
      from: 'bar'
    }
  },
  components: {},
  directives: {},
  filters: {}
};

因此我们再回到代码 vue/src/core/global-api/extend.js 代码中的Vue.extend函数,如下代码:

Vue.extend = function (extendOptions: Object): Function {
  //......
  /*
   Sub.options的值 就是上面options的返回值
  */
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  Sub['super'] = Super;
  if (Sub.options.props) {
    initProps(Sub)
  }
  if (Sub.options.computed) {
    initComputed(Sub)
  }

  // allow further extension/mixin/plugin usage
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use;
  // create asset registers, so extended classes
  // can have their private assets too.
  ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type]
  })
  // enable recursive self-lookup
  if (name) {
    Sub.options.components[name] = Sub
  }

  // keep a reference to the super options at extension time.
  // later at instantiation we can check if Super's options have
  // been updated.
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  // cache constructor
  cachedCtors[SuperId] = Sub
  return Sub
}

因此 Sub.options值为如下:

Sub.options = {
  name: 'button-counter',
  template: '<button @click="count++">You clicked me {{ count }} times.</button>',
  data: function() {
    return {
      count: 0
    }
  },
  _Ctor: {},
  props: {
    'xContent': {
      type: function Number() { ... }
    },
    'name': {
      type: function String() { ... }
    },
    'kk': {'name': 'kongzhi11'}
    }
  },
  inject: {
    'foo': {
      default: 'foo',
      from: 'bar'
    }
  },
  components: {},
  directives: {},
  filters: {}
};

因此执行代码:

if (Sub.options.props) {
  initProps(Sub)
}

从上面的数据我们可以知道 Sub.options.props 有该值的,因此会调用 initProps 函数。代码如下:

function initProps (Comp) {
  const props = Comp.options.props
  for (const key in props) {
    proxy(Comp.prototype, `_props`, key)
  }
}

因此 const props = Comp.options.props; 

即 props = {
  'xContent': {
    type: function Number() { ... }
  },
  'name': {
    type: function String() { ... }
  },
  'kk': {'name': 'kongzhi11'}
  }
}

使用for in 循环该props对象。最后调用 proxy 函数, 该函数的作用是使用 Object.defineProperty来监听对象属性值的变化。
该proxy函数代码如下所示:

该proxy函数代码在 vue/src/core/instance/state.js 中,代码如下所示:

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

继续执行代码如下:

if (Sub.options.computed) {
  initComputed(Sub)
}

判断是否有computed选项, 如果有的话,就调用 initComputed(Sub); 该函数代码在 vue/src/core/instance/state.js; 该代码源码先不分析, 会有对应的章节分析的。最后代码一直到最后, 会返回Sub对象, 该对象值就变为如下了:

Sub = {
  cid: 1,
  component: func,
  directive: func,
  extend: func,
  extendOptions: {
    name: 'button-counter',
    template: '<button @click="count++">You clicked me {{ count }} times.</button>',
    data: function() {
      return {
        count: 0
      }
    },
    _Ctor: {},
    props: {
      'xContent': {
        type: function Number() { ... }
      },
      'name': {
        type: function String() { ... }
      },
      'kk': {'name': 'kongzhi11'}
      }
    },
    inject: {
      'foo': {
        default: 'foo',
        from: 'bar'
      }
    }
  },
  filter,func,
  mixin: func,
  options: {
    name: 'button-counter',
    template: '<button @click="count++">You clicked me {{ count }} times.</button>',
    data: function() {
      return {
        count: 0
      }
    },
    _Ctor: {},
    props: {
      'xContent': {
        type: function Number() { ... }
      },
      'name': {
        type: function String() { ... }
      },
      'kk': {'name': 'kongzhi11'}
      }
    },
    inject: {
      'foo': {
        default: 'foo',
        from: 'bar'
      }
    },
    components: {},
    directives: {},
    filters: {},
    components: button-counter: f VueComponent,
    _base: f Vue()
    ......
  }
};

注意:在代码中会有如下一句代码; 就是会把我们的组件 'button-counter' 放到 Sub.options.components 组件中。

// enable recursive self-lookup
if (name) {
  Sub.options.components[name] = Sub
}

如上代码执行完成 及 返回完成后,我们再回到 vue/src/core/global-api/assets.js 代码中看接下来的代码:

/* @flow */

import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'

export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

因此 最后代码: this.options[type + 's'][id] = definition; 

this.options = {
  components: {
    KeepAlive: {},
    Transition: {},
    TransitionGroup: {},
    button-counter: ƒ VueComponent(options){}
  },
  directives: {},
  filters: {},
  base: f Vue(){}
};

this.options[type + 's'][id] = this.options['components']['button-counter'] = f VueComponent(options);

最后我们返回 definition 该Vue的实列。即definition的值为如下:

definition = ƒ VueComponent (options) {
  this._init(options);
}

最后我们就会调用 new Vue() 方法来渲染整个生命周期函数了,因此button-counter组件就会被注册上可以调用了。