從 Vue typings 看 「this」

在 2.5.0 版本中,Vue 大大改進了類型聲明系統以更好地使用默認的基於對象的 API。html

意味着當咱們僅是安裝 Vue 的聲明文件時,一切也都將會按預期進行:vue

  • this,就是 Vue;
  • this 屬性上,具備 Methods 選項上定義的同名函數屬性;
  • 在實例 data、computed、prop 上定義的屬性/方法,也都將會出如今 this 屬性上;
  • ......

在這篇文章裏,咱們來談談上述背後的故事。git

Methods

當咱們建立 Vue 實例,並在 Methods 上定義方法時, this 不只具備 Vue 實例上屬性,同時也具備與 Methods 選項上同名的函數屬性:github

new Vue({
  methods: {
    test () {
     this.$el   // Vue 實例上的屬性
    }
  },
  
  created () {
    this.test() // methods 選項上同名的方法
    this.$el    // Vue 實例上的屬性
  }
})
複製代碼

爲了探究其原理,咱們把組件選項的聲明改寫成如下方式:typescript

定義 Methods:app

// methods 是 [key: string]: (this: Vue) => any 的集合
type Methods = Record<string, (this: Vue) => any>
複製代碼

這會存在一個問題,Methods 上定義的方法裏的 this,所有都是 Vue 構造函數上的方法,而不能訪問咱們自定義的方法。 咱們須要把 Vue 實例傳進去:函數

type Methods<V> = Record<string, (this: V) => any>
複製代碼

組件選項(一樣也須要傳實例):ui

interface ComponentOption<V> {
  methods: Methods<V>,
  created?(this: V): void
}
複製代碼

咱們可使用它:this

declare function testVue<V extends Vue>(option: ComponentOption<V>): V 複製代碼

此種情形下,咱們必須將組件實例的類型顯式傳入,從而使其編譯經過:spa

interface TestComponent extends Vue {
  test (): void
}

testVue<TestComponent>({
  methods: {
    test () {}
  },

  created () {
    this.test() // 編譯經過
    this.$el    // 經過
  }
})
複製代碼

這有點麻煩,爲了使它能按咱們預期的工做,咱們定義了一個額外的 interface。

在 Vue 的聲明文件裏,使用了一種簡單的方式:經過使用 ThisType<T> 映射類型,讓 this 具備所須要的屬性。

在 TypeScript 倉庫 ThisType<T>PR 下,有一個使用例子:

在這個例子中,經過對 methods 的值使用 ThisType<D & M>,從而 TypeScript 推導出 methods 對象中 this 便是: { x: number, y: number } & { moveBy(dx: number, dy: number ): void }

與此相似,咱們可讓 this 具備 Methods 上定義的同名函數屬性:

type DefaultMethods<V> = Record<string, (this: V) => any>

interface ComponentOption<
  V,
  Methods = DefaultMethods<V>
> {
  methods: Methods,
  created?(): void
}

declare function testVue<V extends Vue, Methods> (
  option: ComponentOption<V, Methods> & ThisType<V & Methods>
): V & Methods

testVue({
  methods: {
    test () {}
  },
  created () {
    this.test() // 編譯經過
    this.$el    // 實例上的屬性
  }
})
複製代碼

在上面代碼中,咱們:

  • 建立了一個 ComponentOption interface,它有兩個參數,當前實例 Vue 與 默認值是 [key: string]: (this: V) => any 的 Methods。
  • 定義了一個函數 testVue,同時將範型 V, Methods 傳遞給 ComponentOption 與 ThisTypeThisType<V & Methods> 標誌着實例內的 this 便是 V 與 Methods 的交叉類型。
  • 當 testVue 函數被調用時,TypeScript 推斷出 Methods 爲 { test (): void },從而在實例內 this 便是:Vue & { test (): void };

Data

得益於上文中的 ThisType<T>,Data 的處理有點相似與 Methods,惟一不一樣之處 Data 可有兩種不一樣類型,Object 或者 Function。它的類型寫法以下:

type DefaultData<V> =  object | ((this: V) => object)
複製代碼

一樣,咱們也把 ComponentOption 與 testVue 稍做修改

interface ComponentOption<
  V,
  Data = DefaultData<V>,
  Methods = DefaultMethods<V>
> {
  data: Data
  methods?: Methods,
  created?(): void
}

declare function testVue<V extends Vue, Data, Methods> ( option: ComponentOption<V, Data, Methods> & ThisType<V & Data & Methods> ): V & Data& Methods 複製代碼

當 Data 是 Object 時,它能正常工做:

testVue({
  data: {
    testData: ''
  },
  created () {
    this.testData // 編譯經過
  }
})
複製代碼

當咱們傳入 Function 時,它並不能:

TypeScript 推斷出 Data 是 (() => { testData: string }),這並非指望的 { testData: string },咱們須要對函數參數 options 的類型作少量修改,當 Data 傳入爲函數時,取函數返回值:

declare function testVue<V extends Vue, Data, Method>(
  option: ComponentOption<V, Data | (() => Data), Method> & ThisType<V & Data & Method>
): V  & Data & Method
複製代碼

這時候編譯能夠經過:

testVue({
  data () {
    return {
      testData: ''
    }
  },

  created () {
    this.testData // 編譯經過
  }
})
複製代碼

Computed

Computed 的處理彷佛有點棘手:它與 Methods 不一樣,當咱們在 Methods 中定義了一個方法,this 也會含有相同名字的函數屬性,而在 Computed 中定義具備返回值的方法時,咱們指望 this 含有函數返回值的同名屬性。

舉個例子:

new Vue({
  computed: {
    testComputed () {
      return ''
    }
  },
  methods: {
    testFunc () {}
  },

  created () {
    this.testFunc()   // testFunc 是一個函數
    this.testComputed // testComputed 是 string,並非一個返回值爲 string 的函數
  }
})

複製代碼

咱們須要一個映射類型,把定義在 Computed 內具備返回值的函數,映射爲 key 爲函數名,值爲函數返回值的新類型:

type Accessors<T> = {
  [K in keyof T]: (() => T[K])
}
複製代碼

Accessors<T> 將會把類型 T,映射爲具備相同屬性名稱,值爲函數返回值的新類型,在類型推斷時,此過程相反。

接着,咱們補充上例:

// Computed 是一組 [key: string]: any 的集合
type DefaultComputed = Record<string, any>

interface ComponentOption<
  V,
  Data = DefaultData<V>,
  Computed = DefaultComputed,
  Methods = DefaultMethods<V>
> {
  data?: Data,
  computed?: Accessors<Computed>
  methods?: Methods,
  created?(): void
}

declare function testVue<V extends Vue, Data, Compted, Methods> (
  option: ComponentOption<V, Data | (() => Data), Compted, Methods> & ThisType<V & Data & Compted & Methods>
): V & Data & Compted & Methods

testVue({
  computed: {
    testComputed () {
      return ''
    }
  },
  created () {
    this.testComputed // string
  }
})

複製代碼

當調用 testVue 時,咱們傳入一個屬性爲 testComputed () => '' 的 Computed,TypeScript 會嘗試將類型映射至 Accessors<T>,從而推導出 Computed 便是 { testComputed: string }

此外,Computed 具備另外一個寫法:get 與 set 形式,咱們只須要把映射類型作相應補充便可:

interface ComputedOptions<T> {
  get?(): T,
  set?(value: T): void
}

type Accessors<T> = {
  [K in keyof T]: (() => T[K]) | ComputedOptions<T[K]> } 複製代碼

Prop

在上篇文章在 Vue 中使用 TypeScript 的一些思考(實踐)中,咱們已經討論了 Prop 的推導,在此再也不贅述。

最後

此篇文章是對 Vue typings 的一次簡單解讀,但願你們看得懂源碼時,不要忘記了 Vue typings,畢竟 Vue typings 纔是給程序行爲以提示和約束的關鍵。

參考

  • https://github.com/Microsoft/TypeScript/pull/14141
  • http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#mapped-types
  • https://github.com/vuejs/vue/blob/dev/types/options.d.ts
相關文章
相關標籤/搜索