Vue 添加響應式屬性的正確姿勢

原文鏈接: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 都已被劫持。

clipboard.png

應對方法是什麼

最簡單的方法是:直接在 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"
/>

進行兩種操作:

  1. 先在 with v-model 框輸入,後在 with @input 框輸入
  2. 先在 with @input 框輸入,後在 with v-model 框輸入

操作一,一切正常;操作二,無法更新。這就證明坊間流傳的 v-model 是 @input 和 :value 的語法糖這個說法至少放在現在肯定是錯的(其實我往下試了幾個版本,這兩個操作表現都是不一致的,覺得很迷惑,但是這不是重點,先不糾結了)。

那麼 v-model 到底做了什麼

在 stackoverflow 上經過大佬指點,上面的情況其實很容易理解,造成這個區別的重點有兩個:

  1. v-model 處理對象屬性會自動觸發 set -> 相關源碼
  2. set 對已存在的屬性並不會再次讓他變爲“響應式” -> 相關源碼

所以對於操作一,v-model 幫你把數據 set 了,自然一切正常;操作二,@input 先把屬性直接靜態添加了,到了 v-model 的時候 set 不會再劫持已經存在的屬性。

這就引出了一個需要注意的地方,若是先直接賦值,即使再用 set 也不能再劫持這個屬性了,這個可憐弱小又的屬性已經無法再變成響應式了。

參考鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章