Vue2 简单上手 Composition API

773 阅读2分钟

Vue3 出来一年多了 @vue/composition-api 也比较稳定了 最近想用 @vue/composition-api 对项目组件进行改造。

本文主要讲述对一个小组件进行重构的过程,对于 Composition API 相关知识可以去官网等渠道了解。

当然这个组件比较简单,也不是一定要重构。

首先是 Options API 的写法

<template>
  <div>
    <span v-if="tags && tags.length" class="tags">
      <el-tag
        v-for="(item, index) in tags"
        type="success"
        :key="item"
        :closable="true"
        :close-transition="false"
        @close="handleCloseTag(index)"
      >
        {{ item }}
      </el-tag>
    </span>

    <el-input
      v-if="tagVisible"
      v-model="tag"
      class="tag-input"
      :placeholder="placeholder"
      @blur="handleAddTag()"
      @keyup.enter.native="handleAddTag()"
    ></el-input>
    <el-button
      v-else
      size="small"
      icon="el-icon-plus"
      @click="tagVisible = true"
    >
      添 加
    </el-button>
  </div>
</template>

<script>
export default {
  name: 'TagsContainer',
  props: {
    value: { type: Array, default: () => [] },
    placeholder: { type: String, default: '' }
  },
  watch: {
    value(newValue, oldValue) {
      if (newValue !== oldValue) {
        this.tags = newValue
      }
    }
  },
  data() {
    return { tags: this.value, tag: '', tagVisible: false }
  },
  methods: {
    handleAddTag() {
      const { tag, tags } = this
      if (tag && !tags.includes(tag)) {
        tags.push(tag)
      }
      this.tagVisible = false
      this.tag = ''
    },
    handleCloseTag(index) {
      this.tags.splice(index, 1)
    }
  }
}
</script>

<style lang="scss" scoped>
.tags {
  .el-tag {
    margin-right: 10px;
  }
}

.tag-input {
  width: 120px;
}
</style>

经过初步重构变成

<script>
import { defineComponent, ref, watch } from '@vue/composition-api'

export default defineComponent({
  name: 'TagsContainer',
  props: {
    value: { type: Array, default: () => [] },
    placeholder: { type: String, default: '' }
  },
  setup(props) {
    const tags = ref(props.value)
    const tag = ref('')
    const tagVisible = ref(false)

    watch(
      () => props.value,
      (newValue, oldValue) => {
        if (newValue !== oldValue) {
          tags.value = newValue
        }
      }
    )

    function handleAddTag() {
      if (tag.value && !tags.value.includes(tag.value)) {
        tags.value.push(tag.value)
      }
      tagVisible.value = false
      tag.value = ''
    }

    function handleCloseTag(index) {
      tags.value.splice(index, 1)
    }

    return {
      tags,
      tagVisible,
      tag,
      handleCloseTag,
      handleAddTag
    }
  }
})
</script>

太多 .value 不友好稍微优化一下

function handleAddTag() {
  if (tag.value && !tags.value.includes(tag.value)) {
    tags.value.push(tag.value)
  }
  tagVisible.value = false
  tag.value = ''
}

改为

function handleAddTag() {
  const tagValue = tag.value
  const tagsValue = tags.value

  if (tagValue && !tagsValue.includes(tagValue)) {
    tagsValue.push(tagValue)
  }

  tagVisible.value = false
  tag.value = ''
}

然后根据逻辑功能来组织代码变成

<script>
import { defineComponent, ref, watch } from '@vue/composition-api'

export default defineComponent({
  name: 'TagsContainer',
  props: {
    value: { type: Array, default: () => [] },
    placeholder: { type: String, default: '' }
  },
  setup(props) {
    const tags = ref(props.value)

    watch(
      () => props.value,
      (newValue, oldValue) => {
        if (newValue !== oldValue) {
          tags.value = newValue
        }
      }
    )

    function handleCloseTag(index) {
      tags.value.splice(index, 1)
    }

    const tag = ref('')
    const tagVisible = ref(false)

    function handleAddTag() {
      const tagValue = tag.value
      const tagsValue = tags.value

      if (tagValue && !tagsValue.includes(tagValue)) {
        tagsValue.push(tagValue)
      }

      tagVisible.value = false
      tag.value = ''
    }

    return {
      tags,
      handleCloseTag,
      //
      tagVisible,
      tag,
      handleAddTag
    }
  }
})
</script>

提炼函数

function handleAddTag() {
  const tagValue = tag.value
  const tagsValue = tags.value

  if (tagValue && !tagsValue.includes(tagValue)) {
    tagsValue.push(tagValue)
  }

  tagVisible.value = false
  tag.value = ''
}

改为

function addTag(tag) {
  if (!tag) {
    return
  }

  const tagsValue = tags.value

  !tagsValue.includes(tag) && tagsValue.push(tag)
}

function handleAddTag() {
  addTag(tag.value)
  tag.value = ''

  tagVisible.value = false
}

封装变量

const tags = ref(props.value)

watch(
  () => props.value,
  (newValue, oldValue) => {
    if (newValue !== oldValue) {
      tags.value = newValue
    }
  }
)

改为

const tags = ref(getModelValue())

watch(getModelValue, (newValue, oldValue) => {
  if (newValue !== oldValue) {
    tags.value = newValue
  }
})

function getModelValue() {
  return props.value
}

现在看起来会好一点

<script>
import { defineComponent, ref, watch } from '@vue/composition-api'

export default defineComponent({
  name: 'TagsContainer',
  props: {
    value: { type: Array, default: () => [] },
    placeholder: { type: String, default: '' }
  },
  setup(props) {
    const tags = ref(getModelValue())

    watch(getModelValue, (newValue, oldValue) => {
      if (newValue !== oldValue) {
        tags.value = newValue
      }
    })

    function getModelValue() {
      return props.value
    }

    function addTag(tag) {
      if (!tag) {
          return
      }

      const tagsValue = tags.value

      !tagsValue.includes(tag) && tagsValue.push(tag)
    }

    function handleCloseTag(index) {
      tags.value.splice(index, 1)
    }

    const tag = ref('')
    const tagVisible = ref(false)

    function handleAddTag() {
      addTag(tag.value)
      tag.value = ''
      
      tagVisible.value = false
    }

    return {
      tags,
      handleCloseTag,
      //
      tagVisible,
      tag,
      handleAddTag
    }
  }
})
</script>

一般来说到这里就差不多了 不知道还能不能更进一步 (.value 实在难受 不知道下面这样会不会好一点)

封装了一个 dataRef

function dataRef(value) {
  return {
    ref: ref(value),
    get() {
      return this.ref.value
    },
    set(value) {
      this.ref.value = value
    }
  }
}
const tags = ref(getModelValue())

改成

const Tags = dataRef(getModelValue())

用大写开头是为了使得 Tags.get() 看起来像静态方法,而且取值变量不需要重命名

const tags = Tags.get()

现在长这样

<script>
import { defineComponent, ref, watch } from '@vue/composition-api'

function dataRef(value) {
  return {
    ref: ref(value),
    get() {
      return this.ref.value
    },
    set(value) {
      this.ref.value = value
    }
  }
}

export default defineComponent({
  name: 'TagsContainer',
  props: {
    value: { type: Array, default: () => [] },
    placeholder: { type: String, default: '' }
  },
  setup(props) {
    const Tags = dataRef(getModelValue())

    watch(getModelValue, (newValue, oldValue) => {
      if (newValue !== oldValue) {
        Tags.set(newValue)
      }
    })

    function getModelValue() {
      return props.value
    }

    function addTag(tag) {
      if (!tag) {
        return
      }

      const tags = Tags.get()

      !tags.includes(tag) && tags.push(tag)
    }

    function handleCloseTag(index) {
      Tags.get().splice(index, 1)
    }

    const Tag = dataRef('')
    const TagVisible = dataRef(false)

    function handleAddTag() {
      addTag(Tag.get())
      Tag.set('')

      TagVisible.set(false)
    }

    return {
      tags: Tags.ref,
      handleCloseTag,
      //
      tagVisible: TagVisible.ref,
      tag: Tag.ref,
      handleAddTag
    }
  }
})
</script>

还能不能更进一步?

watch 在赋值前有个 if 判断

handleAddTag 这个函数在执行完 addTag 函数后有一个重置的操作

function dataRef(value) {
  return {
    ref: ref(value),
    get() {
      return this.ref.value
    },
    set(value) {
      this.ref.value = value
    }
  }
}

改成

import clone from 'rfdc/default'

function dataRef(value, { isSkipSameValue } = { isSkipSameValue: false }) {
  const _defaultValue = clone(value)

  return {
    ref: ref(value),
    get() {
      return this.ref.value
    },
    set(value) {
      if (isSkipSameValue && this.get() === value) {
        return
      }

      this.ref.value = value
    },
    reset() {
      this.set(clone(_defaultValue))
    }
  }
}

重构完成,最终结果如下

<template>
  <div>
    <span v-if="tags && tags.length" class="tags">
      <el-tag
        v-for="(item, index) in tags"
        type="success"
        :key="item"
        :closable="true"
        :close-transition="false"
        @close="handleCloseTag(index)"
      >
        {{ item }}
      </el-tag>
    </span>

    <el-input
      v-if="tagVisible"
      v-model="tag"
      class="tag-input"
      :placeholder="placeholder"
      @blur="handleAddTag()"
      @keyup.enter.native="handleAddTag()"
    ></el-input>
    <el-button
      v-else
      size="small"
      icon="el-icon-plus"
      @click="tagVisible = true"
    >
      添 加
    </el-button>
  </div>
</template>

<script>
import { defineComponent, ref, watch } from '@vue/composition-api'
import clone from 'rfdc/default'

function dataRef(value, { isSkipSameValue } = { isSkipSameValue: false }) {
  const _defaultValue = clone(value)

  return {
    ref: ref(value),
    get() {
      return this.ref.value
    },
    set(value) {
      if (isSkipSameValue && this.get() === value) {
        return
      }

      this.ref.value = value
    },
    reset() {
      this.set(clone(_defaultValue))
    }
  }
}

export default defineComponent({
  name: 'TagsContainer',
  props: {
    value: { type: Array, default: () => [] },
    placeholder: { type: String, default: '' }
  },
  setup(props) {
    const Tags = dataRef(getModelValue() || [], { isSkipSameValue: true })

    watch(getModelValue, Tags.set.bind(Tags))

    function getModelValue() {
      return props.value
    }

    function addTag(tag) {
      if (!tag) {
        return
      }

      const tags = Tags.get()

      !tags.includes(tag) && tags.push(tag)
    }

    function handleCloseTag(index) {
      Tags.get().splice(index, 1)
    }

    const Tag = dataRef('')
    const TagVisible = dataRef(false)

    function handleAddTag() {
      addTag(Tag.get())
      Tag.reset()

      TagVisible.reset()
    }

    return {
      tags: Tags.ref,
      handleCloseTag,
      //
      tagVisible: TagVisible.ref,
      tag: Tag.ref,
      handleAddTag
    }
  }
})
</script>

<style lang="scss" scoped>
.tags {
  .el-tag {
    margin-right: 10px;
  }
}

.tag-input {
  width: 120px;
}
</style>

假如觉得 setup 内容太长,还可以继续提炼函数,setup 只留下主干

import { defineComponent, ref, watch } from '@vue/composition-api'
import clone from 'rfdc/default'

function dataRef(value, { isSkipSameValue } = { isSkipSameValue: false }) {
  const _defaultValue = clone(value)

  return {
    ref: ref(value),
    get() {
      return this.ref.value
    },
    set(value) {
      if (isSkipSameValue && this.get() === value) {
        return
      }

      this.ref.value = value
    },
    reset() {
      this.set(clone(_defaultValue))
    }
  }
}

function useTags(props) {
  const Tags = dataRef(getModelValue() || [], { isSkipSameValue: true })

  watch(getModelValue, Tags.set.bind(Tags))

  function getModelValue() {
    return props.value
  }

  function addTag(tag) {
    if (!tag) {
      return
    }

    const tags = Tags.get()

    !tags.includes(tag) && tags.push(tag)
  }

  function handleCloseTag(index) {
    Tags.get().splice(index, 1)
  }

  return { addTag, tags: Tags.ref, handleCloseTag }
}

function useTag() {
  const Tag = dataRef('')
  const TagVisible = dataRef(false)

  return {
    tag: Tag.ref,
    tagVisible: TagVisible.ref,
    createHandleAddTag(callback) {
      return function () {
        callback(Tag.get())
        Tag.reset()

        TagVisible.reset()
      }
    }
  }
}

export default defineComponent({
  name: 'TagsContainer',
  props: {
    value: { type: Array, default: () => [] },
    placeholder: { type: String, default: '' }
  },
  setup(props) {
    const { addTag, tags, handleCloseTag } = useTags(props)
    const { createHandleAddTag, tag, tagVisible } = useTag()

    return {
      tags,
      handleCloseTag,
      //
      tag,
      tagVisible,
      handleAddTag: createHandleAddTag(addTag)
    }
  }
})
</script>

成为码农差不多 5 年了,前端做的比较少加起来不够一年,JQuery Backbone Angular Vue React 等等主流框架都有用来开发过项目,在掘金看了这么多文章,第一次发文,请各位多多包涵。