在 Vue 2.2 中,我们引入了 model 组件选项,允许组件自定义用于 v-model 的 prop 和事件。在 Vue 3 中,双向数据绑定的 API 已经标准化,以减少开发者在使用 v-model 指令时的混淆,并且更加灵活。

Vue2 中的 v-model 使用

在 2.x 中,在组件上使用 v-model 相当于绑定 value prop 并触发 input 事件:

<ChildComponent v-model="pageTitle" />

<!-- 是以下的简写: -->
<ChildComponent :value="pageTitle" @change="pageTitle = $event" />

如果想要更改 prop 或事件名称,则需要在 ChildComponent 组件中添加 model 选项:

// ChildComponent.vue
export default {
  model: {
    prop: 'value',
    event: 'change'
  },
  props: {
    value: String,
  }
}
使用 v-bind.sync

对某个 prop 进行“双向绑定”,使用 update:xxx 抛出事件。

<ChildComponent :visible.sync="dialogVisible" />

// 相当于
<ChildComponent :visible="dialogVisible" @update:visible="visible = $event" />

// 数据更新
this.$emit('update:visible', false)

Vue3 中的 v-model 使用

在 3.x 中,自定义组件上的 v-model 相当于传递了 modelValue prop 并接收抛出的 update:modelValue 事件:

<ChildComponent v-model="pageTitle" />

<!-- 是以下的简写: -->
<ChildComponent
  :modelValue="pageTitle"
  @update:modelValue="pageTitle = $event"
/>
v-model 参数

若需要更改 model 的名称,可以为 v-model 传递一个参数,以作为组件内 model 选项的替代,也可以作为 .sync 修饰符的替代,而且允许我们在自定义组件上使用多个 v-model。

<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" />

<!-- 是以下的简写: -->

<ChildComponent
  :title="pageTitle"
  @update:title="pageTitle = $event"
  :content="pageContent"
  @update:content="pageContent = $event"
/>
迁移策略
  1. 所有使用 .sync 的部分并将其替换为 v-model
  2. 对于所有不带参数的 v-model,请确保分别将 prop 和 event 命名更改为 modelValue 和 update:modelValue
<ChildComponent v-model="pageTitle" />

// ChildComponent.vue
export default {
  props: {
    modelValue: String // 以前是`value:String`
  },
  emits: ['update:modelValue'],
  methods: {
    changePageTitle(title) {
      this.$emit('update:modelValue', title) // 以前是 `this.$emit('input', title)`
    }
  }
}

介绍

VueUse 是一个 Composition API 的工具集合,适用于 Vue 2.x 或者 Vue 3.x。常用 Hooks 如下:

useVModel

为组件创建自定义 v-model 的绑定, 组件接受 props 和 propName, 当该值被修改时, 组件会向父级发出更新事件, 即 props + emit -> ref.

import { useVModel } from '@vueuse/core'
const emit = defineEmits(['update:formModel'])
const modelValue = useVModel(props, 'formModel', emit)

console.log(modelValue.value) // props.formModel
modelValue.value = {} // emit('update:formModel', {})

useVModel 主要实现逻辑如下:

import { computed, getCurrentInstance } from 'vue'

export const useVModel = (props, propName) => {
  const vm = getCurrentInstance().proxy

  return computed({
    set (value) {
      vm.$emit(`update:${propName}`, value)
    },
    get () {
      return props[propName]
    }
  })
}

除了 useVModel API 之外,还提供 useVModels API 相当于 toRefs(props), 数据改变会触发 emit.

import { useVModels } from '@vueuse/core'

const emit = defineEmits(['update:formModel', 'update:data'])
const { modelValue, data } = useVModels(props, emit)

console.log(data.value) // props.data
data.value = 'foo' // emit('data:data', 'foo')
useWindowScroll

监听窗口滚动距离, 从而实现吸顶效果.

import { useWindowScroll } from '@vueuse/core'

const { x, y } = useWindowScroll()
return { x, y }
useIntersectionObserver

检查元素是否在可视区域, 常用于数据懒加载, 实现无线信息流下拉加载更多.

<div ref="target">
  <h1>Hello world</h1>
</div>

import { ref } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'

export default {
  setup() {
    const target = ref(null)
    const targetIsVisible = ref(false)

    const { stop } = useIntersectionObserver(
      target,
      ([{ isIntersecting }], observerElement) => {
        targetIsVisible.value = isIntersecting
      },
    )

    return {
      target,
      targetIsVisible,
    }
  },
}
  1. target: 被监听的 DOM 元素
  2. fn: 回调函数, 用于通知监听的动作, 其中第一个参数 isIntersecting 表示被监听的元素已经进入可视区
  3. options: 配置选项

stop 是停止观察是否进入或移出可视区域的行为

// src/hooks/useLazy/index.js
import { ref } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'

export const useLazy = async (fn) => {
  const target = ref(null) // 组件最外层元素
  const result = ref([]) // 数据
  const { stop } = useIntersectionObserver(target, ([{ isIntersecting }]) => {
    if (isIntersecting) {
      stop()
      const { rows } = await fn()
      result.value = rows
    }
  })

  return { target, result }
}

使用如下:

<div ref="target"></div>
import { useLazy } from '@/hooks/useLazy'
const { result, target } = useLazy(fn)
// 接口请求
function fn () {...}
useFullscreen

切换全屏。

import { computed, unref } from 'vue'
import { useFullscreen } from '@vueuse/core'
const { isFullscreen, enter, exit, toggle } = useFullscreen()

const title = computed(() => {
  return unref(isFullscreen) ? '退出全屏' : '全屏'
})
useRefHistory

跟踪对 ref 所做的每一步更改, 并将其存储在数组中, 从而实现撤销和重做功能。


import { ref } from 'vue' 
import { useRefHistory } from '@vueuse/core'

const counter = ref(0)
const { history, undo, redo } = useRefHistory(counter)

counter.value += 1

await nextTick()
console.log(history.value)
useResizeObserver

拖拽元素的大小。

<div ref="el">{{ text }}</div>
import { ref } from 'vue'
import { useResizeObserver } from '@vueuse/core'

export default {
  setup () {
    const el = ref(null)
    const text = ref('')

    useResizeObserver(el, (entries) => {
      const entry = entries[0]
      const { width, height } = entry.contentReact

      text.value = `width: ${width}, height: ${height}`
    })

    return { el, text }
  }
}
useStorage

将 LocalStorage 和 SessionStorage 中的数据响应式。

import { useStorage } from '@vueuse/core'

const state = useStorage('userInfo', { name: 'zhangsan' })
state.value = null // 从 storage 中删除

createGlobalState API 可以全局存储数据。

// store.js
import { createGlobalState, useStorage } from '@vueuse/core'

export const useGlobalState = createGlobalState(
  () => useStorage('vueuse-local-storage', 'initialValue'),
)

// component.js
import { useGlobalState } from './store'

export default defineComponent({
  setup () {
    const state = useGlobalState()
    return { state }
  }
})
useFetch
import { useFetch } from '@vueuse/core'

const { isFetching, error, data, execute, abort, canAbort } = useFetch(url, {
  // 请求拦截
  async beforeFetch({ url, options, cancel }) {...},
  // 响应拦截
  afterFetch(ctx) {},
  // 拦截错误
  onFetchError(ctx) {},
  // 防止请求立即被触发
  immediate: false
})

// 终止请求
setTimeout(() => {
  canAbort.value && abort()
}, 1000)


// 在调用之前,防止触发请求
execute()
useDebounceFn

防抖函数。

import { useDebounceFn } from '@vueuse/core'
const handleSearch = useDebounceFn(search, 300)
// 查询方法
function search () {......}
onClickOutside

监听元素外部的点击事件。

<div ref="target">Hello world</div>

import { ref } from 'vue'
import { onClickOutside } from '@vueuse/core'

export default {
  setup() {
    const target = ref(null)

    function close (event) {
      console.log(event)
    }

    onClickOutside(target, close)
    
    return { target }
  }
}

此外 onClickOutside 还可以作为组件使用,代码如下:

<template>
  <onClickOutside @trigger="close">Click Outside of Me</onClickOutside>
</template>

<script setup>
  import { onClickOutside } from '@vueuse/components'

  function close () {...}
</script>
onKeyStroke

监听键盘事件。

<script setup>
  import { onKeyStroke } from '@vueuse/core'

  function handleClose () {...}
  function handleEnter () {...}
  function handleUp () {...}
  function handleDown () {...}

  onKeyStroke('Escape', handleClose);
  onKeyStroke('Enter', handleEnter);
  onKeyStroke('ArrowUp', handleUp);
  onKeyStroke('ArrowDown', handleDown);
</script>

更多 Hooks 的使用,请查看官网:VueUse

Logo

前往低代码交流专区

更多推荐