vue系列---Vue組件化的實現原理(八)

閱讀目錄javascript

在Vue中,組件是一個很強大的功能,組件能夠擴展HTML元素,封裝可重用的代碼。好比在頁面當中的某一個部分須要在多個場景中使用,那麼咱們能夠將其抽出爲一個組件來進行復用。組件能夠大大提升了代碼的複用率。html

全部的Vue組件都是Vue的實列,所以它能夠接受Vue中的全部的生命週期鉤子。Vue又分爲全局註冊和局部註冊兩種方式來註冊組件。全局組件它能夠在任何(根)實列中使用的組件,局部組件只能在某一實列中使用的組件。vue

1.1 全局註冊組件html5

  全局註冊有下面兩種方式註冊:
 1. 經過 Vue.component 直接註冊

全局組件可使用 Vue.component(tagName, option); 來註冊組件。 tagName 是自定義的組件名稱, option是組件的一些選項, 好比能夠在option對象中添加以下一些選項:
1) template 表示組件模板。
2) methods 表示組件裏的方法。
3) data 表示組件裏的數據。java

在Vue中, 定義組件名的方式也是有規範的。定義組件名的方式有兩種:jquery

1) 使用kebab-case webpack

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

kebab-case 的含義是: "短橫線分隔命名" 來定義一個組件, 所以咱們在使用該組件的時候, 就如這樣使用: <my-component-name>web

2) 使用 PascalCasejson

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

PascalCase 的含義是: "首字母大寫命名" 來定義一個組件, 咱們在使用該組件時,能夠經過以下兩種方式來使用:<my-component-name>或<MyComponentName>都是能夠的。
那麼通常的狀況下,咱們習慣使用 第一種方式來使用組件的。api

下面以官方列子來舉例如何註冊一個全局組件,簡單的代碼以下:

<!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組件就會被註冊上能夠調用了。

相關文章
相關標籤/搜索