我似乎發(fā)現(xiàn)了vue的一個(gè)bug

公元2021年7月23日,我以為我發(fā)現(xiàn)了vue的一個(gè)bug,此時(shí)此刻,我離給vue提issue只有1個(gè)字節(jié)的距離。
用過(guò)vue的同學(xué)應(yīng)該都知道Vue.set這個(gè)api的用法吧,來(lái),今天教你一個(gè)"新玩法"。。
事件還原
事情得從同事的一行代碼說(shuō)起,看這里:
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)
}
})
})
}
劃重點(diǎn):
this.$set(this.assoStatList, item2, (item2.RAIN_FORECAST_24H = item1.RAIN_FORECAST_24H))
就是這行代碼,我覺(jué)得寫錯(cuò)了,因?yàn)閰?shù)傳得不對(duì)了。
話不多說(shuō),來(lái)看官方文檔:
顯然,同事這個(gè)寫法明顯是有問(wèn)題的,按文檔意思,第一個(gè)參數(shù)如果是數(shù)組,那第二個(gè)應(yīng)該給索引值,他這里居然給了個(gè)對(duì)象!我難以忍受,甚至手把手地教他怎么按文檔來(lái),然后用兩種正確方式都寫出來(lái)了。
vue出bug了?
但是同事堅(jiān)持說(shuō)他沒(méi)有錯(cuò),然后執(zhí)行給我看,結(jié)果確實(shí)沒(méi)有報(bào)錯(cuò),而且正常執(zhí)行了!?后面在用到this.assoStatList的代碼可以正常執(zhí)行。這讓我相當(dāng)?shù)膶擂危?/p>
我又仔細(xì)看了文檔,上面寫得很清楚,第二個(gè)參數(shù)要么是字符串鍵值,要么是索引,這取決于你要操作的對(duì)象即第一個(gè)參數(shù)是對(duì)象還是數(shù)組;第三個(gè)參數(shù)沒(méi)做限制,寫個(gè)函數(shù)也沒(méi)問(wèn)題。
這矛盾的情況讓我百思不得其解,直覺(jué)告訴我,這里面有問(wèn)題:
這張圖是我打印的被賦值過(guò)的數(shù)組this.assoStatList,結(jié)果發(fā)現(xiàn)這個(gè)數(shù)組除了正常的索引元素,還多了一個(gè)屬性[object Object],屬性值為0,而這個(gè)0就是item1.RAIN_FORECAST_24H帶過(guò)來(lái)的值。

從這張圖可以看出來(lái),第三個(gè)參數(shù)的賦值操作成功了,但是同時(shí)也給數(shù)組this.assoStatList加了一個(gè)[object Object]屬性,這個(gè)屬性其實(shí)是多余的,并不是我要的。

所以如果嚴(yán)格按照文檔來(lái),這種寫法肯定是錯(cuò)誤的,只是鉆了空子,沒(méi)報(bào)錯(cuò)而已。
至于這種寫法為什么會(huì)不報(bào)錯(cuò),我本著認(rèn)真的鉆研精神開(kāi)始了下面的一系列分析,先從vue源碼來(lái)看。
源碼分析
源碼位置: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`或是原始類型,則報(bào)錯(cuò)
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 如果target是數(shù)組且key是數(shù)組索引,則修改數(shù)組對(duì)應(yīng)鍵的值
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 如果是對(duì)象且傳入屬性存在于對(duì)象中,則修改屬性值
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// 判斷是否是響應(yīng)式對(duì)象
const ob = (target: any).__ob__
// 如果是Vue對(duì)象或者是Vue實(shí)例的根數(shù)據(jù)對(duì)象,則報(bào)錯(cuò)
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
}
// 如果是非響應(yīng)式的普通對(duì)象,則給上屬性值就可以了
if (!ob) {
target[key] = val
return val
}
// 如果是響應(yīng)式對(duì)象,則調(diào)用defineReactive方法賦值
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
看源碼,我加上了備注,還是很容易看明白的。在我們這種條件下,target是數(shù)組但key不是索引值,代碼最后其實(shí)會(huì)走到defineReactive,那就順藤摸瓜繼續(xù)找。。
源碼位置:src/core/observer/index.js
class Observer {
constructor (data) {
this.walk(data)
}
walk (data) {
// 遍歷 data 對(duì)象屬性,調(diào)用 defineReactive 方法
let keys = Object.keys(data)
for(let i = 0; i < keys.length; i++){
defineReactive(data, keys[i], data[keys[i]])
}
}
}
// defineReactive方法僅僅將data的屬性轉(zhuǎn)換為訪問(wèn)器屬性即響應(yīng)式
function defineReactive (data, key, val) {
// 遞歸觀測(cè)子屬性
observer(val)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
return val
},
set: function (newVal) {
if(val === newVal){
return
}
// 對(duì)新值進(jìn)行觀測(cè)
observer(newVal)
}
})
}
// observer 方法首先判斷data是不是純JavaScript對(duì)象,如果是,調(diào)用 Observer 類進(jìn)行觀測(cè)
function observer (data) {
if(Object.prototype.toString.call(data) !== '[object Object]') {
return
}
new Observer(data)
}
這段代碼的意思就是給響應(yīng)式屬性設(shè)置值,我加上了注釋,應(yīng)該容易看明白,不過(guò)重點(diǎn)在這一步:Object.defineProperty!請(qǐng)接著往下看:
終極奧義?
上面的代碼在調(diào)用Object.defineProperty方法時(shí),就會(huì)把對(duì)象類型的鍵值轉(zhuǎn)換為'[object Object]'類型的字符串,最后給數(shù)組加了一個(gè)'[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"]
輸出的結(jié)果非常奇葩!直觀看起來(lái),[object Object]: "jack"似乎也是一個(gè)值,但是仔細(xì)一想又不是,因?yàn)樗皇且粋€(gè)對(duì)象,你把[1, 2, s: 2, [object Object]: "jack"]輸入控制臺(tái)是會(huì)報(bào)錯(cuò)的,但是Person['[object Object]']又可以取到值jack,然后你再看一下Person的length,你會(huì)發(fā)現(xiàn),長(zhǎng)度卻是2!我把Person展開(kāi)來(lái)看,發(fā)現(xiàn)是這樣的:

這樣看應(yīng)該比較明白了,你可以把Person理解為特殊的對(duì)象,數(shù)組本身的索引值也是這個(gè)“對(duì)象”的鍵值,"s"也是鍵值,[object Object]也是鍵值。為了謹(jǐn)慎起見(jiàn),我又打印了它的鍵值:
Object.keys(Person) // ["0", "1", "s"]
看到這結(jié)果,是不是又被驚訝到了?剛剛明明似乎看到了[object Object]這個(gè)鍵值,結(jié)果在這里卻沒(méi)有被打印出來(lái)!?好吧,神奇的js。。。我猜也許是因?yàn)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(60, 112, 198);">[object Object]這個(gè)鍵值的特殊性吧,而且你用for in 去遍歷Person的屬性,也是遍歷不到[object Object]的,也就是說(shuō)這個(gè)屬性不是可枚舉的。
[object Object]
最后的最后還是要說(shuō)一下[object Object],它是怎么來(lái)的呢?其實(shí)它是通過(guò)toString[5]方法得到的,mdn上對(duì)它是有說(shuō)明的,就是在默認(rèn)情況下任何對(duì)象調(diào)用toString()都會(huì)返回返回 "[object type]"。可以看下面的代碼:
var s = new Object();
s.toString() // [object Object]
但是我們知道數(shù)組也有toString方法,它覆蓋了默認(rèn)的toString方法,所以并不會(huì)輸出"[object Array]",如果要輸出這個(gè)結(jié)果,可以這樣寫:
var a = new Array();
toString.call(a)
回到上一步,也就是說(shuō)Object.defineProperty實(shí)際上是把它的第二個(gè)參數(shù)強(qiáng)制toSring了,所以在文章最開(kāi)始的地方,我們執(zhí)行這句this.$set(array, object, ())會(huì)得到一個(gè)包含[object Object]屬性的數(shù)組。
要不要提issue?
好了,本文有點(diǎn)冷門,本來(lái)只是懷疑找到了vue的一個(gè)bug,結(jié)果搞出了這么多瓜來(lái),把我吃撐了都!但是話說(shuō)回來(lái),如果api規(guī)定了參數(shù)及類型,入?yún)麇e(cuò)了,即使執(zhí)行不報(bào)錯(cuò),從嚴(yán)格上來(lái)講也要警告才對(duì),所以,要不要提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
掃碼關(guān)注 字節(jié)逆旅 公眾號(hào),為您奉獻(xiàn)更多技術(shù)干貨!
