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


閱讀目錄

在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組件就會被注冊上可以調用了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM