我似乎发现了vue的一个bug
公元2021年7月23日,我以为我发现了vue的一个bug,此时此刻,我离给vue提issue只有1个字节的距离。
用过vue的同学应该都知道Vue.set
这个api的用法吧,来,今天教你一个"新玩法"。。
事件还原
事情得从同事的一行代码说起,看这里:
if (data.result.length > 0) {
data.result.forEach(item1 => {
this.assoStatList.forEach((item2, index2) => {
if (item1.LGTD == item2.LGTD && item1.LTTD == item2.LTTD) {
// this.$set(item2, 'RAIN_FORECAST_24H', item1.RAIN_FORECAST_24H)
// item2.RAIN_FORECAST_24H = item1.RAIN_FORECAST_24H
// this.$set(this.assoStatList, index2, item2)
this.$set(this.assoStatList, item2, (item2.RAIN_FORECAST_24H = item1.RAIN_FORECAST_24H))
console.log(this.assoStatList)
console.log(item2)
}
})
})
}
划重点:
this.$set(this.assoStatList, item2, (item2.RAIN_FORECAST_24H = item1.RAIN_FORECAST_24H))
就是这行代码,我觉得写错了,因为参数传得不对了。
话不多说,来看官方文档:
显然,同事这个写法明显是有问题的,按文档意思,第一个参数如果是数组,那第二个应该给索引值,他这里居然给了个对象!我难以忍受,甚至手把手地教他怎么按文档来,然后用两种正确方式都写出来了。
vue出bug了?
但是同事坚持说他没有错,然后执行给我看,结果确实没有报错,而且正常执行了!?后面在用到this.assoStatList
的代码可以正常执行。这让我相当的尴尬!
我又仔细看了文档,上面写得很清楚,第二个参数要么是字符串键值,要么是索引,这取决于你要操作的对象即第一个参数是对象还是数组;第三个参数没做限制,写个函数也没问题。
这矛盾的情况让我百思不得其解,直觉告诉我,这里面有问题:
这张图是我打印的被赋值过的数组this.assoStatList
,结果发现这个数组除了正常的索引元素,还多了一个属性[object Object]
,属性值为0,而这个0就是item1.RAIN_FORECAST_24H带过来的值。
从这张图可以看出来,第三个参数的赋值操作成功了,但是同时也给数组this.assoStatList
加了一个[object Object]
属性,这个属性其实是多余的,并不是我要的。
所以如果严格按照文档来,这种写法肯定是错误的,只是钻了空子,没报错而已。
至于这种写法为什么会不报错,我本着认真的钻研精神开始了下面的一系列分析,先从vue源码来看。
源码分析
源码位置:src/core/observer/index.js
我从vue2中找到set定义的完整代码:
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/
export function set (target: Array<any> | Object, key: any, val: any): any {
// target如果是`undefined`、`null`或是原始类型,则报错
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 如果target是数组且key是数组索引,则修改数组对应键的值
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__
// 如果是Vue对象或者是Vue实例的根数据对象,则报错
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方法赋值
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
看源码,我加上了备注,还是很容易看明白的。在我们这种条件下,target
是数组但key
不是索引值,代码最后其实会走到defineReactive
,那就顺藤摸瓜继续找。。
源码位置:src/core/observer/index.js
class Observer {
constructor (data) {
this.walk(data)
}
walk (data) {
// 遍历 data 对象属性,调用 defineReactive 方法
let keys = Object.keys(data)
for(let i = 0; i < keys.length; i++){
defineReactive(data, keys[i], data[keys[i]])
}
}
}
// defineReactive方法仅仅将data的属性转换为访问器属性即响应式
function defineReactive (data, key, val) {
// 递归观测子属性
observer(val)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
return val
},
set: function (newVal) {
if(val === newVal){
return
}
// 对新值进行观测
observer(newVal)
}
})
}
// observer 方法首先判断data是不是纯JavaScript对象,如果是,调用 Observer 类进行观测
function observer (data) {
if(Object.prototype.toString.call(data) !== '[object Object]') {
return
}
new Observer(data)
}
这段代码的意思就是给响应式属性设置值,我加上了注释,应该容易看明白,不过重点在这一步:Object.defineProperty!请接着往下看:
终极奥义?
上面的代码在调用Object.defineProperty方法时,就会把对象类型的键值转换为'[object Object]'
类型的字符串,最后给数组加了一个'[Object object]'
属性。不明白的可以看下mdn上的defineProperty定义[2],然后走下这段代码:
let Person = [1,2]
Object.defineProperty(Person, {sL:1}, {
value: 'jack', // 属性值
writable: true // 是否可以改变
});
Person.s = 2;
console.log(Person) // logs [1, 2, s: 2, [object Object]: "jack"]
输出的结果非常奇葩!直观看起来,[object Object]: "jack"
似乎也是一个值,但是仔细一想又不是,因为它不是一个对象,你把[1, 2, s: 2, [object Object]: "jack"]
输入控制台是会报错的,但是Person['[object Object]']
又可以取到值jack
,然后你再看一下Person的length,你会发现,长度却是2!我把Person展开来看,发现是这样的:
这样看应该比较明白了,你可以把Person理解为特殊的对象,数组本身的索引值也是这个“对象”的键值,"s"也是键值,[object Object]
也是键值。为了谨慎起见,我又打印了它的键值:
Object.keys(Person) // ["0", "1", "s"]
看到这结果,是不是又被惊讶到了?刚刚明明似乎看到了[object Object]
这个键值,结果在这里却没有被打印出来!?好吧,神奇的js。。。我猜也许是因为[object Object]
这个键值的特殊性吧,而且你用for in 去遍历Person
的属性,也是遍历不到[object Object]
的,也就是说这个属性不是可枚举的。
[object Object]
最后的最后还是要说一下[object Object]
,它是怎么来的呢?其实它是通过toString[5]方法得到的,mdn上对它是有说明的,就是在默认情况下任何对象调用toString()都会返回返回 "[object type]"。可以看下面的代码:
var s = new Object();
s.toString() // [object Object]
但是我们知道数组也有toString方法,它覆盖了默认的toString
方法,所以并不会输出"[object Array]",如果要输出这个结果,可以这样写:
var a = new Array();
toString.call(a)
回到上一步,也就是说Object.defineProperty
实际上是把它的第二个参数强制toSring了,所以在文章最开始的地方,我们执行这句this.$set(array, object, ())
会得到一个包含[object Object]
属性的数组。
要不要提issue?
好了,本文有点冷门,本来只是怀疑找到了vue的一个bug,结果搞出了这么多瓜来,把我吃撑了都!但是话说回来,如果api规定了参数及类型,入参传错了,即使执行不报错,从严格上来讲也要警告才对,所以,要不要提issue呢?
参考资料:
https://blog.csdn.net/leelxp/article/details/107212555 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty https://www.jianshu.com/p/8fe1382ba135 http://hcysun.me/2017/03/03/Vue%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0/ https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/toString https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/in
扫码关注 字节逆旅 公众号,为您奉献更多技术干货!