从 Vue typings 看 “this”

在 2.5.0 版本中,Vue 大大改进了类型声明系统以更好地使用默认的基于对象的 API。

意味着当我们仅是安装 Vue 的声明文件时,一切也都将会按预期进行:

  • this,就是 Vue;
  • this 属性上,具有 Methods 选项上定义的同名函数属性;
  • 在实例 data、computed、prop 上定义的属性/方法,也都将会出现在 this 属性上;
  • ......

在这篇文章里,我们来谈谈上述背后的故事。

Methods

当我们创建 Vue 实例,并在 Methods 上定义方法时, this 不仅具有 Vue 实例上属性,同时也具有与 Methods 选项上同名的函数属性:

new Vue({
  methods: {
    test () {
     this.$el   // Vue 实例上的属性
    }
  },
  
  created () {
    this.test() // methods 选项上同名的方法
    this.$el    // Vue 实例上的属性
  }
})
复制代码

为了探究其原理,我们把组件选项的声明改写成以下方式:

定义 Methods:

// 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>
复制代码

组件选项(同样也需要传实例):

interface ComponentOption<V> {
  methods: Methods<V>,
  created?(this: V): void
}
复制代码

我们可以使用它:

declare function testVue<V extends Vue>(option: ComponentOption<V>): V
复制代码

此种情形下,我们必须将组件实例的类型显式传入,从而使其编译通过:

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏web前端教室

web前端零基础课-0908*福祥-学习笔记

学了部分js内容后,完成了网站首页部分动态效果(搜索栏、侧边导航条、轮播图),先用最基本的,冗余最多的一步步实现;后面对Js进行了初步的封装,重新构建了Js文件...

843
来自专栏静晴轩

JavaScript 之 this 详解

JavaScript作为一种脚本语言身份的存在,因此被很多人认为是简单易学的。然而情况恰恰相反,JavaScript支持函数式编程、闭包、基于原型的继承等高级功...

4165
来自专栏Golang语言社区

Golang 语言--map 用range遍历不能保证顺序输出

按照之前我对map的理解,map中的数据应该是有序二叉树的存储顺序,正常的遍历也应该是有序的遍历和输出,但实际试了一下,却发现并非如此,网上查了下,发现从Go1...

4218
来自专栏C/C++基础

C++编码格式建议

每个人都可能有自己的代码风格和格式,但如果一个项目中的所有人都遵循同一风格的话,这个项目就能更顺利地进行。每个人未必能同意下述的每一处格式规则,而且其中的不少规...

1142
来自专栏个人随笔

JavaScript 网页脚本语言 由浅入深

1)基础 学习目的: 1. 客户端表单验证 2. 页面动态效果 3. jQuery的基础 什么是JavaScript? 一种描述性语言,也是一种基于对象和事件驱...

36110
来自专栏Java帮帮-微信公众号-技术文章全总结

JavaWeb03-轻松理解JS(Java真正的全栈开发)

? 一.js常用对象 ljs中的常见对象有以下几个: Boolean Number String Array 数组 Date 日期 Math 数学 RegEx...

28312
来自专栏繁花云

liunx下sed命令的用法

单引号里面,s表示替换,三根斜线中间是替换的样式,特殊字符需要使用反斜线”\”进行转义,但是单引号”‘”是没有办法用反斜线”\”转义的,这时候只要把命令中的单...

740
来自专栏C++

python笔记:#011#循环

1812
来自专栏全沾开发(huā)

如何在ES5与ES6环境下处理函数默认参数

1474
来自专栏转载gongluck的CSDN博客

python笔记:#011#循环

循环 目标 程序的三大流程 while 循环基本使用 break 和 continue while 循环嵌套 01. 程序的三大流程 在程序开发中,一共有三种流...

3604

扫码关注云+社区