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
}
})
- TSts
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