前言
相信大家都知道,在vue2.0x中,使用陣列下標改變值時,是不會觸發回應式的
以下來自:Vue官方檔案
Vue 不能檢測以下陣列的變動:
- 當你利用索引直接設定一個陣列項時,例如:vm.items[indexOfItem] = newValue
- 當你修改陣列的長度時,例如:vm.items.length = newLength
但是其實還是有特殊情況的,讓我們來分析分析
正常情況
讓我們看看,使用陣列下標直接改變陣列元素的值,是否會回應式變化(按照官方檔案是不可行的)
<div id="app">
<ul>
<li v-for="item in list">
{{item}}
</li>
</ul>
</div>
const app = new Vue({
el: '#app',
data: {
list: ['4', '5', '6']
},
mounted () {
this.list[1] = '6'
console.log(this.list)
}
})
這里通過this.list[1],也就是陣列下標改變陣列值時,讓我們看看是否會回應式變化
下面為頁面中的列印結果


通過列印結果,可以看出,this.list[1]是成功改變了data中list陣列下標為1的值的,因為列印出的list的索引為1的值為"6"了
但是在頁面中,并沒有將這個6渲染出來,所以可以得出這一個賦值不是回應式的,并不會讓界面跟著渲染
所以可以得出結論
通過陣列下標改變元素值時,是不會回應式變化的
誤區
而這個時候,會有些人走入一個誤區
這個誤區,可以看看代碼
<div id="app">
<ul>
<li v-for="item in objectList">
{{item}}
</li>
</ul>
</div>
const app = new Vue({
el: '#app',
data: {
objectList: [
{ value: 1, id: 1 },
{ value: 2, id: 2 },
{ value: 3, id: 3 },
]
},
mounted () {
this.objectList[1].value = 3
}
})
列印結果如下

這里就是大多數人的誤區:你看!我用的也是陣列下標,為什么這樣改變的就會是回應式的呢?
其實這就是被官方檔案繞進去了,看到陣列下標就以為不是回應式,其實這里的陣列下標只是獲取到那個物件而已,而這個物件卻是回應式的,所以你改變這個物件的值,當然也就回應式的變化了
那么進入誤區的人,現在又進入了一個難以理解的點,為什么陣列的物件元素就是回應式的呢?
這時候,可以從原始碼中來看
陣列中物件元素的回應式
說到回應式,之前也分析過,從initData中執行observe函式,那么從observe開始看
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 判斷是否為物件 判斷是否為VNode
if (!isObject(value) || value instanceof VNode) {
// 如果不是物件 或者 是實體化的Vnode 也就是vdom
return
}
// 觀察者 創建一個ob
let ob: Observer | void
// 檢測是否有快取ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// 直接將快取的ob拿到
ob = value.__ob__
} else if (
// 如果沒有快取的ob
shouldObserve && // 當前狀態是否能添加觀察者
!isServerRendering() && // 不是ssr
(Array.isArray(value) || isPlainObject(value)) && // 是物件或陣列
Object.isExtensible(value) && // 是否可以在它上面添加新的屬性
!value._isVue // 是否是Vue實體
) {
// new 一個Observer實體 復制給ob
// 也是把value進行回應化,并回傳一個ob實體,還添加了__ob__屬性
ob = new Observer(value)
}
// 如果作為根data 并且當前ob已有值
if (asRootData && ob) {
// ++
ob.vmCount++
}
// 最后回傳ob,也就是一個Obesrver實體 有這個實體就有__ob__,然后其物件和陣列都進行了回應化
return ob
}
剛開始初始化時,內部肯定是沒有ob這個屬性的,所以會執行new Observer
因此繼續看class Observer的建構式
constructor (value: any) {
this.value = value
// 這里會new一個Dep實體
this.dep = new Dep()
this.vmCount = 0
// def添加__ob__屬性,value必須是物件
def(value, '__ob__', this)
// 判斷當前value是不是陣列
if (Array.isArray(value)) {
// 如果是陣列
// 檢測當前瀏覽器中有沒有Array.prototype
// 當能使用__proto__時
// 這里完成了陣列的回應式,不使用這7個方法都不會觸發回應式
if (hasProto) {
// 有原型時 將arrayMethods覆寫value.__proto__,也就是把增加了副作用的7個陣列方法放了進來
protoAugment(value, arrayMethods)
} else {
// 復制增加了副作用的7個陣列方法
copyAugment(value, arrayMethods, arrayKeys)
}
// 遍歷將陣列所有元素進行observe
this.observeArray(value)
} else {
// 不是陣列是物件,執行這里
// walk就是給物件的所有key進行回應化
this.walk(value)
}
}
分析如圖

可以看到,如果是陣列,會首先對7個陣列方法進行添加副作用,然后執行observeArray函式
所以繼續看observeArray這個函式
// 遍歷將陣列所有元素進行observe
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
這段代碼大家應該都能一下就知道在干啥,遍歷陣列,對所有陣列元素執行observe函式
那么繼續看observe函式
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 判斷是否為物件 判斷是否為VNode
if (!isObject(value) || value instanceof VNode) {
// 如果不是物件 或者 是實體化的Vnode 也就是vdom
return
}
// 觀察者 創建一個ob
let ob: Observer | void
// 檢測是否有快取ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// 直接將快取的ob拿到
ob = value.__ob__
} else if (
// 如果沒有快取的ob
shouldObserve && // 當前狀態是否能添加觀察者
!isServerRendering() && // 不是ssr
(Array.isArray(value) || isPlainObject(value)) && // 是物件或陣列
Object.isExtensible(value) && // 是否可以在它上面添加新的屬性
!value._isVue // 是否是Vue實體
) {
// new 一個Observer實體 復制給ob
// 也是把value進行回應化,并回傳一個ob實體,還添加了__ob__屬性
ob = new Observer(value)
}
// 如果作為根data 并且當前ob已有值
if (asRootData && ob) {
// ++
ob.vmCount++
}
// 最后回傳ob,也就是一個Obesrver實體 有這個實體就有__ob__,然后其物件和陣列都進行了回應化
return ob
}
分析如圖

可以看到,observe進行了一次處理,不是物件的他就不會執行observe去添加ob屬性
如果是物件的話,就會執行new Observer,這時候又回到之前的那個建構式,只不過這次執行的下面的else分支,這次不是陣列了,所以會執行this.walk
如圖

其實walk就是給物件所有的key進行defineReactive添加資料劫持(這里不細說,反正這樣已經完成回應式了,不懂得可以看這篇)
所以陣列中物件元素都是回應式的,因此下次再碰到這種情況,就不要再進入誤區了,這里并沒有通過陣列下標去改變值,而是獲取相應的物件,而這個物件是回應式的!
又一個誤區
還有一個誤區,也讓很多人認為通過陣列下標改變值會是回應式,直接看代碼
<div id="app">
<ul>
<li v-for="item in objectList">
{{item.value}}
</li>
</ul>
<ul>
<li v-for="item in list">
{{item}}
</li>
</ul>
</div>
const app = new Vue({
el: '#app',
data: {
objectList: [
{ value: 1, id: 1 },
{ value: 2, id: 2 },
{ value: 3, id: 3 },
],
list: ['4', '5', '6']
},
mounted () {
this.list[1] = '6'
this.objectList[1].value = 3
}
})
這里一個通過陣列下標改變了一個值,又通過陣列下標獲取一個物件并改變物件的值
由之前得出的結論,第一個值的改變是不會回應式變化的,第二個值的改變會回應式變化
所以頁面上應該顯示1,3,3, 4, 5,6
但是列印結果如下

阿這,跟我們預想的不一樣啊,為什么通過陣列下標的this.list[1] = '6’回應式變化了呢?
這就是又一個誤區
其實這里我們可以在this.list[1]='6’后,列印一下this.list
mounted () {
this.list[1] = '6'
console.log(this.list)
this.objectList[1].value = 3
}

可以看到,這時的list值是成功改變的了,只是沒有回應式的渲染在頁面上
所以繼續執行后面的this.objectList[1].value = 3時,這是一個回應化的操作,因此當值改變時,會觸發setter中的dep.notify,去通知視圖更新,經過一系列vdom,patch后,vue會發現data中有個list陣列中一個元素值也改變了,因此也會將當前改變的值和list陣列中改變的那個值都給重新渲染了
因此這里的陣列下標改變值的回應化其實是后一句執行 this.objectList[1].value = 3,這一句通知視圖更新時,會檢測到前一個list陣列中值有變化,但是視圖中沒更新,因此才會一起渲染
實作陣列下標改變值的回應式
其實,vue2.0x是可以實作陣列下標改變值的回應式的,且非常簡單
前面我們也知道,通過陣列下標改變值,能成功改變data中的值,但是因為沒有監聽,因此不會觸發回應式更新
那么我們可以通過添加屬性監聽完成這一操作,眾所周知,陣列索引也是陣列的一個屬性,因此讓我們重寫一下對陣列的操作
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// this.observeArray(value)
this.walkArray(value)
} else {
this.walk(value)
}
}
walkArray (obj: Object) {
obj.forEach((item, index) => {
defineReactive(obj, index)
})
}
我把原本的observeArray注釋了,換成了自己寫的walkArray,而walkArray就是實作了對索引的監聽,
這時候就已經實作成功了,看看效果
<div id="app">
<ul>
<li v-for="item in list">
{{item}}
</li>
</ul>
</div>
const app = new Vue({
el: "#app",
data: {
list: ['1', '2', '3']
},
mounted (){
this.list[1] = 4
}
})
來看看這次,通過陣列下標改變值會不會回應式變化
列印如下圖

好的,成功實作了!
疑問
這時肯定都會有個疑問,如果這么簡單就能實作這個陣列下標的回應式的話,為什么尤大不寫進去呢?
這里尤大有回答過:為什么vue沒有提供對陣列屬性的監聽?


既然尤大都說了性能問題,那不寫入陣列下標回應式肯定是他們經過考量之后的決定了
所以現在只要知道怎么實作以及避免這兩個誤區即可
總結
這也是在群里有人問了之后,我也踩進了誤區,然后經過看原始碼后,算是對其有了清晰的認識了
所以作為一篇隨筆寫在這分享給大家,也算是避雷吧哈哈哈
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/7600.html
標籤:AI
下一篇:Canvas悟空推箱子
