阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 事件处理函数的缓存

事件处理函数的缓存

作者:陈川 阅读数:43795人阅读 分类: Vue.js

事件处理函数的缓存

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

缓存触发条件

事件处理函数被缓存需要满足以下条件:

  1. 函数引用保持不变
  2. 组件实例相同
  3. 事件类型相同

当这些条件满足时,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" />`
}

性能优化实践

为了充分利用事件缓存机制,可以遵循以下实践:

  1. 将事件处理函数定义在 setupmethods
  2. 避免在渲染函数或模板中创建匿名函数
  3. 对于复杂组件,使用 useMemocomputed 优化事件处理函数
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
      )
    }
  }
}

缓存失效场景

某些情况下缓存会失效:

  1. 使用动态组件时
  2. 强制重新挂载组件
  3. 使用 v-if 切换组件
  4. 修改事件处理函数的引用
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 中,refreactive 创建的事件处理函数会被自动缓存:

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 场景下,事件处理函数缓存机制仍然有效,但有以下区别:

  1. 客户端激活时会重新绑定事件
  2. 服务端不会实际执行事件绑定
  3. 水合过程会复用服务端生成的标记
// 服务端组件
export default {
  setup() {
    const clickHandler = () => {
      // 这个函数在服务端不会被调用
      console.log('Client only')
    }
    
    return { clickHandler }
  },
  template: `<button @click="clickHandler">SSR Button</button>`
}

调试缓存行为

可以通过以下方式调试事件缓存:

  1. 使用 Vue Devtools 检查事件监听器
  2. patchEvent 函数中添加日志
  3. 比较前后渲染的函数引用
// 自定义 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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌