Skip to content

vue3

props

Name:
Age: 0
Email: 2
Address: 3
child
Name: 1
Age:
Email:
  • Props Index
查看代码
vue
<script setup>
import Child from './Child.vue'
import { reactive } from 'vue'

const form = reactive({
  name: '1',
  age: 0,
  email: '2',
  address: '3',
})

defineOptions({
  name: 'Props',
})

</script>

<template>
  <div>
    <div><span>Name:</span> <input type="text" v-model="form.name" class="form-input" /></div>
    <div><span>Age:</span> {{ form.age }}</div>
    <div><span>Email:</span> {{ form.email }}</div>
    <div><span>Address:</span> {{ form.address }}</div>
    <div style="text-align: left; margin-top: 20px;">child</div>
    <Child :form="form" />
  </div>
</template>
<style>
.form-input {
  border: 1px solid #303030;
}
</style>
  • Props Child
查看代码
vue
<script setup>
import { reactive } from 'vue'
const props = defineProps({
  form: {
    type: Object,
    default: () => ({
      name: 'John Doe',
      age: 20,
      email: 'john.doe@example.com'
    })
  }
})
// v-model 绑定 ref(props.form) 或直接props.form(实际使用都可以)
const formRef = reactive(props.form)

</script>
<template>
  <div>
    <div><span>Name:</span> {{ formRef.name }}</div>
    <div><span>Age:</span> <input class="form-input" type="number" v-model="formRef.age" /></div>
    <div><span>Email:</span> <input class="form-input" type="email" v-model="formRef.email" /></div>
  </div>
</template>

自定义指令

实现

  • permission-control 权限控制
查看代码
ts
import type { DirectiveBinding } from 'vue'

// import { NewPermissionControlKey } from '@/constants/injection'
import { useAuthStore } from '@/store/modules/auth'
import emitter from '@/utils/mitt'
import { NewPermissionControlKey } from '@/constants/injection'
// 这里的权限:控制按钮的是否需要输入账号密码
const permissionControl = {
  mounted(el: HTMLElement, binding: DirectiveBinding, vnode: any) {
    const { value } = binding
    const { format = 'emitter' } = value
    const authStore = useAuthStore()

    if (!value.permissionCode) {
      console.log('v-permission-control no bind permissionCode')

      return
    }

    const clearPermission = () => {
      authStore.clearExtraButtonPermissions(value.permissionCode)
    }
    // Remove any existing click listeners by cloning and replacing the element
    // const newEl = el.cloneNode(true) as HTMLElement

    // el.parentNode?.replaceChild(newEl, el)
    const isClearPermission = typeof value.isClearPermission === 'boolean' ? value.isClearPermission : true

    el.addEventListener('click', () => {
      // 触发方式有两种:
      // eventBus触发
      if (format === 'emitter') {
        emitter.emit('permission-control-show', {
          permissionCode: value.permissionCode,
          successPermissionCb: () => {
            value.successPermissionCb?.(clearPermission)
            if (isClearPermission) clearPermission()
          },
          noPermissionHandle: () => {
            value.noPermissionHandle?.()
          },
        })
      // provide触发
      } else if (format === 'provide') {
        // #region provide
        const newPermissionControl = vnode?.ctx?.provides[NewPermissionControlKey]
        newPermissionControl(
          value.permissionCode,
          () => {
            value.successPermissionCb?.(clearPermission)
            // 默认获取权限后,清除权限,下次再点击时,需要重新获取权限
            if (isClearPermission) clearPermission()
          },
          () => {
            value.noPermissionHandle?.()
          }
        )
        // #endregion provide
      }
    })
  },
  unmounted(el: HTMLElement, binding: DirectiveBinding) {
    const { value } = binding
    const newEl = el.cloneNode(true) as HTMLElement

    el.parentNode?.replaceChild(newEl, el)
    const authStore = useAuthStore()

    authStore.clearExtraButtonPermissions(value.permissionCode)
  },
}

export default permissionControl
  • enable-keyboard 键盘控制启用
查看代码
ts
import type { DirectiveBinding, ObjectDirective } from 'vue'

// 给元素扩展事件处理器
interface InputElementWithHandler extends HTMLInputElement {
  _disableInputHandler?: (event: KeyboardEvent) => void
  _beforeInputHandler?: (event: InputEvent) => void
  _disableContextMenuHandler?: (event: MouseEvent) => void
  _compositionEndHandler?: (event: CompositionEvent) => void
  _inputHandler?: () => void
  _target?: InputElementWithHandler | null
  _lastInputTime?: number | null
  _scanBuffer?: string[] | null
  _previousValue?: string | null
}

const enableKeyboard: ObjectDirective<InputElementWithHandler, boolean> = {
  mounted(el, binding: DirectiveBinding<boolean>) {
    const _target: InputElementWithHandler | null =
      el.tagName.toLowerCase() === 'input' || el.tagName.toLowerCase() === 'textarea'
        ? el
        : el.querySelector('input, textarea')

    if (!_target) return
    el._target = _target
    el._lastInputTime = 0
    el._scanBuffer = []

    // 键盘事件处理:这里只保留 Enter(避免阻塞 input 事件)
    el._disableInputHandler = (event: KeyboardEvent) => {
      if (binding.value) return // 允许输入

      // 屏蔽 Ctrl+V(Windows / Linux)
      if (event.ctrlKey && (event.key === 'v' || event.key === 'V')) {
        event.preventDefault()
      }

      // 屏蔽 Cmd+V(Mac)
      if (event.metaKey && (event.key === 'v' || event.key === 'V')) {
        event.preventDefault()
      }

      if (event.key === 'Backspace' || event.key === 'Delete') {
        event.preventDefault()
      }
    }
    el._compositionEndHandler = (event) => {
      console.log('compositionEnd', event)
      event.preventDefault()
      event.stopPropagation()
      console.log('compositionEnd', el._previousValue)
      el._target!.value = el._previousValue || ''
      el._target!.dispatchEvent(new Event('input', { bubbles: true }))
    }

    el._inputHandler = () => {
      el._previousValue = el._target!.value
    }
    // beforeinput 处理:区分人工输入 / 扫描枪
    el._beforeInputHandler = (event: InputEvent) => {
      // debugger
      if (binding.value) return // 允许输入

      const now = Date.now()
      const diff = now - (el._lastInputTime || 0)

      el._lastInputTime = now

      if (event.inputType === 'insertCompositionText') {
        console.log('insertCompositionText', event.data)
        event.preventDefault()

        return
      }


      // 插入文本(用户打字/中文输入/扫描枪逐字)
      if (event.inputType === 'insertText' && event.data) {
        el._scanBuffer!.push(event.data)

        // 如果间隔太大(>30ms),认为是人工输入,重置缓冲
        if (diff > 30) {
          el._scanBuffer = [event.data]
        }
        // 全部禁掉
        event.preventDefault()
      }

      // 粘贴 / 扫描枪一次性写入,允许
      if (event.inputType === 'insertFromPaste') {
        event.preventDefault()

        return
      }

      // 换行(通常扫描枪会自动发 Enter)
      if (event.inputType === 'insertLineBreak') {
        // 这里排除一种情况,就是:输入一个键,然后就按回车,至少是已经连续6个了,那就是扫描枪输入
        if (el._scanBuffer!.length >= 6) {
          // ✅ 认为是扫描枪输入
          console.log('✅ 扫描枪输入:', el._scanBuffer!.join(''))
          // 手动控制显示
          el._target!.value = el._scanBuffer!.join('')
          el._target!.dispatchEvent(new Event('input', { bubbles: true }))
          el._previousValue = el._scanBuffer!.join('')
        } else {
          // ⛔ 人工输入回车,禁止
          event.preventDefault()
        }
        el._scanBuffer = []
      }
    }

    el._disableContextMenuHandler = (event: MouseEvent) => {
      event.stopPropagation()
      event.preventDefault()
    }

    if (!binding.value) {
      _target.addEventListener('keydown', el._disableInputHandler)
      _target.addEventListener('beforeinput', el._beforeInputHandler)
      _target.addEventListener('contextmenu', el._disableContextMenuHandler)
      _target.addEventListener('compositionend', el._compositionEndHandler)
    }else{
      _target.addEventListener('input', el._inputHandler)
    }
  },
  updated(el, binding: DirectiveBinding<boolean>) {
    if (!el._target) return

    if (binding.value) {
      el._target.addEventListener('input', el._inputHandler!)
      el._target.removeEventListener('keydown', el._disableInputHandler!)
      el._target.removeEventListener('beforeinput', el._beforeInputHandler!)
      el._target.removeEventListener('contextmenu', el._disableContextMenuHandler!)
      el._target.removeEventListener('compositionend', el._compositionEndHandler!)
    } else {
      el._target.removeEventListener('input', el._inputHandler!)
      el._target.addEventListener('keydown', el._disableInputHandler!)
      el._target.addEventListener('beforeinput', el._beforeInputHandler!)
      el._target.addEventListener('contextmenu', el._disableContextMenuHandler!)
      el._target.addEventListener('compositionend', el._compositionEndHandler!)
    }
  },
  unmounted(el) {
    if (el._disableInputHandler) {
      el._target?.removeEventListener('keydown', el._disableInputHandler)
      el._disableInputHandler = undefined
    }

    if (el._beforeInputHandler) {
      el._target?.removeEventListener('beforeinput', el._beforeInputHandler)
      el._beforeInputHandler = undefined
    }

    if (el._disableContextMenuHandler) {
      el._target?.removeEventListener('contextmenu', el._disableContextMenuHandler)
      el._disableContextMenuHandler = undefined
    }

    if (el._compositionEndHandler) {
      el._target?.removeEventListener('compositionend', el._compositionEndHandler)
      el._compositionEndHandler = undefined
    }

    if (el._inputHandler) {
      el._target?.removeEventListener('input', el._inputHandler)
      el._inputHandler = undefined
    }

    el._target = null
    el._previousValue = null
    el._scanBuffer = null
    el._lastInputTime = null
  },
}

export default enableKeyboard

其他

  • 自定义指令中调用 provide
  • 在 Vue 3 的自定义指令中直接使用 inject 是不可以的。
  • inject 只能在组件的 setup 函数或生命周期钩子中使用,而不能在指令的钩子函数中直接使用。
ts
const newPermissionControl = vnode?.ctx?.provides[NewPermissionControlKey]
newPermissionControl(
  value.permissionCode,
  () => {
    value.successPermissionCb?.(clearPermission)
    // 默认获取权限后,清除权限,下次再点击时,需要重新获取权限
    if (isClearPermission) clearPermission()
  },
  () => {
    value.noPermissionHandle?.()
  }
)

TS 与 组合式API

props

ts
// 3.5+
// 解决:失去了为 props 声明默认值的能力
interface Props {
  msg?: string
  labels?: string[]
}
const { msg = 'hello', labels = ['one', 'two'] } = defineProps<Props>()
ts
// 在 3.4 及更低版本
// 使用 withDefaults 编译器宏
interface Props {
  msg?: string
  labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  labels: () => ['one', 'two']
})

emits

vue
<script setup lang="ts">
// 3.3+: 可选的、更简洁的语法
const emit = defineEmits<{
  change: [id: number]
  update: [value: string]
}>()
</script>

reactive

ts
import { reactive } from 'vue'
interface Book {
  title: string
  year?: number
}
const book: Book = reactive({ title: 'Vue 3 指引' })

event

ts
function handleChange(event: Event) {
  console.log((event.target as HTMLInputElement).value)
}

provide / inject

  • InjectionKey
ts
import { provide, inject } from 'vue'
import type { InjectionKey } from 'vue'
const key = Symbol() as InjectionKey<string>
provide(key, 'foo') // 若提供的是非字符串值会导致错误
const foo = inject(key) // foo 的类型:string | undefined
  • 参数
ts
// 注入一个值,若为空则使用提供的默认值
const bar = inject('path', '/default-path')
// 注入一个值,若为空则使用提供的函数类型的默认值
const fn = inject('function', () => {})
// 注入一个值,若为空则使用提供的工厂函数
const baz = inject('factory', () => new ExpensiveObject(), true)

模板引用

ts
// Vue 3.5 和 @vue/language-tools 2.1
const el = useTemplateRef<HTMLInputElement>(null)
vue
// 3.5 前的用法
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const el = ref<HTMLInputElement | null>(null)
onMounted(() => {
  el.value?.focus()
})
</script>
<template>
  <input ref="el" />
</template>

组件模板引入

vue
<!-- App.vue -->
<script setup lang="ts">
import { useTemplateRef } from 'vue'
import Foo from './Foo.vue'
import Bar from './Bar.vue'
type FooType = InstanceType<typeof Foo>
type BarType = InstanceType<typeof Bar>
const compRef = useTemplateRef<FooType | BarType>('comp')
</script>
<template>
  <component :is="Math.random() > 0.5 ? Foo : Bar" ref="comp" />
</template>

<script setup>

defineModel

  • 第一个参数:如果第一个参数是一个字符串字面量,它将被用作 prop 名称; 否则,prop 名称将默认为 "modelValue"
js
// 声明 "modelValue" prop,由父组件通过 v-model 使用
const model = defineModel()
// 或者:声明带选项的 "modelValue" prop
const model = defineModel({ type: String })
// 在被修改时,触发 "update:modelValue" 事件
model.value = "hello"
js
// 子组件
// 声明 "count" prop,由父组件通过 v-model:count 使用
const count = defineModel("count")
// 或者:声明带选项的 "count" prop
const count = defineModel("count", { type: Number, default: 0 })

function inc() {
  // 在被修改时,触发 "update:count" 事件
  count.value++
}
// 父组件
const myRef = ref()
<Child v-model:count="myRef"></Child>
  • 默认值
js
// 子组件:
const model = defineModel({ default: 1 })
// 父组件
const myRef = ref()
<Child v-model="myRef"></Child>
// => 父组件的 myRef 是 undefined,而子组件的 model 是 1:
  • 修饰符
js
const [modelValue, modelModifiers] = defineModel({
  // get() 省略了,因为这里不需要它
  set(value) {
    // 如果使用了 .trim 修饰符,则返回裁剪过后的值
    if (modelModifiers.trim) {
      return value.trim()
    }
    // 否则,原样返回
    return value
  }
})
  • TS
    ts
    const modelValue = defineModel<string>()
    //    ^? Ref<string | undefined>
    // 用带有选项的默认 model,设置 required 去掉了可能的 undefined 值
    const modelValue = defineModel<string>({ required: true })
    //    ^? Ref<string>
    const [modelValue, modifiers] = defineModel<string, "trim" | "uppercase">()
    //                 ^? Record<'trim' | 'uppercase', true | undefined>

defineExpose

vue
<script setup>
import { ref } from 'vue'

const a = 1
const b = ref(2)

defineExpose({
  a,
  b
})
</script>

defineSlots

不能实现动态控制子组件的调用

vue
<script setup lang="ts">
const slots = defineSlots<{
  default(props: { msg: string }): any
}>()
</script>

useSlots() 和 useAttrs()

vue
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
</script>

如果想要实现动态\无感控制子组件的调用,可以使用 动态组件<component :is="ComA">

vue
<template>
  <div>
    <component ref="component" :is="props.is" />
    <div class="test-btn">
      <Button @click="handleClick" />
    </div>
  </div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import type { Component } from 'vue'
import Button from './Button.vue'

const props = defineProps<{
  is: Component
}>()

const component = ref<any>(null)
const handleClick = () => {
  component.value?.test()
}
</script>
<style scoped>
.test-btn {
  display: flex;
  justify-content: flex-end;
  margin-top: 10px;
}
</style>
vue
<script lang="ts" setup>
import ComA from './components/comA.vue'
</script>
<template>
  <Test :is="ComA" />
</template>

signal

更细颗粒度的响应式 示例代码:

js
function createSignal(initialValue) {
  let value = initialValue
  const subscribers = new Set()

  return {
    get value() {
      // 依赖收集:如果当前有激活的副作用,订阅它
      if (activeEffect) {
        subscribers.add(activeEffect)
      }
      return value
    },
    set value(newValue) {
      if (newValue !== value) {
        value = newValue
        // 通知订阅者更新
        subscribers.forEach(fn => fn())
      }
    }
  }
}

let activeEffect = null

function effect(fn) {
  activeEffect = fn
  fn()         // 初始执行,触发依赖收集
  activeEffect = null
}


const count = createSignal(0)
const doubleCount = createSignal(0)

// 计算双倍的效果
effect(() => {
  debugger
  doubleCount.value = count.value * 2
  console.log('doubleCount updated:', doubleCount.value)
})

// 改变 count,会触发对应依赖 effect 执行
// doubleCount updated: 0
count.value = 1
// 输出:doubleCount updated: 2
// 输出:doubleCount updated: 2
count.value = 2
// 输出:doubleCount updated: 4
// 输出:doubleCount updated: 4