一文完全掌握Vue中的$set方法
作者:upward_tomato
这篇文章主要给大家介绍了关于如何完全掌握Vue中$set方法的相关资料,vue中$set方法对数组和对象的处理本质上的一样的,对新增的值添加响应然后手动触发派发更新,需要的朋友可以参考下
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.通过数组索引修改数据
对应源码
完整源码
export function set (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 in Object.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. 接受参数类型分别为 数组/对象; 任意 ; 任意 export function set(target: Array<any> | Object, key: any, val: any): any { // 2. 判断第一个参数 不为 undefined null string number symbol boolean if ( 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 in Object.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 } /* 工具函数 */ function isPrimitive(value) { return ( typeof value === 'string' || typeof value === 'number' || // $flow-disable-line typeof value === 'symbol' || typeof value === 'boolean' ) } // explicitness and function inlining. function isUndef(v) { return v === undefined || v === null } // 是否是有效的数组索引 function isValidArrayIndex(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. * 如果需要,删除属性并触发更改。 */ export function del(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; } // 如果 属性不是自身的属性,直接 return if (!hasOwn(target, key)) { return; } // 删除对应的key delete target[key]; // 不是响应式的不做处理(这个地方可以理解为,浅层监听的 watch,有些深层的属性不需要watch,就会走这个情况) if (!ob) { return; } // 手动触发 !! 有作者在想,直接在代码中 `.__ob__` 手动通知不就ok了? 虽然可以但是不建议这样做、 ob.dep.notify(); }
end
上述的演示,源码查看的是 vue@2.6
。 vue3中由于响应式实现原理发生了变化,所以不需要 $set 了,所以不做探究。
到此这篇关于完全掌握Vue中$set方法的文章就介绍到这了,更多相关Vue $set方法内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!