# Vue Property Decorator

此文的示例都来自 Vue Property Decorator (opens new window),只是增加了一些自己的理解,作为基础学习文档

主要罗列一下 vue 装饰器的常用方法。官方文档共有以下装饰器/Mixin

  • @Prop
  • @PropSync
  • @Model
  • @ModelSync
  • @Watch
  • Computed
  • @Provide
  • @Inject
  • @ProvideReactive
  • @InjectReactive
  • @Emit
  • @Ref
  • @VModel
  • @Component (provided by vue-class-component)
  • Mixins (the helper function named mixins provided by vue-class-component)

如何使用:

Vue Property Decorator 主要用于 vue2.x + ts。创建 vue 项目的时候,选择 class-style component syntax,就会默认安装好 vue-class-componentvue-property-decorator

# @Prop

语法:@Prop(options: (PropOptions | Constructor[] | Constructor) = {}),不用特意去记,跟原 Props 的参数基本一致,只是写法稍微变了一点

import { Vue, Component, Prop } from 'vue-property-decorator'

@Component
export default class YourComponent extends Vue {
  @Prop(Number) readonly propA: number | undefined
  @Prop({ default: 'default value' }) readonly propB!: string
  @Prop([String, Boolean]) readonly propC: string | boolean | undefined
}

// 注意不能直接用下面的方式去定义默认值
@Prop() prop = 'default value' // ×

对应的源代码为:

export default {
  props: {
    propA: {
      type: Number
    },
    propB: {
      default: "default value"
    },
    propC: {
      type: [String, Boolean]
    }
  }
}

# @PropSync

语法:@PropSync(propName: string, options: (PropOptions | Constructor[] | Constructor) = {}),跟 vue2 中的 .sync 修饰符实现的效果类似,但是不太建议使用这种方式,会破坏 vue 组件单向数据流的设定

import { Vue, Component, PropSync } from 'vue-property-decorator'

@Component
export default class YourComponent extends Vue {
  @PropSync('name', { type: String }) syncedName!: string
}

对应的源代码为:

export default {
  props: {
    name: {
      type: String
    }
  },
  computed: {
    syncedName: {
      get() {
        return this.name
      },
      set(value) {
        this.$emit("update:name", value)
      }
    }
  }
}

如上,在父组件传递一个 name 参数,在子组件,@PropSync 定义了一个计算属性 syncedName,并且设置了 settergetter,在访问 syncedName 时,返回父组件传递过来的 name 值,在设置 syncedName 时,emit 一个更新事件给父组件。而在父组件,也只需要用 .sync 修饰符:<parentComponent :name.sync="name"></parentComponent>。后续也只需要在子组件操作 syncedName 变量即可与父组件的变量 name 同步

# @Model

语法:@Model(event?: string, options: (PropOptions | Constructor[] | Constructor) = {})。先看代码

import { Vue, Component, Model } from 'vue-property-decorator'

@Component
export default class YourComponent extends Vue {
  @Model('change', { type: Boolean }) readonly checked!: boolean
}

// 不要忘了同步数据时需要 $emit 一下,事件对应@Model中的事件
syncValue(): void {
  this.$emit('change', someValue)
}

对应的源代码为:

export default {
  model: {
    prop: "checked",
    event: "change"
  },
  props: {
    checked: {
      type: Boolean
    }
  }
}

@Model 这一项对应的是:在组件中使用 v-model 中的 model 选项

一般情况下,v-model 会默认利用名为 valueprop 和名为 input 的事件(最常用)。但对于单选框、复选框这种输入控件,便不太符合场景。因此就有了 model 选项。具体示例可以看下上面的源代码,或更为详细的 vue 官方文档:自定义组件的 v-model (opens new window)

通过 Model 选项,我们可以自定义地修改子组件接收的 prop(如上源代码中的 checked 变量),并且修改触发的事件(如上源码中的 input 事件修改为 change),这样就可以自由控制什么时候将数据“同步”给父组件

# @ModelSync

语法:@ModelSync(propName: string, event?: string, options: (PropOptions | Constructor[] | Constructor) = {}),与 @Model 想比较之下,@ModelSync 会自动 $emit 对应事件给父组件,使用起来应该会更舒服一些。不过说实话,基本没咋用过 model 选项 em...

import { Vue, Component, ModelSync } from 'vue-property-decorator'

@Component
export default class YourComponent extends Vue {
  @ModelSync('checked', 'change', { type: Boolean })
  readonly checkedValue!: boolean
}

// 现在修改 checkedValue 会自动 $emit 给父组件

对应的源代码为:

export default {
  model: {
    prop: "checked",
    event: "change"
  },
  props: {
    checked: {
      type: Boolean
    }
  },
  computed: {
    checkedValue: {
      get() {
        return this.checked
      },
      set(value) {
        this.$emit("change", value)
      }
    }
  }
}

# @Watch

语法:@Watch(path: string, options: WatchOptions = {}),也就是监听器 watch,基本都一样了,稍微熟悉一下即可

import { Vue, Component, Watch } from "vue-property-decorator"

@Component
export default class YourComponent extends Vue {
  @Watch("child") //修饰符
  onChildChanged(val: string, oldVal: string) {} //对应方法

  @Watch("person", { immediate: true, deep: true })
  onPersonChanged1(val: Person, oldVal: Person) {}

  @Watch("person")
  onPersonChanged2(val: Person, oldVal: Person) {}

  @Watch("person")
  @Watch("child")
  onPersonAndChildChanged() {}
}

对应的源代码为:

export default {
  watch: {
    child: [
      {
        handler: "onChildChanged",
        immediate: false,
        deep: false
      },
      {
        handler: "onPersonAndChildChanged",
        immediate: false,
        deep: false
      }
    ],
    person: [
      {
        handler: "onPersonChanged1",
        immediate: true,
        deep: true
      },
      {
        handler: "onPersonChanged2",
        immediate: false,
        deep: false
      },
      {
        handler: "onPersonAndChildChanged",
        immediate: false,
        deep: false
      }
    ]
  },
  methods: {
    onChildChanged(val, oldVal) {},
    onPersonChanged1(val, oldVal) {},
    onPersonChanged2(val, oldVal) {},
    onPersonAndChildChanged() {}
  }
}

# Computed

computed 选项已经被替代为 gettersetter

import Vue from "vue"
import Component from "vue-class-component"

@Component
export default class HelloWorld extends Vue {
  firstName = "John"
  lastName = "Doe"

  // Declared as computed property getter
  get name() {
    return this.firstName + " " + this.lastName
  }

  // Declared as computed property setter
  set name(value) {
    const splitted = value.split(" ")
    this.firstName = splitted[0]
    this.lastName = splitted[1] || ""
  }
}

# @Provide、@Inject

语法:@Provide(key?: string | symbol) / @Inject(options?: { from?: InjectKey, default?: any } | InjectKey),先看代码

import { Component, Inject, Provide, Vue } from 'vue-property-decorator'

const symbol = Symbol('baz')

@Component
export class MyComponent extends Vue {
  @Inject() readonly foo!: string
  @Inject('bar') readonly bar!: string
  @Inject({ from: 'optional', default: 'default' }) readonly optional!: string
  @Inject(symbol) readonly baz!: string

  @Provide() foo = 'foo'
  @Provide('bar') baz = 'bar'
}

对应的源代码为:

const symbol = Symbol("baz")

export const MyComponent = Vue.extend({
  inject: {
    foo: "foo",
    bar: "bar",
    optional: { from: "optional", default: "default" },
    baz: symbol
  },
  data() {
    return {
      foo: "foo",
      baz: "bar"
    }
  },
  provide() {
    return {
      foo: this.foo,
      bar: this.baz
    }
  }
})

vue2.x 中,于本人来说 Provide/Inject 用的是相对比较少的,一方面不会使用层级过深的组件,另一方面 Provide/Inject 也具有其一定的缺陷:依赖注入 (opens new window);在 vue3.x 中,可以用来全局注入,如 vue2.x 中的 Vue.prototypevue3.x 中已被废除,Provide/Inject 作为替代方案:查看文档 (opens new window)

# @Emit

语法:@Emit(event?: string),直接看代码,熟悉熟悉就可以上手的了

import { Vue, Component, Emit } from "vue-property-decorator"

@Component
export default class YourComponent extends Vue {
  count = 0

  @Emit()
  addToCount(n: number) {
    this.count += n
  }

  @Emit("reset")
  resetCount() {
    this.count = 0
  }

  @Emit()
  returnValue() {
    return 10
  }

  @Emit()
  onInputChange(e) {
    return e.target.value
  }

  @Emit()
  promise() {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(20)
      }, 0)
    })
  }
}

对应的源代码为:

export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    addToCount(n) {
      this.count += n
      this.$emit("add-to-count", n)
    },
    resetCount() {
      this.count = 0
      this.$emit("reset")
    },
    returnValue() {
      this.$emit("return-value", 10)
    },
    onInputChange(e) {
      this.$emit("on-input-change", e.target.value, e)
    },
    promise() {
      const promise = new Promise(resolve => {
        setTimeout(() => {
          resolve(20)
        }, 0)
      })

      promise.then(value => {
        this.$emit("promise", value)
      })
    }
  }
}

# Vuex-Class

vuex 也有对应的装饰器,用起来比较舒服,不用写那么长的代码了:this.$store.state.user.userInfo...

官网地址:vuex-class (opens new window)

import Vue from "vue"
import Component from "vue-class-component"
import { State, Getter, Action, Mutation, namespace } from "vuex-class"

const someModule = namespace("path/to/module")

@Component
export class MyComp extends Vue {
  @State("foo") stateFoo
  @State(state => state.bar) stateBar
  @Getter("foo") getterFoo
  @Action("foo") actionFoo
  @Mutation("foo") mutationFoo
  @someModule.Getter("foo") moduleGetterFoo

  // If the argument is omitted, use the property name
  // for each state/getter/action/mutation type
  @State foo
  @Getter bar
  @Action baz
  @Mutation qux

  created() {
    this.stateFoo // -> store.state.foo
    this.stateBar // -> store.state.bar
    this.getterFoo // -> store.getters.foo
    this.actionFoo({ value: true }) // -> store.dispatch('foo', { value: true })
    this.mutationFoo({ value: true }) // -> store.commit('foo', { value: true })
    this.moduleGetterFoo // -> store.getters['path/to/module/foo']
  }
}

# vuex-module-decorators 起步

新增 vuex-module-decorators,文档及示例地址为:'vuex-module-decorators (opens new window)

  • State
  • @Mutation
  • Getter
  • @Action
  • @MutationAction
  • Dynamic Module

# State

vuex-module-decorators 取消了 State 选项,跟 vue3 一样,直接定义写变量即可。

import { Module, VuexModule } from "vuex-module-decorators"

@Module
export default class Vehicle extends VuexModule {
  wheels = 2
}

对应的源代码为:

export default {
  state: {
    wheels: 2
  }
}

# @Mutation

当使用 Mutation 装饰器时,使用 this 修改 State 数据。同时,在 Mutation 中避免使用异步函数以及箭头函数

import { Module, VuexModule, Mutation } from "vuex-module-decorators"

@Module
export default class Vehicle extends VuexModule {
  wheels = 2

  @Mutation
  puncture(n: number) {
    this.wheels = this.wheels - n
  }
}

对应的源代码为:

export default {
  state: {
    wheels: 2
  },
  mutations: {
    puncture: (state, payload) => {
      state.wheels = state.wheels - payload
    }
  }
}

# Getter

此时的 Getter 不再是 store 的一个选项,而是成为了 es6getter 函数

import { Module, VuexModule } from "vuex-module-decorators"

@Module
export default class Vehicle extends VuexModule {
  wheels = 2
  get axles() {
    return this.wheels / 2
  }
}

对应的源代码为:

export default {
  state: {
    wheels: 2
  },
  getters: {
    axles: state => state.wheels / 2
  }
}

# @Action

如果需要在 Action 执行长时间运行的任务,建议将其定义为异步函数(async-await)。如果不这么做的话,vuex-module-decorators 也会将你的函数包装成一个 Promise 函数并等待它;同时也不要将其定义为箭头函数,因为需要重新绑定 Actions

这里稍微注意一下,在 Action 中需要修改 state 的话,还是需要通过提交 Mutation。而现在需要使用 this.context.commit() 提交

import { Module, VuexModule, Mutation, Action } from "vuex-module-decorators"
import { get } from "request"

@Module
export default class Vehicle extends VuexModule {
  wheels = 2

  @Mutation
  addWheel(n: number) {
    this.wheels = this.wheels + n
  }

  @Action
  async fetchNewWheels(wheelStore: string) {
    const wheels = await get(wheelStore)
    this.context.commit("addWheel", wheels)
  }
}

对应的源代码为:

const request = require("request")
export default {
  state: {
    wheels: 2
  },
  mutations: {
    addWheel: (state, payload) => {
      state.wheels = state.wheels + payload
    }
  },
  actions: {
    fetchNewWheels: async (context, payload) => {
      const wheels = await request.get(payload)
      context.commit("addWheel", wheels)
    }
  }
}

# @MutationActions

如果当前有一个异步操作,并且获得的结果需要提交到 state,那么可以使用 @MutationActions。需要注意,当其返回的是 undefined 时,不会提交到 state

import {VuexModule, Module, MutationAction} from 'vuex-module-decorators'

@Module
class TypicodeModule extends VuexModule {
  posts: Post[] = []
  users: User[] = []

  @MutationAction
  async function updatePosts() {
    const posts = await axios.get('https://jsonplaceholder.typicode.com/posts')

    return { posts }
  }
}

对应的源代码为:

const typicodeModule = {
  state: {
    posts: [],
    users: []
  },
  mutations: {
    updatePosts: function(state, posts) {
      state.posts = posts
    }
  },
  actions: {
    updatePosts: async function(context) {
      const posts = await axios.get("https://jsonplaceholder.typicode.com/posts")
      context.commit("updatePosts", posts)
    }
  }
}

# Dynamic Module

使用 vuex-module-decorators 之后,可以使用动态 Modules。动态 Modules在注册时,跟一般的 vuex 不一样,在 @store/index.ts 中,只需要 new 一个空的 Vuex.Store({})

// Dynamic Modules
import Vue from "vue"
import Vuex from "vuex"

Vue.use(Vuex)
// Declare empty store first, dynamically register all modules later.
export default new Vuex.Store({})

在具体的 module 下,需要注意:

  • 指定 module 为动态 module
  • 必须要指定 name
  • 需要通过 getModule 将模块 export 出去
import { Module, VuexModule, getModule } from "vuex-module-decorators"
import store from "@/store"

@Module({
  namespaced: true,
  dynamic: true,
  store,
  name: "User"
})
export default class User extends VuexModule {
  userInfo = {
    name: "wolfBerry",
    phone: "18819490370",
    email: "906368017@qq.com"
  }
}

export const UserModule = getModule(User)

最后就可以在页面上,通过 import 引入后再使用

import { UserModule } from "@/store/module/user"
import { TestModule } from "@/store/module/test"

@Component
export default class Home extends Vue {
  created() {
    console.log(UserModule.userInfo)
  }

  handleChange() {
    TestModule.SET_COUNT(2)
  }
}

# 结尾

目前 vuex-module-decorator 不支持动态模块和嵌套模块混合使用,只能使用其中一种

vuex-module-decoratorvuex-classvue-class-component 三者熟练使用起来可以省掉很多代码,真的是谁用谁舒服,并且可以让代码看起来更 “高级” 一些哈!!!