本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
1 前言
前面写了一篇vue2class 写法源码分析 vue-class-component(1), 但是光有那个库是还不够的, 因为它只有一个类装饰器, 只提供了选项中的
data
,methods
,computed
的处理,对于watch
和props
等都没有处理, 这些都将在这个库中解决
在这个库中, 导出了一堆方法装饰器
2 再看 vue-class-component
它还导出了两个方法
2.1 mixins
export function mixins(...Ctors: VueClass<Vue>[]): VueClass<Vue> {
return Vue.extend({
// mixins的是一个函数的话, 会被认为是 Vue 的子类, 真正 mixins 的是 Ctor.options
mixins: Ctors,
})
}
2.2 createDecorator
在
vue-class-component
导出了这个函数, 提供注册在处理完类装饰器后生成options
时的回调
export function createDecorator(
factory: (options: ComponentOptions<Vue>, key: string, index: number) => void
): VueDecorator {
// 传入的函数, 作为 push 到 __decorators__ 中的回调
// 这个返回的函数会作为 装饰器函数, 它没有返回值
return (target: Vue | typeof Vue, key?: any, index?: any) => {
// target 是原型对象, target.constructor 指向构造函数, 就是 @Component 装饰的类
const Ctor =
typeof target === 'function'
? (target as DecoratedClass)
: (target.constructor as DecoratedClass)
if (!Ctor.__decorators__) {
Ctor.__decorators__ = []
}
if (typeof index !== 'number') {
index = undefined
}
// 保存起这个回调函数, 它的用处, 在上篇文章中有写
Ctor.__decorators__.push((options) => factory(options, key, index))
}
}
3 vue-property-decorator 导出了好多方法装饰器
3.1 Emit
// 匹配 `非单词边界` 后 `跟大写字母` 'aB' => 'B', 'AB' => 'B', 'ABC' => 'B'和'C', 'ABC DE' => 'B','C'和'E'
const hyphenateRE = /\B([A-Z])/g
const hyphenate = (str: string) => str.replace(hyphenateRE, '-$1').toLowerCase()
/**
* decorator of an event-emitter function
* @param event The name of the event
* @return MethodDecorator
*/
export function Emit(event?: string) {
return function (_target: Vue, propertyKey: string, descriptor: any) {
// 驼峰转连线符
const key = hyphenate(propertyKey)
// 保存旧函数
const original = descriptor.value
// 赋值新函数
descriptor.value = function emitter(...args: any[]) {
const emit = (returnValue: any) => {
// 触发的事件名
const emitName = event || key
// 如果旧函数返回有值, 把它放在最前面
returnValue ?? args.unshift(returnValue)
// 触发事件
this.$emit(emitName, ...args)
}
// 调用旧函数
const returnValue: any = original.apply(this, args)
// 如果旧函数返回 Promise
if (isPromise(returnValue)) {
returnValue.then(emit)
} else {
emit(returnValue)
}
return returnValue
}
}
}
function isPromise(obj: any): obj is Promise<any> {
return obj instanceof Promise || (obj && typeof obj.then === 'function')
}
用法
<!-- 父组件 -->
<template>
<div>
<Emit @click="clickEmit" @fn="clickEmit"></Emit>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import Emit from './emit.vue'
@Component({
components: {
Emit,
},
})
class Demo extends Vue {
clickEmit(...args) {
console.log(args)
}
}
export default Demo
</script>
<!-- 子组件 -->
<template>
<div class="home">
<div @click="emit('后面传的参')">emit 装饰器命名</div>
<div @click="fn('后面传的参')">emit 函数命名</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Emit } from 'vue-property-decorator'
@Component
export default class Home extends Vue {
@Emit('click')
emit() {
console.log('emit 装饰器命名')
return '前面传的参'
}
@Emit()
fn() {
console.log('emit 函数命名, 触发 fn, 没有返回值')
}
}
</script>
3.1.1 对比
// 装饰器用法
import { Component, Vue, Emit } from 'vue-property-decorator'
@Component
export default class Demo extends Vue {
@Emit('emit-demo')
emitFn() {
// ...
console.log('emitFn')
// ...
}
}
// options 写法
export default {
methods: {
oldEmitFn() {
// ...
console.log('emitFn')
// ...
},
emitFn(...args) {
const returnValue = this.oldEmitFn(...args)
const emit = (returnValue) => {
returnValue ?? args.unshift(returnValue)
// 只有这里不同, 如果 @emit() 传入了事件名就使用这个事件名, 否则使用函数名
const emitName = 'emit-demo'
this.$emit(emitName, ...args)
}
if (isPromise(returnValue)) {
returnValue.then(emit)
} else {
emit(returnValue)
}
},
},
}
3.2 Model
import Vue, { PropOptions } from 'vue2'
import { createDecorator } from 'vue-class-component'
import { Constructor } from 'vue2/types/options'
/**
* decorator of model
* @param event event name
* @param options options
* @return PropertyDecorator
*/
export function Model(
event?: string,
options: PropOptions | Constructor[] | Constructor = {}
) {
//
return (target: Vue, key: string) => {
// ==== 装饰器函数开始 ===
const factory = (componentOptions, k) => {
// 修改 props 和 model
componentOptions.props || ((componentOptions.props = {}) as any)
componentOptions.props[k] = options
componentOptions.model = { prop: k, event: event || k }
}
createDecorator(factory)(target, key)
// ==== 装饰器函数结束 ===
}
}
3.2.1 对比
// 装饰器用法
import { Component, Vue, Model } from 'vue-property-decorator'
@Component
export default class Demo extends Vue {
@Emit('event', { type: Object })
eventName
}
// options 写法
export default {
props: {
event: { type: Object },
},
model: {
prop: 'eventName',
event: 'event',
},
}
3.3 ModelSync
import Vue, { PropOptions } from 'vue2'
import { createDecorator } from 'vue-class-component'
import { Constructor } from 'vue2/types/options'
/**
* decorator of synced model and prop
* @param propName the name to interface with from outside, must be different from decorated property
* @param event event name
* @param options options
* @return PropertyDecorator
*/
export function ModelSync(
propName: string,
event?: string,
options: PropOptions | Constructor[] | Constructor = {}
) {
return (target: Vue, key: string) => {
const factory = (componentOptions, k) => {
componentOptions.props || ((componentOptions.props = {}) as any)
componentOptions.props[propName] = options
componentOptions.model = { prop: propName, event: event || k }
// 比着 Model 多传入了一个 propName 多处理了一下 computed
componentOptions.computed || (componentOptions.computed = {})
componentOptions.computed[k] = {
get() {
return (this as any)[propName]
},
set(value: any) {
this.$emit(event, value)
},
}
}
createDecorator(factory)(target, key)
}
}
3.3.1 对比
// 装饰器写法
import { Component, Vue, ModelSync } from 'vue-property-decorator'
@Component
export default class Demo extends Vue {
@ModelSync('propName', 'event', { type: Object })
eventName!: any
}
// options写法
export default {
props: {
propName: { type: Object },
},
model: {
prop: 'eventName',
event: 'event',
},
computed: {
eventName: {
get() {
return this.propsName
},
set(value) {
this.$emit('event', value)
},
},
},
}
3.4 Prop
import Vue, { PropOptions } from 'vue2'
import { createDecorator } from 'vue-class-component'
import { Constructor } from 'vue2/types/options'
/**
* decorator of a prop
* @param options the options for the prop
* @return PropertyDecorator | void
*/
export function Prop(options: PropOptions | Constructor[] | Constructor = {}) {
return (target: Vue, key: string) => {
createDecorator((componentOptions, k) => {
componentOptions.props || ((componentOptions.props = {}) as any)
// 简简单单, 就是给 props 增加一个 属性, k 是装饰的方法名, options 是 Prop 的参数
componentOptions.props[k] = options
})(target, key)
}
}
3.4.1 对比
// 装饰器写法
import { Component, Vue, Prop } from 'vue-property-decorator'
@Component
export default class Demo extends Vue {
@Prop({ type: Object })
propName!: any
}
// options写法
export default {
props: {
propName: { type: Object },
},
}
3.5 PropSync
import Vue, { PropOptions } from 'vue2'
import { createDecorator } from 'vue-class-component'
import { Constructor } from 'vue2/types/options'
/**
* decorator of a synced prop
* @param propName the name to interface with from outside, must be different from decorated property
* @param options the options for the synced prop
* @return PropertyDecorator | void
*/
export function PropSync(
propName: string,
options: PropOptions | Constructor[] | Constructor = {}
) {
return (target: Vue, key: string) => {
createDecorator((componentOptions, k) => {
componentOptions.props || (componentOptions.props = {} as any)
componentOptions.props[propName] = options
componentOptions.computed || (componentOptions.computed = {})
componentOptions.computed[k] = {
get() {
return (this as any)[propName]
},
set(this: Vue, value: any) {
this.$emit(`update:${propName}`, value)
},
}
})(target, key)
}
}
3.5.1 对比
// 装饰器写法
import { Component, Vue, PropSync } from 'vue-property-decorator'
@Component
export default class Demo extends Vue {
@PropSync('propName', { type: Object })
computedKey!: any
}
// options写法
export default {
props: {
propName: { type: Object },
},
computed: {
computedKey: {
get() {
return this.propName
},
set(value) {
this.$emit('update:propName', value)
},
},
},
}
3.6 Ref
import Vue from 'vue2'
import { createDecorator } from 'vue-class-component'
/**
* decorator of a ref prop
* @param refKey the ref key defined in template
*/
export function Ref(refKey?: string) {
return createDecorator((options, key) => {
options.computed = options.computed || {}
options.computed[key] = {
cache: false,
get(this: Vue) {
return this.$refs[refKey || key]
},
}
})
}
3.6.1 对比
// 装饰器写法
import { Component, Vue, Ref } from 'vue-property-decorator'
@Component
export default class Demo extends Vue {
@Ref('refKey')
key!: any
}
// options写法
export default {
computed: {
key: {
cache: false,
get() {
return this.$refs.refKey
},
},
},
}
3.7 VModel
import Vue, { PropOptions } from 'vue2'
import { createDecorator } from 'vue-class-component'
/**
* decorator for capturings v-model binding to component
* @param options the options for the prop
*/
export function VModel(options: PropOptions = {}) {
const valueKey: string = 'value'
return createDecorator((componentOptions, key) => {
componentOptions.props || ((componentOptions.props = {}) as any)
componentOptions.props[valueKey] = options
componentOptions.computed || (componentOptions.computed = {})
componentOptions.computed[key] = {
get() {
return (this as any)[valueKey]
},
set(this: Vue, value: any) {
this.$emit('input', value)
},
}
})
}
3.7.1 对比
// 装饰器写法
import { Component, Vue, VModel } from 'vue-property-decorator'
@Component
export default class Demo extends Vue {
@VModel({ type: Object })
computedKey!: any
}
// options写法
export default {
props: {
value: {
type: Object,
},
},
computed: {
computedKey: {
get() {
return this.value
},
set(value) {
this.$emit('input', value)
},
},
},
}
3.8 Watch
import { WatchOptions } from 'vue2'
import { createDecorator } from 'vue-class-component'
/**
* decorator of a watch function
* @param path the path or the expression to observe
* @param WatchOption
* @return MethodDecorator
*/
export function Watch(path: string, options: WatchOptions = {}) {
const { deep = false, immediate = false } = options
return createDecorator((componentOptions, handler) => {
if (typeof componentOptions.watch !== 'object') {
componentOptions.watch = Object.create(null)
}
const watch: any = 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, deep, immediate })
})
}
3.8.1 对比
// 装饰器写法
import { Component, Vue, Watch } from 'vue-property-decorator'
@Component
export default class Demo extends Vue {
@Watch('path', { deep: true })
watchFn() {
// ...
}
}
// options写法
export default {
watch: {
path: {
...{ deep: true },
handler: this.watchFn,
},
},
methods: {
watchFn() {
// ...
},
},
}
4 总结
常用的就俩,
Prop
,Watch
, 其它的还不如直接用options
写法
它就是能帮我们更好得类型推导, 可以脱离
options
的上下跳
举个例子就明白了, 如果看掘金, 没有右侧边导航, 只能上下滚动, 那是多么的麻烦,
class
写法可以很好的类型推导, 在编辑器中, 可以快速定位
想象一下如果没有这个导航定位
5 最后
下一篇计划写
模块化
, 敬请期待, 感觉这篇文章能改到你启发的, 希望给个点赞, 评论, 收藏, 关注...
按照惯例, 附上之前写的几篇文章