[譯] Vue 3: Data 江河日下,Events 拔犀擢象

chuoke發表於2020-06-06

Vue 3: Data down, Events up | Vue Mastery - Thorsten Lünborg

前言

在這篇文章中,我將展示一個我們可以應用 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 是一個偉大的、靈活的工具。上面顯示的只是冰山一角。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
? 我的導航網站已經可以公開使用啦:Cootab

相關文章