解讀 vue-class-component 源碼實現原理

導讀

使用過一段時間 class 來定義組件,要用 vue-property-decorator 提供定義好的裝飾器,輔助完成所需功能,對這個過程好奇,就研究了源碼。內部主要依靠 vue-class-component 實現,因此將重點放在對 vue-class-component 的解讀上。javascript

本文主要內容有:前端

  • 裝飾器做用在 class 定義的組件,發生了什麼
  • 解讀 Component 裝飾器實現過程
  • vue-property-decorator 中如何擴展裝飾器

裝飾器做用在 class 定義的組件,發生了什麼

沒有使用 class 方式定義組件時,一般導出一個選項對象:vue

<script>
export default {
  props: {
    name: String
  },
  data() {
    return {
      message: '新消息'
    }
  },
  watch: {
    message(){
      console.log('message改變觸發')
    }
  },
  computed:{
    hello: {
      get(){
        return this.message + 'hello';
      },
      set(newValue){}
    }
  },
  methods:{
    clickHandler(){}
  }
  mounted(){
    console.log('掛載完畢');
  }
}
</script>

這個對象告訴 Vue 你要作什麼事情,須要哪些功能。 根據字段的不一樣做用,把須要添加的屬性和方法,寫在指定的位置,例如,須要響應式數據寫在 data 中、計算屬性寫在 computed 中、事件函數寫在 methods中、直接寫生命週期函數等 。Vue 內部會調用 Vue.extend() 建立組件的構造函數,以便在模板中使用時,經過構造函數初始化此組件。java

若是使用了 class 來定義組件,上面的字段可省略,但要符合 Vue 內部使用數據的規則,就須要重組這些數據。git

定義 class 組件:github

<script lang="ts">
class Home extends Vue {
  message = '新數據';

  get hello(){
    return this.message + 'hello';
  }
  set hello(newValue){}

  clickHandler(){}
  mounted(){}
}

Home.prototype.age = '年齡'

</script>

message 做爲響應式的數據,應該放在 data 中,但問題是 message 寫在類中,爲初始化後實例上的屬性,就要想辦法在初始化後拿到 message,放在 data 中。app

age 直接寫在原型上,值不是函數,也應該放在 data 中。函數

hello 寫了訪問器,做爲計算屬性,寫在 computed 中;clickHandler做爲方法,寫在 methods 中;mounted 是生命週期函數,掛載原型上就能夠,不須要動。這三個都是方法,定義在原型上,須要拿到原型對象,找到這三類方法,按照特性放在指定位置。工具

這就引起一個問題,怎麼把這些定義的屬性放在 Vue 須要解析的數據中,「上帝的歸上帝,凱撒的歸凱撒」。學習

最終處理成這樣:

{
  data:{
    message: '新數據',
    age: '年齡'
  },
  methods:{
    clickHandler(){}
  },
  computed:{
    hello:{
      get(){
        return this.message + 'hello';
      }
    }
  },
  mounted(){}
}

最好是無入侵式的添加功能,開發者無感知,正常寫業務代碼,提供封裝好功能來完成歸類數據這件事。

裝飾器模式,在不改變自身對象的基礎上,動態增長額外的功能,這個模式的思路符合上述內容的要求。具體可參考一篇文章詳細瞭解,裝飾者模式和TypeScript裝飾器

vue-class-component 的代碼使用 ts 書寫,若是對 ts 語法不熟悉,能夠忽略定義的類型,直接看函數體內的邏輯,不影響閱讀。或者直接看打包後,沒有壓縮的代碼,也很少,大約200行左右。

本文分析的代碼主要文件在:倉庫地址

解讀 Component 裝飾器

先來看大體結構和如何使用:

function Component(options) {
    // options 是 function類型,是要裝飾的類
  if (typeof options === 'function') {
    return componentFactory(options);
  }
  
  // 執行後,這個函數做爲裝飾器函數,接收要裝飾的類
  // options 爲傳入的選項數據。
  return function (Component) {
    return componentFactory(Component, options);
  };
}
// 使用1
@Component
class Home Extend Vue {}

// 使用2
@Component({
  components:{}
  data:{newMessage: '增長的消息'},
  methods:{
    moveHandler(){}
  },
  computed:{
    reveserMessage(){
        return this.newMessage + '翻轉'
    }
  }
  // ... vue中選項對象其餘值
})
class Home Extend Vue {}

Component 做爲裝飾器函數,接受的 options 就是要裝飾的類 Homejs 中類不過是一種語法糖,typeof Home 獲得爲 function 類型。

Component 函數做爲工廠函數,執行並傳入參數 options(爲了稱呼方便,後面把這個參數叫作 裝飾器選項數據),工廠函數執行後,返回裝飾器函數,一樣是接受要裝飾的類 Home

從代碼中能夠看出來,都調用了 componentFactory ,第一個參數爲要裝飾的類,第二參數可選,傳入的話就是裝飾器選項數據。

解讀 componentFactory 函數

從名字上能夠看出來,componentFactory 用來產生組件的工廠,通過一系列的執行後,返回新的組件函數。省略其餘,先看關鍵代碼 代碼地址

function componentFactory(Component) {
  // 省略其餘代碼...
  
  // 參數爲兩個,說明第二個是傳入的部分選項數據;
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  // 獲得繼承的父類,不出意外爲 Vue 
  var superProto = Object.getPrototypeOf(Component.prototype);
  // 若是原型鏈上確實有 Vue,則獲得構造函數;不爲 Vue,則直接使用 Vue;
  // 目的是爲了找到 extend 函數。
  var Super = superProto instanceof Vue ? superProto.constructor : Vue;
  // 根據選項對象,新建一個組件的構造函數
  var Extended = Super.extend(options);
  // 返回新的構造函數
  return Extended;
}

驗證了上面的猜想,調用了 Vue.extend 返回新的組件函數。但在返回以前,要處理原來組件上的屬性,和原型上的方法。

歸類原型上方法

首先對選項上的方法歸類,方法歸 methods;非方法歸 data;有訪問器歸 computed

// 須要忽略的屬性
const $internalHooks = [
  'data',
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeDestroy',
  'destroyed',
  'beforeUpdate',
  'updated',
  'activated',
  'deactivated',
  'render',
  'errorCaptured', // 2.5
  'serverPrefetch' // 2.6
]

function componentFactory(Component) {
  // 其餘代碼省略...
  // 拿到原型對象
  const proto = Component.prototype
  // 返回對象上全部自身屬性,包括不可枚舉的屬性
  Object.getOwnPropertyNames(proto).forEach(function (key) {
    // 構造函數,不作處理
    if (key === 'constructor') {
      return
    }

    // 鉤子函數之類的屬性,直接賦值到 options對象上,不須要歸類
    if ($internalHooks.indexOf(key) > -1) {
      options[key] = proto[key]
      return
    }
    // 拿到對應屬性的描述對象,用這個方法能避免繼續查找原型鏈上的屬性
    const descriptor = Object.getOwnPropertyDescriptor(proto, key);
    // 若是此屬性的值不爲 undefined,說明有值
    if (descriptor.value !== void 0) {
      // methods
      // 若是爲函數,則直接歸爲 methods
      if (typeof descriptor.value === 'function') {
        (options.methods || (options.methods = {}))[key] = descriptor.value
      } else {
        // 若是值不爲函數,則歸爲data,這裏採用 mixins,混合數據的方式來作
        (options.mixins || (options.mixins = [])).push({
          data (this: Vue) {
            return { [key]: descriptor.value }
          }
        })
      }
    } else if (descriptor.get || descriptor.set) {
     // value 爲空,可是有 get或set的訪問器,則歸爲computed
      (options.computed || (options.computed = {}))[key] = {
        get: descriptor.get,
        set: descriptor.set
      }
    }
  })
}

從上述代碼能夠看出來,拿到屬性對應的描述對象,根據屬性對應的值,進行類型判斷,來決定歸爲哪一類。

值得注意的是這段代碼,目的是把非函數的屬性,混合在 data 中:

if(typeof descriptor.value === 'function'){/*省略*/}
else{// 處理原型上不是函數的狀況
  (options.mixins || (options.mixins = [])).push({
    data (this: Vue) {
      return { [key]: descriptor.value }
    }
  })
}

通常寫在類中的只有是函數才能放在原型上,但有別的方式能夠把非函數的值添加到原型上:

// 第一種,直接給原型添加屬性
Home.prototype.age = 18;

// 第二種,用屬性裝飾器
function ageDecorator(prototype, key){
  return {  // 裝飾器返回描述對象,會在 prototype增長key這個屬性
    enumerable: false,
    value: 18
  }
}
class Home extends Vue {
  @ageDecorator
  age: number = 18;
}

若是用了 ts 的屬性裝飾器,並返回描述對象,就會在 prototype 增長這個屬性,因此在上面 componentFactory 源碼中要處理這種狀況,通常在項目中比較少見。

處理實例上的屬性

寫在類中的屬性,不添加在原型上,只有經過獲得實例後拿到這些值,能夠沿着這個思路進行分析。

先看實例上屬性的狀況:

class Home {
  message: '新消息',
  clickHandler(){}
}
let home = new Home();
console.log(home);

// 打印實例,簡化後:
{
  message: "新消息"
  __proto__:
    constructor: class Home
    clickHandler: ƒ clickHandler()
    __proto__: Object
}

componentFactory 中作了單獨的處理:

function componentFactory(Component){
    // 省略其餘代碼
  ;(options.mixins || (options.mixins = [])).push({
    data () {
      return collectDataFromConstructor(this, Component)
    }
  })
}

這裏依然使用混合 data 的方式,混合功能很強大,敲黑板記下來。mixins 會在初始化組件時,調用 data 對應的函數,獲得要混合的數據,又調用了 collectDataFromConstructor,傳入 this,爲組件實例,跟平時寫項目在 mounted 中使用的那個 this 同樣,都爲渲染組件的實例;第二參數爲 Component,是原來裝飾的類,上面例子中就是 Home 類。

分析 collectDataFromConstructor 函數

這個函數的目的是把原來裝飾的類,初始以後,拿到實例上的屬性組成對象返回。代碼地址

來看代碼:

// 用來收集被裝飾類中定義的屬性
// vm 爲要渲染的組件實例
// Component 爲原來要裝飾的組件類

function collectDataFromConstructor(vm, Component) {
  // 先保存原有的 _init,目的是不執行 Vue上的 _init 作其餘初始化動做
  var originalInit = Component.prototype._init;

  // 在被裝飾的類的原型上手動增長 _init,在Vue實例化事內部會調用
  Component.prototype._init = function () {
    var _this = this;

    // 拿到渲染組件對象上的屬性,包括不可枚舉的屬性,包含組件內定義的 $開頭屬性 和 _開頭屬性,還有自定義的一些方法
    var keys = Object.getOwnPropertyNames(vm); 

    // 若是渲染組件含有,props,可是並無放在原組件實例上,則添加上
    if (vm.$options.props) {
      for (var key in vm.$options.props) {
        if (!vm.hasOwnProperty(key)) {
          keys.push(key);
        }
      }
    }
    // 把給原組件實例上 Vue 內置屬性設置爲不可遍歷。
    keys.forEach(function (key) {
      if (key.charAt(0) !== '_') {
        Object.defineProperty(_this, key, {
          get: function get() {
            return vm[key];
          },
          set: function set(value) {
            vm[key] = value;
          },
          configurable: true
        });
      }
    });
  }; 
  
  // 手動初始化要包裝的類,目的是拿到初始化後實例
  var data = new Component(); 
  // 從新還原回原來的 _init,防止一直引用原有的實例,形成內存泄漏
  Component.prototype._init = originalInit;
    
 // 從新定義對象
  var plainData = {};
 // Object.keys 拿到可被枚舉的屬性,添加到對象中
  Object.keys(data).forEach(function (key) {
    if (data[key] !== undefined) {
      plainData[key] = data[key];
    }
  });
  return plainData;
}

具體要作的話,經過 new Component() 獲得被裝飾類的實例,但要注意,Component 繼承了 Vue 類,初始化後實例上有不少 Vue 內部添加上的屬性,好比 &dollar;options&dollar;parent&dollar;attrs&dollar;listeners&dollar;data 等等,還有以 _ 開頭的屬性,_watcher_renderProxy 等等,還有咱們須要的屬性。這裏只是簡單舉幾個屬性,你能夠手動初始化,在控制檯打印輸出看一下。

_ 開頭的屬性,是內置方法,不可被枚舉;以 &dollar; 開頭的屬性,也是內置方法,可是可被枚舉。若是直接循環實例,會拿到以 &dollar; 開頭的屬性,這並非咱們須要的。

那怎麼辦呢?代碼中給了答案,在初始化一系列組件內置的屬性後,組件內部會調用 Component.prototype._init 方法,可經過改寫這個方法,來處理屬性爲不可枚舉。

最後經過 Object.keys() 獲得可以被遍歷的屬性。

上面拐的彎比較多,不免看蒙了,根據核心意思,簡化以下:

原來有個組件:

class Home {
  message: '新消息'
}

如今有個須要渲染的組件,要把上面定義在 Home 中的 message 寫在現有組件的 data 中:

const App = Vue.extend({
  // 混合功能
  mixins:[{
    data(){
        // 初始化後拿到實例,就能拿到 message 屬性
        let data = new Home(); 
        let plainData = {};
        Object.keys(data).forEach(function (key) {
          if (data[key] !== undefined) {
            plainData[key] = data[key];
          }
        });
        return plainData;
    }
  }],
  data(){
    return {
        other: '其餘data'
    }
  }
})

new App().$mounted('#app');

簡化後,是否是清晰不少,本質就是初始類獲得實例,拿屬性組成對象,混合到渲染的組件中。

小的優化點,簡化代碼:

// 保留原有的 _init 方法
var originalInit = Component.prototype._init;
Component.prototype._init = function(){
  // 其餘代碼省略
};
Component.prototype._init = originalInit;

這段代碼,在改寫的 _init 內部使用了外面的引用 vmComponent,就會一直在內存中,爲防止內存泄漏,從新賦回原來的函數。

vue-property-decorator 中如何擴展裝飾器

vue-property-decorator 依賴 vue-class-component 實現,主要用了內部提供的 createDecorator 方法。

若是你想增長更多裝飾器,也能夠經過調用 createDecorator 方法,原理很簡單,就是向選項對象上增長所需數據。

執行 createDecorator 添加的裝飾函數

vue-class-component 中提供了工具函數 createDecorator 容許添加其餘額外的裝飾函數,統一掛載在 Component.__decorators__ 上,並把 options 傳過去,對 options 增長鬚要的屬性,實際上會調用這些裝飾函數,讓這些函數有機會處理 options

function componentFactory(Component) {
 // 省略其餘代碼....
  var decorators = Component.__decorators__;
  if (decorators) {
    decorators.forEach(function (fn) {
      return fn(options);
    });
    delete Component.__decorators__;
  }
}

咱們能夠利用 createDecorator,擴展其餘的裝飾器,vue-property-decorator 內部就是利用這個函數擴展了 @Prop@Watch 等裝飾器。

function createDecorator(factory) {
  return (target, key, index) => {
     // 是函數類型,則爲裝飾的類;
     // 不然,爲原型,經過constructor拿到構造函數
      const Ctor = typeof target === 'function'
          ? target
          : target.constructor;
      if (!Ctor.__decorators__) {
          Ctor.__decorators__ = [];
      }
      // 當爲參數裝飾器時,index爲number
      if (typeof index !== 'number') {
          index = undefined;
      }
      Ctor.__decorators__.push(options => factory(options, key, index));
  };
}s

從源碼中能夠看出來,createDecorator 調用後會返回一個函數,這個函數能夠做爲裝飾器函數,接收的 target 若是是函數類型,說明做爲類裝飾器,target 就是被裝飾的類;不然,獲得的是原型,經過 constructor 拿到構造函數。

向要裝飾的類上添加靜態屬性 decorators,存入一個函數,得到 options

如今來看 vue-property-decoratorwatch 裝飾器的源碼,代碼地址

function Watch(path, options) {
    if (options === void 0) { options = {}; }
    return createDecorator(function (componentOptions, handler) {
        if (typeof componentOptions.watch !== 'object') {
            componentOptions.watch = Object.create(null);
        }
        var watch = componentOptions.watch;
        if (typeof watch[path] === 'object' && !Array.isArray(watch[path])) {
            watch[path] = [watch[path]];
        }
        else if (typeof watch[path] === 'undefined') {
            watch[path] = [];
        }
        watch[path].push({ handler: handler});
    });
}

傳入 createDecorator 的回調函數,會接受兩個參數,componentOptions 爲一個對象,就是在上面 componentFactory 中調用 Component.__decorators__,傳入的對象,目的是向這個對象添加或增長 watch 屬性,給要裝飾的類使用;handler 是函數名字;

這樣使用:

@Component 
class Home extend Vue {
    message='新消息'
    
    @watch('message')
    messageHandler(){
        console.log('當message改變後,執行這裏')
    }
}

通過 @watch 裝飾器處理後,選項對象上會增長一段數據:

{
  watch: {
   message: 'messageHandler'
  },
  methods:{
    messageHandler(){
        console.log('當message改變後,執行這裏')
    }
  }
}

以上即是 vue-property-decorator 增長裝飾器的實現方式,對其餘裝飾器感興趣,能夠看倉庫源碼,作進一步瞭解,思路都大同小異。

以上若有誤差歡迎指正學習,謝謝。~~~~

github博客地址:https://github.com/WYseven/blog,歡迎star。

若是對你有幫助,請關注【前端技能解鎖】:
qrcode_for_gh_d0af9f92df46_258.jpg

相關文章
相關標籤/搜索