一 Vue 中的 $set
方法详解
$set
是 Vue.js 提供的一个重要 API,用于解决 Vue 响应式系统的限制问题。下面我将详细介绍它的用法、原理和实际应用场景。
1. 基本介绍
$set
是 Vue 实例的一个方法,用于向响应式对象添加一个新的 property,并确保这个新 property 同样是响应式的,且触发视图更新。
语法
javascript
vm.$set(target, propertyName/index, value)
参数
target
:要修改的目标对象(Object 或 Array)propertyName/index
:要添加或修改的属性名(对象)或索引(数组)value
:要设置的值
返回值
设置的值
2. 为什么需要 $set
Vue 2.x 使用 Object.defineProperty
实现响应式,它有一些限制:
-
无法检测对象属性的添加或删除
javascriptdata() { return { user: { name: '张三' } } } // 这样添加的属性不是响应式的 this.user.age = 25
-
无法检测数组索引直接设置项
javascriptdata() { return { items: ['a', 'b', 'c'] } } // 这样修改数组项不是响应式的 this.items[1] = 'x'
$set
就是用来解决这些问题的。
3. 使用示例
对象属性添加
javascript
export default {
data() {
return {
user: {
name: '张三',
age: 20
}
}
},
methods: {
addProperty() {
// 错误方式 - 不是响应式的
// this.user.gender = '男'
// 正确方式
this.$set(this.user, 'gender', '男')
}
}
}
数组项修改
javascript
export default {
data() {
return {
items: ['苹果', '香蕉', '橙子']
}
},
methods: {
updateItem() {
// 错误方式 - 不是响应式的
// this.items[1] = '西瓜'
// 正确方式
this.$set(this.items, 1, '西瓜')
}
}
}
动态添加嵌套属性
javascript
export default {
data() {
return {
formData: {}
}
},
methods: {
initForm() {
// 动态添加嵌套的响应式属性
this.$set(this.formData, 'userInfo', {})
this.$set(this.formData.userInfo, 'name', '李四')
}
}
}
4. 与 Vue.set 的关系
$set
是 Vue 实例方法,而 Vue.set
是全局 API,两者功能完全相同:
javascript
// 在组件内部
this.$set(target, key, value)
// 在任何地方
Vue.set(target, key, value)
5. 原理分析
$set
的实现主要做了以下几件事:
- 如果目标是数组,使用
splice
方法(Vue 重写了数组的变异方法) - 如果目标是对象,且属性已存在,直接赋值
- 如果目标是对象,且属性不存在:
- 将属性添加到对象
- 使用
defineReactive
方法使其成为响应式 - 触发依赖通知
6. 替代方案
在某些情况下,可以使用以下替代方案:
对象替代方案
javascript
// 使用 Object.assign 创建新对象
this.user = Object.assign({}, this.user, { gender: '男' })
// 使用扩展运算符
this.user = { ...this.user, gender: '男' }
数组替代方案
javascript
// 使用数组变异方法
this.items.splice(1, 1, '西瓜')
7. 注意事项
- 不要滥用 $set :对于已知的属性,应该在
data
中预先声明 - 性能考虑 :频繁使用
$set
会影响性能,应考虑数据结构设计 - Vue 3 的变化 :Vue 3 使用 Proxy 实现响应式,不再需要
$set
8. 实际应用场景
场景1:动态表单字段
javascript
export default {
data() {
return {
form: {
basicInfo: {
name: '',
age: ''
}
}
}
},
methods: {
addCustomField(fieldName) {
if (!this.form.basicInfo.hasOwnProperty(fieldName)) {
this.$set(this.form.basicInfo, fieldName, '')
}
}
}
}
场景2:表格行编辑
javascript
export default {
data() {
return {
tableData: [
{ id: 1, name: '产品A', price: 100 },
{ id: 2, name: '产品B', price: 200 }
]
}
},
methods: {
updatePrice(index, newPrice) {
this.$set(this.tableData[index], 'price', newPrice)
}
}
}
场景3:树形结构操作
javascript
export default {
data() {
return {
treeData: {
id: 1,
label: '根节点',
children: []
}
}
},
methods: {
addChildNode(parentNode, newNode) {
if (!parentNode.children) {
this.$set(parentNode, 'children', [])
}
parentNode.children.push(newNode)
}
}
}
9. 常见问题
Q: 为什么直接赋值不生效?
A: Vue 2.x 的响应式系统无法检测属性的添加或删除,必须使用 $set
或预先声明所有属性。
Q: $set
和 Vue.set
有什么区别?
A: 功能完全相同,只是调用方式不同。$set
是实例方法,Vue.set
是全局方法。
Q: Vue 3 还需要 $set
吗?
A: Vue 3 使用 Proxy 实现响应式,可以检测属性添加/删除,大多数情况下不再需要 $set
。
总结
$set
是 Vue 2.x 中解决响应式限制的重要工具,合理使用可以确保数据变化的响应性。但在 Vue 3 中,由于响应式系统的改进,$set
的使用场景大大减少。在开发中,应根据 Vue 版本和具体场景选择合适的数据操作方式。
二 Vue 中的 $nextTick
方法详解
$nextTick
是 Vue.js 中一个非常重要的 API,它允许你在 DOM 更新完成后执行延迟回调。下面我将全面介绍它的工作原理、使用场景和最佳实践。
1. 核心概念
什么是 $nextTick
?
$nextTick
是一个异步方法,它将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,可以获取更新后的 DOM。
基本语法
javascript
// 作为实例方法
this.$nextTick(() => {
// DOM 更新完成后执行的代码
})
// 作为全局 API
Vue.nextTick(() => {
// DOM 更新完成后执行的代码
})
// 也可以返回 Promise
this.$nextTick().then(() => {
// DOM 更新完成后执行的代码
})
2. 为什么需要 $nextTick
Vue 的 DOM 更新是异步的,当数据变化时:
- Vue 开启一个队列
- 缓冲在同一事件循环中发生的所有数据变更
- 在下一个事件循环"tick"中刷新队列
这种机制导致:
javascript
this.message = '更新后的消息'
console.log(this.$el.textContent) // 这里获取的还是旧DOM
this.$nextTick(() => {
console.log(this.$el.textContent) // 这里获取的是更新后的DOM
})
3. 典型使用场景
场景1:获取更新后的 DOM
javascript
methods: {
updateMessage() {
this.message = '新消息'
this.$nextTick(() => {
// 现在可以获取更新后的DOM
const height = this.$refs.messageBox.offsetHeight
console.log('更新后的高度:', height)
})
}
}
场景2:与第三方库集成
javascript
methods: {
initEditor() {
this.content = '<p>初始内容</p>'
this.$nextTick(() => {
// 确保DOM更新后再初始化编辑器
this.editor = new Editor(this.$refs.editor)
})
}
}
场景3:滚动到最新项
javascript
methods: {
addItem() {
this.items.push(newItem)
this.$nextTick(() => {
// 滚动到最新添加的元素
const lastItem = this.$refs.items[this.items.length - 1]
lastItem.scrollIntoView()
})
}
}
4. 工作原理
Vue 的异步更新队列机制:
- 数据变更:当你修改响应式数据时
- 虚拟DOM:Vue 开始重新渲染虚拟DOM
- 队列:将 DOM 更新操作放入队列
- 事件循环 :
- 当前调用栈执行完毕
- 开始处理微任务队列
- 执行 DOM 更新
- nextTick回调 :此时执行
$nextTick
的回调
5. 与 Promise 的关系
$nextTick
返回一个 Promise,所以可以这样使用:
javascript
async updateData() {
this.data = await fetchData()
await this.$nextTick()
console.log('DOM已更新')
// 继续其他操作
}
6. 常见问题解答
Q: $nextTick
和 setTimeout(fn, 0)
有什么区别?
A:
$nextTick
优先级更高,在微任务阶段执行setTimeout
是宏任务,执行时机更晚$nextTick
能确保在 Vue 的 DOM 更新后立即执行
Q: 为什么有时候不用 $nextTick
也能获取更新后的DOM?
A:
- 在某些同步代码块中,浏览器可能会在同一个tick中完成渲染
- 但这不可靠,依赖这种行为会导致难以追踪的bug
Q: $nextTick
会阻塞渲染吗?
A:
- 不会,它只是将回调推迟到DOM更新之后
- 回调执行时界面已经更新完成
7. 最佳实践
-
避免嵌套 :不要在一个
$nextTick
回调中再嵌套$nextTick
-
合理使用 :不是所有DOM操作都需要
$nextTick
,只有依赖更新后DOM时才需要 -
错误处理:
javascript
this.$nextTick()
.then(() => {
// 成功回调
})
.catch(err => {
console.error('nextTick出错:', err)
})
- 组件销毁处理:
javascript
mounted() {
this.$nextTick(() => {
if (!this._isDestroyed) {
// 安全操作
}
})
}
8. Vue 3 中的变化
Vue 3 中 nextTick
的行为基本保持一致,但有一些小变化:
-
从
Vue.nextTick
改为直接导入:javascriptimport { nextTick } from 'vue' nextTick(() => {...})
-
在组合式API中使用:
javascriptsetup() { const count = ref(0) async function increment() { count.value++ await nextTick() console.log('DOM已更新') } return { count, increment } }
9. 性能考虑
虽然 $nextTick
很有用,但过度使用会影响性能:
- 不要滥用:只在真正需要时使用
- 批量操作:多个DOM操作尽量放在同一个回调中
- 替代方案 :考虑使用
watch
或watchEffect
监听变化
10. 实际案例
案例1:自动聚焦输入框
javascript
methods: {
showInput() {
this.isShow = true
this.$nextTick(() => {
this.$refs.input.focus()
})
}
}
案例2:测量元素尺寸
javascript
methods: {
updateLayout() {
this.layoutChanged = true
this.$nextTick(() => {
const width = this.$refs.container.offsetWidth
// 根据新宽度调整布局
})
}
}
案例3:集成非响应式插件
javascript
methods: {
initPlugin() {
this.dataLoaded = true
this.$nextTick(() => {
// 确保DOM渲染完成后再初始化插件
this.plugin = new ThirdPartyPlugin(this.$el)
})
}
}
总结
$nextTick
是 Vue 响应式系统的关键部分,它解决了以下问题:
- 在数据变化后安全地操作DOM
- 确保获取到最新的DOM状态
- 与第三方库正确集成
记住它的核心原则:将回调延迟到下次DOM更新循环之后执行。合理使用这个API可以避免许多常见的异步更新问题。
三 Vue 中的 $refs
详解
$refs
是 Vue 提供的一个重要特性,用于直接访问 DOM 元素或子组件实例。下面我将全面介绍它的用法、注意事项和最佳实践。
1. 基本概念
什么是 $refs
?
$refs
是一个对象,持有注册过 ref
特性的所有 DOM 元素和子组件实例。
核心特点:
- 引用类型:可以是 DOM 元素或组件实例
- 响应式:不是响应式的,只作为直接访问子组件的"逃生舱"
- 生命周期:在组件挂载完成后填充,不是响应式的
2. 基本用法
引用 DOM 元素
html
<template>
<input ref="inputRef" type="text">
<button @click="focusInput">聚焦输入框</button>
</template>
<script>
export default {
methods: {
focusInput() {
this.$refs.inputRef.focus()
}
}
}
</script>
引用子组件
html
<template>
<child-component ref="childRef"></child-component>
<button @click="callChildMethod">调用子组件方法</button>
</template>
<script>
export default {
methods: {
callChildMethod() {
this.$refs.childRef.someMethod()
}
}
}
</script>
3. 使用场景
场景1:表单操作
javascript
methods: {
validateForm() {
this.$refs.formRef.validate(valid => {
if (valid) {
// 提交表单
}
})
}
}
场景2:与第三方库集成
javascript
mounted() {
this.$nextTick(() => {
this.chart = new Chart(this.$refs.chartCanvas, {
// 图表配置
})
})
}
场景3:父组件调用子组件方法
javascript
// 父组件
methods: {
refreshData() {
this.$refs.childComponent.loadData()
}
}
// 子组件
methods: {
loadData() {
// 获取数据逻辑
}
}
4. 生命周期与 $refs
$refs
在不同生命周期的状态:
生命周期钩子 | $refs 状态 |
---|---|
beforeCreate | {} |
created | {} |
beforeMount | {} |
mounted | 已填充所有ref |
beforeUpdate | 包含当前refs |
updated | 包含更新后的refs |
beforeDestroy | 仍然可以访问 |
destroyed | undefined |
5. 注意事项
-
不是响应式的:
javascript// 错误用法 - 不会自动更新 this.$refs.someRef = newValue
-
v-for 中的 ref:
- 使用 v-for 时,
$refs
会是一个数组
html<div v-for="item in list" :ref="setItemRef"></div>
javascriptdata() { return { itemRefs: [] } }, methods: { setItemRef(el) { if (el) { this.itemRefs.push(el) } } }
- 使用 v-for 时,
-
动态 ref:
html<component :is="currentComponent" :ref="dynamicRef"></component>
-
避免过度使用:
- 优先使用 props 和 events 进行组件通信
- 仅在需要直接访问 DOM 或子组件方法时使用
6. Vue 3 中的变化
在 Vue 3 中:
-
组合式 API 中使用 ref:
html<template> <div ref="root"></div> </template> <script> import { ref, onMounted } from 'vue' export default { setup() { const root = ref(null) onMounted(() => { console.log(root.value) // <div></div> }) return { root } } } </script>
-
v-for 中的 ref不再自动创建数组,需要使用函数ref:
javascriptconst itemRefs = ref([]) const setItemRef = el => { if (el) { itemRefs.value.push(el) } }
7. 最佳实践
-
命名规范:
- 使用有意义的ref名称
- 推荐后缀:
Ref
(如inputRef
,formRef
)
-
安全访问:
javascriptif (this.$refs.myRef) { // 安全操作 }
-
配合
$nextTick
:javascriptthis.showComponent = true this.$nextTick(() => { this.$refs.myComponent.doSomething() })
-
避免滥用:
- 优先使用 props/events
- 避免用refs修改子组件状态
8. 常见问题解答
Q: 为什么我的 $refs
是空的?
A: 可能原因:
- 在
mounted
之前访问 - ref所在的元素有
v-if
且条件为false - 组件未正确挂载
Q: 如何在父组件访问孙组件?
A: 不推荐直接访问,应该:
- 通过子组件暴露方法
- 使用 provide/inject
- 使用事件总线或状态管理
Q: $refs
和 $el
有什么区别?
A:
$el
是组件自身的根元素$refs
是通过ref属性注册的任意元素或组件
9. 实际案例
案例1:图片预览组件
javascript
methods: {
zoomIn() {
this.$refs.image.style.transform = 'scale(1.2)'
},
resetZoom() {
this.$refs.image.style.transform = 'scale(1)'
}
}
案例2:表单组件集成
javascript
submitForm() {
this.$refs.form.validate().then(() => {
// 验证通过
}).catch(() => {
// 验证失败
this.$refs.firstErrorField.focus()
})
}
案例3:视频播放控制
javascript
playVideo() {
this.$refs.videoPlayer.play()
},
pauseVideo() {
this.$refs.videoPlayer.pause()
}
10. 替代方案
在某些情况下,可以考虑替代方案:
- DOM 事件:使用原生事件代替直接DOM操作
- 自定义事件 :通过
$emit
实现组件通信 - 作用域插槽:通过插槽prop暴露数据和方法
总结
$refs
是 Vue 中一个强大的特性,但需要谨慎使用:
✅ 适用场景:
- 集成第三方库需要DOM元素
- 触发子组件方法
- 访问DOM属性/方法
❌ 避免场景:
- 组件间常规通信
- 频繁修改子组件状态
- 替代Vue的数据驱动方式
合理使用 $refs
可以解决特定问题,但过度使用会导致代码难以维护。在大多数情况下,优先考虑使用 Vue 的声明式数据流和组件通信机制。