start
- 今天在使用 $set 的时候,发现如果 被赋值的数据 层级较深会出现报错的情况。
- 一知半解,是我最讨厌的状态,今天就带着问题,再阅读一下对应的源码,了解问题的本质。
问题说明
简单说明一下我遇到的问题,明确探究问题的目标。
需求
我有一个空对象,我希望可以给它的属性的属性的属性赋值。
错误代码:
<template>
<div>
lazy_tomato
<h2>{{ obj }}</h2>
<button @click="handleChange">点击我给obj赋值</button>
</div>
</template>
<script>
export default {
data() {
return {
obj: {},
}
},
methods: {
handleChange() {
this.obj.a = {
b: {
c: '爱吃番茄',
},
}
console.log(JSON.stringify(this.obj))
// 直接新增属性,不会触发 vue2本质的Object.defineProperty。所以数据更新了视图不更新
},
},
}
</script>
正确代码
<template>
<div>
lazy_tomato
<h2>{{ obj }}</h2>
<button @click="handleChange">点击我给obj赋值</button>
</div>
</template>
<script>
export default {
data() {
return {
obj: {},
}
},
methods: {
handleChange() {
// 错误代码二 typeError: Cannot read properties of undefined (reading '__ob__')
// this.$set(this.obj.a, 'b.c', '爱吃番茄')
// 正确代码
this.$set(this.obj, 'a', { b: { c: '爱吃番茄' } })
console.log(JSON.stringify(this.obj))
},
},
}
</script>
所以 $set 对这三个参数分别是如何处理的?如何避免我们错误使用?
官方文档
区分 Vue.set 和 vm.$set
**
Vue
构造函数自身上的
set
和
vm
实例上的
$set
是相同的函数。**
解决了以下问题:
1.新增对象的属性
2.删除对象的属性
3.通过数组索引修改数据
对应源码
完整源码
exportfunctionset(target: Array<any>| Object,key: any,val: any): any {if(process.env.NODE_ENV!=='production'&&(isUndef(target)||isPrimitive(target))){warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)}if(Array.isArray(target)&&isValidArrayIndex(key)){
target.length = Math.max(target.length, key)
target.splice(key,1, val)return val
}if(key in target &&!(key inObject.prototype)){
target[key]= val
return val
}const ob =(target: any).__ob__
if(target._isVue ||(ob && ob.vmCount)){
process.env.NODE_ENV!=='production'&&warn('Avoid adding reactive properties to a Vue instance or its root $data '+'at runtime - declare it upfront in the data option.')return val
}if(!ob){
target[key]= val
return val
}defineReactive(ob.value, key, val)
ob.dep.notify()return val
}
分析源码
// 1. 接受参数类型分别为 数组/对象; 任意 ; 任意exportfunctionset(target: Array<any>| Object,key: any,val: any): any {// 2. 判断第一个参数 不为 undefined null string number symbol booleanif(
process.env.NODE_ENV!=='production'&&(isUndef(target)||isPrimitive(target))){warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)}// 3. 如果是数组,而且第二个参数是有效索引if(Array.isArray(target)&&isValidArrayIndex(key)){// 更新数组长度 有可能传入的索引大于现有索引
target.length = Math.max(target.length, key)// 调用 splice
target.splice(key,1, val)// // 返回值是设置的值return val
}// 4. 是该对象的属性 (且不是原型链上的属性)if(key in target &&!(key inObject.prototype)){// 直接赋值 (这里赋值可以触发 Object.defineProperty)
target[key]= val
// 返回值是设置的值return val
}// 5. 获取 observe实例const ob =(target: any).__ob__
if(target._isVue ||(ob && ob.vmCount)){
process.env.NODE_ENV!=='production'&&warn('Avoid adding reactive properties to a Vue instance or its root $data '+'at runtime - declare it upfront in the data option.')return val
}// 6. 无observe实例,直接赋值,// 返回值是设置的值if(!ob){
target[key]= val
return val
}// 7. 收集依赖defineReactive(ob.value, key, val)// 8. 手动通知,触发视图更新
ob.dep.notify()// // 返回值是设置的值return val
}/* 工具函数 */functionisPrimitive(value){return(typeof value ==='string'||typeof value ==='number'||// $flow-disable-linetypeof value ==='symbol'||typeof value ==='boolean')}// explicitness and function inlining.functionisUndef(v){return v ===undefined|| v ===null}// 是否是有效的数组索引functionisValidArrayIndex(val){const n =parseFloat(String(val))return n >=0&& Math.floor(n)=== n &&isFinite(val)}
小结:
主要的处理顺序:
- 处理数组(使用 劫持过的数组 splice 方法);
- 处理对象上自带的属性;
- 收集依赖,手动触发。
// this.$set(this.obj.a, 'b.c', '爱吃番茄')
错误的原因,this.obj.a 本身是 undefined 所以直接被第一步就拦截了。
// this.obj.a={}// this.$set(this.obj.a, 'b.c', '爱吃番茄')
也达不到效果,它会直接吧 b.c当做属性名初始化
思考:
- 虽然官方文档设定,第二个参数是数字和字符串,理论上可以传入其他类型的。
- 第二个参数最好是单层级的属性值
扩展 :del 方法
/**
* Delete a property and trigger change if necessary.
* 如果需要,删除属性并触发更改。
*/exportfunctiondel(target: Array<any>| Object,key: any){if(
process.env.NODE_ENV!=="production"&&// 如果是 undefined 或 null; 或者是原始值 ---同Vue.$set(isUndef(target)||isPrimitive(target))){warn(`Cannot delete reactive property on undefined, null, or primitive value: ${target}`);}// 数组,利用splice,直接改if(Array.isArray(target)&&isValidArrayIndex(key)){
target.splice(key,1);return;}// ---同Vue.$set 排除Vue实例 和 根对象const ob =(target: any).__ob__;if(target._isVue ||(ob && ob.vmCount)){
process.env.NODE_ENV!=="production"&&warn("Avoid deleting properties on a Vue instance or its root $data "+"- just set it to null.");return;}// 如果 属性不是自身的属性,直接 returnif(!hasOwn(target, key)){return;}// 删除对应的keydelete target[key];// 不是响应式的不做处理(这个地方可以理解为,浅层监听的 watch,有些深层的属性不需要watch,就会走这个情况)if(!ob){return;}// 手动触发 !! 有作者在想,直接在代码中 `.__ob__` 手动通知不就ok了? 虽然可以但是不建议这样做、
ob.dep.notify();}
end
上述的演示,源码查看的是
[email protected]
。 vue3中由于响应式实现原理发生了变化,所以不需要 $set 了,所以不做探究。
版权归原作者 upward_tomato 所有, 如有侵权,请联系我们删除。