原文鏈接:https://ssshooter.com/2019-09...
默認此文讀者明白簡單的 Vue 底層原理,對此陌生的讀者可以先看:
此文使用的 Vue 版本是 2.0+,在線例子看這裏,下面順便也把關鍵代碼貼出來。
<template>
<div class="hello">
<button @click="inputvalue.aaaa = 'aaaa is here'">show aaaa</button>
<button @click="$forceUpdate()">forceupdate</button> {{inputvalue.aaaa}}
<br />
cccc {{inputvalue.cccc}}
<input v-model="inputvalue.cccc" placeholder="with v-model" />
<input
@input="inputvalue.cccc = $event.target.value"
:value="inputvalue.cccc"
placeholder="with @input"
/>
<br />
bbbb {{inputvalue.bbbb}}
<input v-model="inputvalue.bbbb" placeholder="with v-model" />
<input
@input="inputvalue.bbbb = $event.target.value"
:value="inputvalue.bbbb"
placeholder="with @input"
/>
</div>
</template>
<script>
export default {
data() {
return {
inputvalue: {
bbbb: '',
},
}
},
}
</script>
提出問題
最近的項目大量接觸到動態新增的數據,覺得必須要搞清楚到底什麼時候 vue 會讓視圖更新,視圖修改數據又會不會反映到數據模型。
於是寫了簡單幾個例子作爲對比,結合一年前研究了一下但是現在忘得差不多的 Vue 原理知識,解決了這麼個問題 ——
什麼情況下動態添加對象屬性是安全操作(換句話說就是可以保證數據是響應式的)?
直接賦值爲什麼不可以
首先解釋例子中 inputvalue.aaaa
不顯示的問題。
這要從 Vue 的響應式原理說起。在初始化的時候 Vue 會把 data 的數據遞歸掃描一遍,設置 setter 和 getter。
getter 的作用是在數據被讀取時記下當前的調用者,這個調用者也就是這個數據的“訂閱者”。
若視圖使用了某個數據,處理頁面時就會調用該數據,成爲該數據的一個訂閱者。
setter 的作用是在數據被賦值時,會提醒他的訂閱者該數據已更新,然後訂閱者就知道要運行對應的更新操作,例如視圖更新、watch 函數。
設置 getter,setter 常被稱爲劫持,感覺也挺形象的,下面就簡單用劫持指代這個行爲。
既然在初始化時數據才被劫持,那麼你突然的定義 this.inputvalue.aaaa = 'aaaa is here'
顯然會讓 Vue 猝不及防。這個屬性即使有訂閱者,但是因爲沒有走到“劫持”這一步,所以這個屬性根本意識不到他有訂閱者。
其實把數據打印出來可以簡單地判定這個數據是否已經被劫持。如下圖,bb 沒有被劫持,aa、cc 都已被劫持。
應對方法是什麼
最簡單的方法是:直接在 data 寫清楚,也就是頁面用了什麼屬性都必須寫上。例如對於 inputvalue.aaaa
,就直接在 data 裏面加上 aaaa 屬性。
但是...想了想,這大概不算“動態”添加了吧 😂
使用 set
向響應式對象中添加一個屬性,並確保這個新屬性同樣是響應式的,且觸發視圖更新。它必須用於向響應式對象上添加新屬性,因爲 Vue 無法探測普通的新增屬性 (比如 this.myObject.newProperty = 'hi')
Vue.set
或者 Vue 實例的 $set
都是一樣的,總之就是手動觸發一次劫持,之後在更新的時候就能觸發視圖重新渲染啦!
不過,其實 set 在一種情況下會失效,這個後面會提到...
使用 forceUpdate
這個方法算是一種曲線救國吧。
如果你不需要雙向綁定,在動態新增屬性時你可以使用 $forceUpdate()
。這個函數的作用就如其名,強制更新重新渲染。
上面說過了,雖然你設置新數據沒有通知頁面重新渲染,不過數據終究是改了。所以你只需要強制更新視圖,就能看到數據修改後的效果。
Vue 單向綁定
<input
:value="myValue.property"
@input="myValue.property = $event.target.value"
/>
你可能從未聽過 Vue 單向綁定,但是這樣做也算是一個單向綁定了 😂
當你的輸入確實地改變了 myValue.property
的值,但是不會觸發任何關於 myValue.property
的更新。真的需要更新的時候 forceUpdate
就可以了。
數組呢
如果數組裏有對象,只要單個對象符合上面操作即可,沒有特別需要注意的地方。
但是老調重彈,數組更新方法還是需要注意,你可以通過整個數組重新賦值以及 push()、pop()、shift()、unshift()、splice()、sort()、reverse() 這幾個經過包裹的方法觸發更新。
奇葩情況
例子
對比上面 cccc
的兩個輸入框:
<input v-model="inputvalue.cccc" placeholder="with v-model" />
<input
@input="inputvalue.cccc = $event.target.value"
:value="inputvalue.cccc"
placeholder="with @input"
/>
進行兩種操作:
- 先在 with v-model 框輸入,後在 with @input 框輸入
- 先在 with @input 框輸入,後在 with v-model 框輸入
操作一,一切正常;操作二,無法更新。這就證明坊間流傳的 v-model 是 @input 和 :value 的語法糖這個說法至少放在現在肯定是錯的(其實我往下試了幾個版本,這兩個操作表現都是不一致的,覺得很迷惑,但是這不是重點,先不糾結了)。
那麼 v-model 到底做了什麼
在 stackoverflow 上經過大佬指點,上面的情況其實很容易理解,造成這個區別的重點有兩個:
所以對於操作一,v-model 幫你把數據 set 了,自然一切正常;操作二,@input 先把屬性直接靜態添加了,到了 v-model 的時候 set 不會再劫持已經存在的屬性。
這就引出了一個需要注意的地方,若是先直接賦值,即使再用 set 也不能再劫持這個屬性了,這個可憐弱小又的屬性已經無法再變成響應式了。