十年前我刚接触前端开发时,jQuery的$('#id').css()这类DOM操作简直是前端工程师的标配。但在Vue/React这类现代框架中,我们却经常听到"不要直接操作DOM"的忠告。这背后的设计哲学值得深思。
Vue的核心是数据驱动视图,通过响应式系统自动完成DOM更新。但在实际项目中,我们仍会遇到必须直接操作DOM的场景:
我曾在电商项目中遇到一个典型案例:商品图片的懒加载组件。虽然可以用Intersection Observer API,但在需要兼容老版本浏览器时,不得不通过DOM操作实现滚动监听和位置计算。
ref是Vue官方推荐的DOM操作方式。在组合式API中,使用方式尤为简洁:
javascript复制<template>
<input ref="inputRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => {
inputRef.value.focus() // 自动获得焦点
})
</script>
关键细节:
ref在组件挂载完成后才会被赋值经验:在组件销毁前最好手动清除ref引用,避免内存泄漏
虽然不够"Vue",但document.querySelector这类方法在某些场景下依然实用:
javascript复制const element = document.getElementById('my-element')
element.classList.add('active')
适用场景:
性能对比实测:
在1000次DOM操作测试中:
Vue的异步更新机制意味着数据变化后DOM不会立即更新。这时就需要nextTick:
javascript复制import { nextTick } from 'vue'
async function updateContent() {
message.value = '更新了!'
await nextTick()
console.log('DOM已更新:', inputRef.value.textContent)
}
典型使用场景:
对于需要复用的DOM操作,自定义指令是最优雅的方案:
javascript复制// 自动聚焦指令
app.directive('focus', {
mounted(el) {
el.focus()
}
})
进阶技巧:
在渲染列表后操作特定元素是个常见需求。我的解决方案:
javascript复制<template>
<div v-for="item in list" :ref="setItemRef">
{{ item.name }}
</div>
</template>
<script setup>
const itemRefs = []
const setItemRef = el => {
if (el) itemRefs.push(el)
}
</script>
子组件的DOM不应该被父组件直接操作。正确的做法是通过expose暴露方法:
javascript复制// 子组件
const inputRef = ref(null)
defineExpose({
focus: () => inputRef.value.focus()
})
// 父组件
const childRef = ref(null)
childRef.value.focus()
集成jQuery插件的最佳实践:
javascript复制onMounted(() => {
const $el = $(inputRef.value)
$el.pluginName({ options })
})
onUnmounted(() => {
$el.pluginName('destroy')
})
通过计算属性缓存结果:
javascript复制const optimizedList = computed(() =>
bigList.value.filter(item => item.active)
)
使用文档片段减少重绘:
javascript复制const fragment = document.createDocumentFragment()
items.forEach(item => {
const li = document.createElement('li')
li.textContent = item
fragment.appendChild(li)
})
listRef.value.appendChild(fragment)
务必在卸载时清理:
javascript复制onUnmounted(() => {
window.removeEventListener('resize', resizeHandler)
thirdPartyLib.destroy()
})
使用Vue Test Utils的DOM API:
javascript复制test('focuses input', async () => {
const wrapper = mount(MyComponent)
await wrapper.find('input').trigger('focus')
expect(document.activeElement).toBe(wrapper.find('input').element)
})
$0访问当前选中的元素inspect(componentRef.value)调试Vue ref在大型项目中,我形成了这样的DOM操作规范:
例如,我们将富文本编辑器的集成封装成了:
javascript复制const { editorRef, initialize, destroy } = useRichTextEditor()
这种模式既保持了Vue的数据流清晰,又能应对复杂的DOM需求。