前段时间听说 Vue3.x 要使用 TypeScript 重构了, 本来一直都想研究一下 Vue 的源码, 这次带着夙愿来从头编写一个简单的、现代化的 Vue.
搭建好 TypeScript 开发环境后, 开始了一段 TypeScript 与 Vue 源码的探索之旅.
我使用 ES6 class
创建了一个 Vue 类, 为了实现数据监听, 我使用了 ES2015 中的 proxy
方法来对数据进行封装,
并且将这个 proxy
返回给类的构造方法, 以便于获取 vm
实例.
代码如下
查看源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| interface IOptions { data: () => Record<string, any> }
class Vue { private $options: IOptions = { data: () => ({}) }
constructor(options: IOptions) { this.$options = options const proxy = this.initProxy() return proxy }
initProxy() { const data = this.$options.data ? this.$options.data() : {}
return new Proxy(this, { set(_, key: string, value) { data[key] = value return true }, get(_, key: string) { return data[key] } }) } }
const vm = new Vue({ data() { return { a: 1 } } })
vm.a = 2
console.log(vm.a)
|
在线版本
如果你查看了在线版本, 可以看到, 我们在使用实例属性 vm.a
时报了一个 TS 错误 Property 'a' does not exist on type 'Vue'
,
意思是说 vm
实例上不存在属性 a
.
虽然这段代码可以正常执行, 但是静态类型检查没有通过呀.
没关系, 万物皆 Any, 我们这样
1 2 3 4 5 6 7 8 9 10 11 12
| const vm: any = new Vue({ data() { return { a: 1 } } })
vm.a = 2
console.log(vm.a)
|
Everything is any!
看起来还不错, 代码正常运行了, 静态类型检查也通过了. But …
但是我们使用 TypeScript 的初衷呢? 我们不就是为了类型安全吗, 现在编辑器也不提示 vm
上有哪些属性了,
就算用了 vm.b
也不报错了 [掀桌.jpg]
不行, 我们不能这样.
经过一番思考与探索, 最终我选择求助 overstackflow 上的网友, 在提出我的
问题
后的短短 1 个小时内, 竟然获得了三个回答
其中有个网友提到, TypeScript 并不能知道 Proxy 上可能出现的属性, 必须在运行时才能知道.
但是我的观点是, 我并没有等到运行时才声明才会出现的属性, 而是在类声明之后的调用中声明属性, 我认为这仍然处于编译时的阶段.
不久之后看到了一个新的回答, 他说道这个问题需要两个步骤,
第一个步骤是正确处理 initProxy
的返回值, 它将包含实例中动态声明的属性.
第二步是让类的返回值中也包含这个动态类型和类本身.
在 Proxy 的初始化方法 initProxy()
中, 我们将 target
绑定到了类的 this
中, 以便与我们使用 vm.a
来访问这个 proxy,
它的返回值就是类本身
在构造函数 constractor
中我们将这个 proxy 返回给类, 试图让类的实例也获得 proxy 中声明的类型, 但是上面我们知道了, 这样行不通,
虽然这个实例确实获得了 proxy 的指向, 但是类型并没有被一并获得.
根据 @Titian Cernicova-Dragomir 的回答 我们需要对这种动态的类型使用泛型变量
首先我们需要正确的获取 initProxy() 的返回值类型
查看源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| interface IOptions<T> { data: () => T }
class Vue<T = {}> { private $options: IOptions<T> = { data: () => ({}) } as IOptions<T>
constructor(options: IOptions<T>) { this.$options = options const proxy = this.initProxy() return proxy } public initProxy(): T & Vue<T> { const data = this.$options.data ? this.$options.data() : {}
return new Proxy(this as unknown as T & Vue<T>, { set(_, key: string, value) { data[key] = value return true }, get(_, key: string) { return data[key] } }) } }
const vm = new Vue({ data() { return { a: 1 } } })
vm.initProxy().a
|
在线版本
我们使用泛型变量 T
来告诉 TypeScript 我们即将要声明的 data
的类型, 这个类型变量将会用于声明:
接口 IOptions
的 data
属性的类型
1 2 3
| interface IOptions<T> { data: () => T }
|
proxy
的返回值类型
1 2
| return new Proxy(this as unknown as T & Vue<T>, {})
|
IOptions<T>
这个接口在构造函数中进行了声明, 实例化类时就会获得泛型变量 T
的实际类型
而 proxy
的返回值类型中我们使用到了 TS 中的交叉类型 (Intersection Types), 它会返回这两个类型的所有属性.
@Titian Cernicova-Dragomir 还指出,
即使 TypeScript 允许我们指定 constractor 的返回值, 我们也无力改变这个类的返回值的类型, 所以
我们有两个办法来克服这个限制:
我们将构造函数私有化, 并且声明一个静态方法来实例话这个类, 在这个静态方法中返回类的类型和泛型 T
查看源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| interface IOptions<T> { data: () => T }
class Vue<T = {}> { private $options: IOptions<T> = { data: () => ({}) } as IOptions<T>
private constructor(options: IOptions<T>) { this.$options = options const proxy = this.initProxy() return proxy } static create<T>(data: IOptions<T>): Vue<T> & T { return new Vue<T>(data) as unknown as Vue<T> & T } initProxy(): T & Vue<T> { const data = this.$options.data ? this.$options.data() : {}
return new Proxy(this as unknown as T & Vue<T>, { set(_, key: string, value) { data[key] = value return true }, get(_, key: string) { return data[key] } }) } }
const vm = Vue.create({ data() { return { a: 1 } } })
vm.a = 2
|
在线版本
这种办法解决了我们的问题, 但是不完美. 我们实例话 Vue 类的时候, 需要这样写
1
| const vm = Vue.create({})
|
这一点都不 OOP ~ [掀桌.jpg]x2
查看源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| interface IOptions<T> { data: () => T }
class _Vue<T = {}> { private $options: IOptions<T> = { data: () => ({}) } as IOptions<T>
private constructor(options: IOptions<T>) { this.$options = options const proxy = this.initProxy() return proxy } initProxy(): Vue<T> { const data = this.$options.data ? this.$options.data() : {}
return new Proxy(this as unknown as Vue<T>, { set(_, key: string, value) { data[key] = value return true }, get(_, key: string) { return data[key] } }) } }
type Vue<T> = _Vue<T> & T const Vue: new <T>(data: IOptions<T>) => Vue<T> = _Vue as any
const vm = new Vue({ data() { return { a: 1 } } })
vm.a = 2
|
在线版本
完美的解决了我们的问题, 虽然这个办法需要声明一个额外的类型, 但是由于我们使用的是封装的高阶方法, 只要不影响使用就行啦!
再次感谢 @Titian Cernicova-Dragomir !