前言

在这篇文章中,我将展示一个我们可以应用 Vue 3 的新 Composition API 解决一个特定挑战的模式。我不会介绍所有的基础知识,因此熟悉这个 新 API 的基础知识 将对您有所帮助。

重要提示: Composition API 是一个添加剂,是一个新特性,它没有也不会取代 Vue 1 和 2 中你所了解和喜爱的很好的旧 “Options API”。只要把这个新的 API 看作你工具箱中另一个工具,那么在使用 Options API 解决有些笨拙的情况下,它可能会派上用场。


我喜欢 Vue 的新 Composition API。对我来说,Vue 的反应系统似乎已经摆脱了组件的约束,现在我可以使用它来编写任何我想要的反应代码。

根据我的经验,一旦您对它有所了解,它就是一个创建灵活、可重用代码的极好的方法,这些代码可以很好地组合,并让你看到组件的所有部分和特性是如何交互的。

我将以一个小工具开始这篇文章,你可以用它更容易地使用组件和 v-model


概述: v-model 指令

如果你使用过 Vue,你就知道 v-model 指令:

<input type="text" v-model="localStateProperty">

这是一个非常赞的快捷方式,以避免我们输入复杂的模板标记,像这样:

<input 
  type="text" 
  :value="localStateProperty" 
  @change="localStateProperty = $event.target.value"
>

最棒的是,我们还可以在组件上使用它:

<message-editor v-model="message">

这相当于做以下事情:

<message-editor 
  :modelValue="message" 
  @update:modelValue="message = $event"
>

但是,为了实现属性(prop)和事件(event)的约定,我们的 <message-editor> 组件必须看起来像这样:

<template> 
  <label> 
    <input type="text" :value="modelValue", @change="(event) => $emit('update:modelValue', event.target.value)" > 
  <label>
</template>

<script> export default { 
  props: { 
    'modelValue': String, 
  } 
}
</script>

然而,这似乎相当冗长。🧐

我们必须这样做,因为无法直接写到属性。我们必须发出正确的事件,并将其留给父组件来决定如何处理我们传递的更新,因为它是父组件的数据,而不是 <message-editor> 组件的数据。因此,在这种情况下,我们不能在 input 上使用 v-model。烦人。

你可能从 Vue 2 了解到 Options API 中有一些处理这个问题的模式,但是今天我想看看使用 Composition API 提供的工具如何以一种简洁的方式解决这个问题。


挑战: 减少样板

我们想要实现的是一个抽象,它允许我们在输入中使用相同的 v-model 快捷方式,即使我们实际上不想写入本地状态,而是想发出正确的事件。这是我们希望模板完成后的样子:

<template> 
  <label> 
    <input 
      type="text" 
      v-model="message" 
    /> 
  <label>
</template>

那么,让我们使用 Composition API 来实现这个:

import { computed } from 'vue'

export default { 
  props: { 
    'modelValue': String, 
  },
  setup(props, { emit }) { 
    const message = computed({ 
      get: () => props.modelValue, 
      set: (value) => emit('update:modelValue', value) 
    }) 

    return { 
      message,
    } 
  }
}

好吧,这是一段新的代码,也许有点异形。让我们来分解一下:

import { computed } from 'vue'

首先,我们导入 computed() 函数,该函数返回一个用于计算属性的引用(ref) —— 一个用于从其他反应性数据(例如 props)派生出值的包装器。

const message = computed({ 
  get: () => props.modelValue, 
  set: (event) => emit('update:modelValue')
})

在 setup 中,我们创建了这样一个计算属性,但是是一个特殊的属性:我们计算属性有一个 gettersetter,因此我们实际上可以读取它的派生值并为它赋一个新值。

这是我们计算属性在 Javascript 中使用时的行为:

message.value
// => '这返回一个字符串'
message.value = 'This will be emitted up'
// => 调用 emit('onUpdate:ModelValue', 'This will be fired up')

通过从 setup() 函数返回这个计算属性,我们将它暴露给模板。现在,我们可以将它和 v-model 一起使用,得到一个干净漂亮的模板:

<template> 
  <label> 
    <input 
      type="text" 
      v-model="message" 
    > 
  <label>
</template>

<script>
import { computed } from 'vue'

export default { 
  props: { 
    'modelValue': String, 
  },
  setup(props, { emit }) { 
    const message = computed({ 
      get: () => props.modelValue, 
      set: (value) => emit('update:modelValue', value) 
    }) 

    return { 
      message, 
    } 
  }
}
</script>

现在模板非常干净。但与此同时,我们必须在 setup() 中编写一堆样板文件来实现这一点。看起来我们只是将样板文件从模板移到了 setup 函数中。

因此,让我们将这个逻辑提取到它自己的函数中——一个 composition 函数——或者简称为 “composable”。


将其转换为 composable

composable 只是一个函数,我们使用它从 setup 函数中抽象出一些代码。可组合性(Composables)是这个新 Composition API 的优势,也是允许更好的代码抽象和组合的核心原则。

这就是我们的目标:

📄 modelWrapper.js

import { computed } from 'vue'

export function useModelWrapper(props) { 
  /* 暂时不讨论实现 */
}

📄 MessageEditor.vue

import { useModelWrapper } from '../utils/modelWrapper'

export default { 
  props: { 
    'modelValue': String, 
  } 
  setup(props, { emit }) { 
    return { 
      message: useModelWrapper(props), 
    } 
  }
}

注意,我们的 setup() 函数中的代码是如何简化为一行代码的(如果我们假设我们在一个很好的编辑器中工作,它可以为我们自动添加 useModelWrapper 的导入,比如 VSCode)。

我们是怎么做到的呢?实际上,我们所要做的就是将 setup 中的代码复制粘贴到这个新函数中!这是它的样子:

📄 modelWrapper.js

import { computed } from 'vue'

export function useModelWrapper(props) { 
  return computed({ 
    get: () => props.modelValue, 
    set: (value) => emit('update:modelValue', value) 
  })
}

好吧,这很简单,但我们能做得更好吗? 是的,我们可以!

不过为了理解我们可以用什么方式,我们将绕一小圈回到 v-model 如何在组件上工作……

在 Vue 3 中,v-model 可以使用额外的参数将其应用到 modelValue 以外的属性上。如果组件想要公开多个属性作为 v-model 的目标,这是非常有用的。

v-model:argument

<message-editor 
  v-model="message" 
  v-model:draft="isDraft" 
/>

第二个 v-model 可以这样实现:

<input
  type="checkbox" 
  :checked="draft",
  @change="(event) => $emit('update:modelValue', event.target.checked)"
 >

再重复一遍冗长的模板代码。是时候调整新的 composition 函数,这样我们也可以在这个例子中重用它。

要实现这一点,我们需要向函数添加第二个参数,该函数指定我们实际想要封装的属性名称。由于 modelValuev-model 的默认属性名称,我们也可以将它作为包装器的第二个参数的默认名称:

📄 modelWrapper.js

import { computed } from 'vue'

export function useModelWrapper(props, name = 'modelValue') { 
  return computed({ 
    get: () => props[name], 
    set: (value) => emit(`update:${name}`, value) 
  })
}

就是这样。现在我们可以对任何 v-model 属性使用这个包装器。

所以最终的组件看起来是这样的:

<template> 
  <label> 
    <input type="text" v-model="message" > 
    <label> <label> 
    <input type="checkbox" v-model="isDraft"> Draft 
  </label>
</template>

<script>
import { useModelWrapper } from '../utils/modelWrapper'

export default { 
  props: { 
    modelValue: String, 
    draft: Boolean
  },
  setup(props, { emit }) { 
    return { 
      message: useModelWrapper(props, 'modelValue'), 
      isDraft: useModelWrapper(props, 'draft') 
    }
  }
}
</script>

下一步

这种可组合性不仅在我们希望将 modelValue 属性映射到模板中的输入时有用,还可以使用它将计算属性引用传递给其他需要引用的可赋值组合。

通过像上面那样,首先包装 modelValue 属性,其次组合函数可以不知道我们实际上没有处理局部状态的事实。我们在可组合的小 useModelWrapper 中抽象了实现细节,因此其他可组合的可以将其视为本地状态。


“快速输入,否则丢失”

作为一个公认的愚蠢示例,我们有一个名为 useMessageReset 的可组合组件。当你停止输入 5 秒后,它会将你的信息重置为空字符串。它是这样的:

function useMessageReset(message) {
  let timeoutId
  const reset = () => {
    if (timeoutId) {
      clearTimeout(timeoutId)
    }
    timeoutId = setTimeout(() => (message.value = ''), 5000)
    watch(message, () => message.value !== '' && reset())
  }
}

此可组合使用 watch(),这是 Composition API 的另一个功能。

  • 每当消息更改时,第二个参数中的回调函数就会运行。
  • 如果消息不是空的,它将(重新)启动超时,5 秒后将消息重置为空值。

注意,这个函数期望接收一个它可以监视的引用并为其赋值。

我们在使用 modelValue 属性时会遇到问题,因为我们不能直接写入它。但使用 useModelWrapper,我们可以提供一个可写的计算属性引用到这个组合:

import { useModelWrapper } from '../utils/modelWrapper'
import { useMessageReset } from '../utils/messageReset'

export default {
  props: { modelValue: Boolean },
  setup(props, { emit }) {
    const message = useModelWrapper(props)
    useMessageReset(message)
    return { message }
  }
}

注意,这个可组合组件是如何不知道 message 实际上是为父组件的 v-model 发出一个事件的。就像我们通过普通的 ref() 一样,它只能分配给 .value

还要注意,我们的其余功能是如何不受此影响的。我们仍然可以在模板中使用 message,或者以其他方式使用它。


最后的感想

对于每个想要使用它的 Vue 开发人员来说,composition API 是一个伟大的、灵活的工具。上面显示的只是冰山一角。