事件处理函数的缓存
事件处理函数的缓存
Vue3 在事件处理方面做了优化,通过缓存事件处理函数减少不必要的重新渲染。当组件重新渲染时,如果事件处理函数没有变化,Vue 会复用之前创建的函数实例。这种机制在频繁触发更新的场景下能显著提升性能。
const MyComponent = {
setup() {
const count = ref(0)
// 这个函数会被缓存
const increment = () => {
count.value++
}
return {
count,
increment
}
},
template: `
<button @click="increment">
{{ count }}
</button>
`
}
缓存机制实现原理
Vue3 通过 patchProp
函数处理 DOM 属性时,会对事件处理器进行特殊处理。在 runtime-dom/src/patchProp.ts
中,事件处理函数会被包装在一个高阶函数中:
export function patchEvent(
el: Element,
rawName: string,
prevValue: any,
nextValue: any,
instance: ComponentInternalInstance | null = null
) {
// 处理事件绑定逻辑
const invoker = el._vei || (el._vei = {})
const existingInvoker = invoker[rawName]
if (nextValue) {
if (existingInvoker) {
// 更新现有调用器
existingInvoker.value = nextValue
} else {
// 创建新调用器
const name = rawName.slice(2).toLowerCase()
const invoker = (invoker[rawName] = createInvoker(nextValue, instance))
el.addEventListener(name, invoker)
}
} else if (existingInvoker) {
// 移除事件监听
el.removeEventListener(name, existingInvoker)
invoker[rawName] = undefined
}
}
缓存触发条件
事件处理函数被缓存需要满足以下条件:
- 函数引用保持不变
- 组件实例相同
- 事件类型相同
当这些条件满足时,Vue 会复用之前创建的事件监听器。例如在以下情况不会触发重新绑定:
const App = {
setup() {
const handler = () => console.log('clicked')
return { handler }
},
template: `<button @click="handler">Click</button>`
}
动态事件名称处理
对于动态事件名称,Vue3 会进行特殊处理:
const App = {
setup() {
const eventName = ref('click')
const handler = () => console.log('dynamic event')
setTimeout(() => {
eventName.value = 'dblclick'
}, 1000)
return { eventName, handler }
},
template: `<button @[eventName]="handler">Click</button>`
}
在这种情况下,Vue 会先移除旧的事件监听器,然后添加新的事件监听器,但处理函数本身仍然会被缓存。
内联处理函数的特殊情况
直接写在模板中的内联函数不会被缓存:
const App = {
template: `
<button @click="() => console.log('inline')">
Inline Handler
</button>
`
}
每次重新渲染都会创建新的函数实例,这种情况下应该避免在频繁更新的组件中使用内联函数。
自定义事件的缓存
自定义事件同样适用缓存机制:
const Child = {
emits: ['custom'],
setup(props, { emit }) {
const emitEvent = () => emit('custom', 'data')
return { emitEvent }
},
template: `<button @click="emitEvent">Emit</button>`
}
const Parent = {
components: { Child },
setup() {
const handler = (data) => console.log(data)
return { handler }
},
template: `<Child @custom="handler" />`
}
性能优化实践
为了充分利用事件缓存机制,可以遵循以下实践:
- 将事件处理函数定义在
setup
或methods
中 - 避免在渲染函数或模板中创建匿名函数
- 对于复杂组件,使用
useMemo
或computed
优化事件处理函数
const ComplexComponent = {
setup() {
const data = ref([])
const filter = ref('')
// 优化后的处理函数
const filteredHandler = computed(() => {
return () => {
console.log(data.value.filter(item => item.includes(filter.value)))
}
})
return {
data,
filter,
filteredHandler
}
}
}
与 Vue2 的对比
Vue2 中每次更新都会重新创建事件监听器:
// Vue2 示例
new Vue({
methods: {
handleClick() {
console.log('clicked')
}
},
template: `<button @click="handleClick">Click</button>`
})
虽然方法定义在 methods
中,但每次更新都会重新绑定事件。Vue3 的改进在于只在必要时更新事件监听。
源码中的关键函数
在 runtime-core/src/componentRenderUtils.ts
中,renderProps
函数负责处理事件绑定:
function renderProps(
instance: ComponentInternalInstance,
props: Data,
isSVG: boolean
) {
const { attrs, props: propsOptions } = instance
for (const key in props) {
if (isReservedProp(key)) continue
const value = props[key]
if (isOn(key)) {
// 处理事件
patchEvent(
instance.vnode.el as Element,
key,
null,
value,
instance
)
} else {
// 处理普通属性
patchAttr(
instance.vnode.el as Element,
key,
null,
value,
isSVG
)
}
}
}
缓存失效场景
某些情况下缓存会失效:
- 使用动态组件时
- 强制重新挂载组件
- 使用
v-if
切换组件 - 修改事件处理函数的引用
const App = {
setup() {
const handler = () => console.log('original')
const changeHandler = () => {
handler = () => console.log('changed') // 这将导致重新绑定
}
return { handler, changeHandler }
},
template: `
<button @click="handler">Click</button>
<button @click="changeHandler">Change Handler</button>
`
}
与 Composition API 的交互
在 Composition API 中,ref
和 reactive
创建的事件处理函数会被自动缓存:
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
const double = computed(() => count.value * 2)
// 这个函数会被缓存
const increment = () => {
count.value++
}
return {
count,
double,
increment
}
}
}
服务端渲染中的行为
在 SSR 场景下,事件处理函数缓存机制仍然有效,但有以下区别:
- 客户端激活时会重新绑定事件
- 服务端不会实际执行事件绑定
- 水合过程会复用服务端生成的标记
// 服务端组件
export default {
setup() {
const clickHandler = () => {
// 这个函数在服务端不会被调用
console.log('Client only')
}
return { clickHandler }
},
template: `<button @click="clickHandler">SSR Button</button>`
}
调试缓存行为
可以通过以下方式调试事件缓存:
- 使用 Vue Devtools 检查事件监听器
- 在
patchEvent
函数中添加日志 - 比较前后渲染的函数引用
// 自定义 patchEvent 进行调试
const originalPatchEvent = Vue.__patchEvent
Vue.__patchEvent = function(...args) {
console.log('Patching event:', args)
return originalPatchEvent.apply(this, args)
}
与其它框架的对比
React 中也有类似的优化,但实现方式不同:
// React 示例
function ReactComponent() {
const handleClick = useCallback(() => {
console.log('Memoized handler')
}, [])
return <button onClick={handleClick}>Click</button>
}
Vue3 的缓存是自动的,而 React 需要显式使用 useCallback
。Angular 则通过变更检测机制自动优化事件绑定。
高级缓存控制
对于需要精细控制的情况,可以使用 markRaw
标记对象:
import { markRaw } from 'vue'
const rawObject = markRaw({
handler: () => console.log('raw handler')
})
export default {
setup() {
return {
rawObject
}
},
template: `<button @click="rawObject.handler">Raw</button>`
}
这样即使组件重新渲染,事件处理函数也不会被重新创建。
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益,请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:虚拟DOM的静态标记
下一篇:插槽内容的稳定化处理