vue中遇到的坑 --- 變化檢測問題(數組相關)


  最近在項目中遇到了一個問題,不知道為什么,所以最后通過動手做demo實踐、查文檔的方式解決了,這里做一個總結。

  

例1

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>vue</title>
  <script src="https://unpkg.com/vue@2.3.3/dist/vue.js"></script>
  <style>
    li:hover {
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div class="wrap">
    <ul>
      <li v-for="item,index in items" v-on:click="handle(index)">
        <span>{{item.name}}</span>
        <span>{{numbers[index]}}</span>
      </li>
    </ul>
  </div>
  <script>
    var vm = new Vue({
      el: ".wrap",
      data: {
        numbers: [],
        items: [
          {name: 'jjj'},
          {name: 'kkk'},
          {name: 'lll'},
        ]
      },
      methods: {
        handle: function (index) {
          // WHY: 更新數據,view層未渲染,但通過console這個數組可以發現數據確實更新了
           if (typeof(this.numbers[index]) === "undefined" ) {
             this.numbers[index] = 1;
           } else {
             this.numbers[index]++;
           }
        }
      }
    });
  </script>
</body>
</html>

這里的實現目的很明確 --- 我希望在點擊li時先檢測是否存在,當然是不存在的,所以就將值設置為1, 如果再次點擊,就讓數字累加。

但是出現的問題是: 點擊之后數字並沒有在view層更新,而通過console打印發現數據確實更新了,只是view層沒有及時的檢測到, 而我一直以來的想法就是: 既然vue實現的時數據雙向綁定,那么在model層發生了變化之后為什么就沒有在view層更新呢?  

首先,我就考慮了這是不是數組的問題,於是,我測試了下面的例子:

 

 

例2

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>vue</title>
  <script src="https://unpkg.com/vue@2.3.3/dist/vue.js"></script>
  <style>
    li:hover {
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div class="wrap">
    <ul>
      <li v-for="item,index in items" v-on:click="handle(index)">
        <span>{{item.name}}</span>
        <span>{{numbers[index]}}</span>
      </li>
    </ul>
  </div>
  <script>
    var vm = new Vue({
      el: ".wrap",
      data: {
        numbers: [],
        items: [
          {name: 'jjj'},
          {name: 'kkk'},
          {name: 'lll'},
        ]
      },
      methods: {
        handle: function (index) {
          // 不是數組,這里更新數據就可以直接在view層渲染
          this.items[index].name += " success";
        }
      }
    });
  </script>
</body>
</html>

這時,我再測試時就發現,這里的model層發生了變化時,view層就能及時、有效的得到更新。

而數組為什么不可以呢?  

 

 

於是在文檔上的一個不起眼的地方找到了下面的說明:

其中最重要的一句話就是 --- 如果對象是響應式的,確保屬性被創建后也是響應式的,同時觸發視圖更新,這個方法主要用於避開Vue不能檢測到屬性被添加的限制。

 

那么什么情況下Vue是不能檢測到屬性被添加呢?  根據參考鏈接,我們在文檔上看到了很好的說明 --- 深入響應式原理

 

首先,我們要了解Vue是如何實現數據的雙向綁定的!  

  把一個普通 JavaScript 對象傳給 Vue 實例的 data 選項,Vue 將遍歷此對象所有的屬性,並使用 Object.defineProperty 把這些屬性全部轉為 getter/setter。Object.defineProperty 是僅 ES5 支持,且無法 shim 的特性,這也就是為什么 Vue 不支持 IE8 以及更低版本瀏覽器的原因。

知識補充:

  訪問器屬性不包含數據值,他們包含一對getter函數和setter函數(這兩個函數不是必須的)。在讀取訪問器屬性時,會調用getter函數,這個函數負責返回有效的值;在寫入訪問器屬性是,會調用setter函數並傳入新值,這個函數負責決定如何處理數據。

  訪問器屬性不能直接定義,必須是用Object.defineProperty()來定義。

  下面是一個例子:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>vue</title>
</head>
<body>
  <script>
    var book={
        _year:2004,
        edition:1
    };
    Object.defineProperty(book,"year",{
        get:function(){
            return this._year;
        },
        set:function(newValue){
            if(newValue>2004){
                this._year=newValue;
                this.edition+=newValue-2004;
            }
        }
    });
    console.log(book.year); // 2004  在讀取訪問器屬性時會調用get函數
    book.year=2005;  // 在給訪問器屬性賦值時會調用set函數
    console.log(book.edition); // 2
  </script>
</body>
</html>

  這個例子應該可以很好的理解訪問器屬性了。

 所以,當對象下的訪問器屬性值發生了改變之后(vue會將屬性都轉化為訪問器屬性,之前提到了), 那么就會調用set函數,這時vue就可以通過這個set函數來追蹤變化,調用相關函數來實現view視圖的更新。

 每個組件實例都有相應的 watcher 實例對象,它會在組件渲染的過程中把屬性記錄為依賴,之后當依賴項的 setter 被調用時,會通知 watcher 重新計算,從而致使它關聯的組件得以更新

  即在渲染的過程中就會調用對象屬性的getter函數,然后getter函數通知wather對象將之聲明為依賴,依賴之后,如果對象屬性發生了變化,那么就會調用settter函數來通知watcher,watcher就會在重新渲染組件,以此來完成更新。

 

OK!既然知道了原理,我們就可以進一步了解為什么出現了之前數組的問題了!

 

變化檢測問題

  收到現代JavaScript瀏覽器的限制,其實主要是 Object.observe() 方法支持的不好,Vue不能檢測到對象的添加或者刪除。然而Vue在初始化實例時就對屬性執行了setter/getter轉化過程,所以屬性必須開始就在對象上,這樣才能讓Vue轉化它。 

  所以對於前面的例子就不能理解了 --- 數組中index都可以看做是屬性,當我們添加屬性並賦值時,Vue並不能檢測到對象中屬性的添加或者刪除,但是其的確是添加或刪除了,故我們可以通過console看到變化,所以就沒有辦法做到響應式; 而在第二個例子中,我們是在已有的屬性的基礎上進行修改的,這些屬性是在最開始就被Vue初始化實例時執行了setter/getter的轉化過程,所以說他們的修改是有效的,model的數據可以實時的在view層中得到相應。

補充知識: 什么是 Object.observe() ?

  在介紹之前,不得不殘忍的說,盡管這個方法可以在某些瀏覽器上運行,但事實是這個方法已經廢棄

  概述此方法用於異步地監視一個對象的修改。當對象的屬性被修改時,方法的回調函數會提供一個有序的修改流,然而這個接口已經從各大瀏覽器移除,可以使用通用的 proxy 對象      

    方法: 

Object.observe(obj, callback[, acceptList])

  其中obj就是被監控的對象, callback是一個回調函數,其中的參數包括changes和acceptList,

  changes一個數組,其中包含的每一個對象代表一個修改行為。每個修改行為的對象包含: 

  • name: 被修改的屬性名稱。
  • object: 修改后該對象的值。
  • type: 表示對該對象做了何種類型的修改,可能的值為"add""update", or "delete"
  • oldValue: 對象修改前的值。該值只在"update""delete"有效。 

  acceptList在給定對象上給定回調中要監視的變化類型列表。如果省略, ["add", "update", "delete", "reconfigure", "setPrototype", "preventExtensions"] 將會被使用。

var obj = {
  foo: 0,
  bar: 1
};

Object.observe(obj, function(changes) {
  console.log(changes);
});

obj.baz = 2;
// [{name: 'baz', object: <obj>, type: 'add'}]

obj.foo = 'hello';
// [{name: 'foo', object: <obj>, type: 'update', oldValue: 0}]

delete obj.baz;
// [{name: 'baz', object: <obj>, type: 'delete', oldValue: 2}]

 

  如上所示: 但是chrome也是不支持的,瀏覽器的兼容性如下:

 

參考文檔: Object.ovserve()

推薦閱讀文章: Object.observe() 引爆數據綁定革命

 

 

解決方法

  使用 Vue.set(object, key, value) 方法將響應屬性添加到嵌套的對象上。 還可以使用 vm.$set 實例方法,這也是全局 Vue.set 方法的別名。

例3

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>vue</title>
  <script src="https://unpkg.com/vue@2.3.3/dist/vue.js"></script>
  <style>
    li:hover {
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div class="wrap">
    <ul>
      <li v-for="item,index in items" v-on:click="handle(index)">
        <span>{{item.name}}</span>
        <span>{{numbers[index]}}</span>
      </li>
    </ul>
  </div>
  <script>
    var vm = new Vue({
      el: ".wrap",
      data: {
        numbers: [],
        items: [
          {name: 'jjj'},
          {name: 'kkk'},
          {name: 'lll'},
        ]
      },
      methods: {
        handle: function (index) {
          // WHY: 更新數據,view層未渲染,但通過console這個數組可以發現數據確實更新了
           if (typeof(this.numbers[index]) === "undefined" ) {
             this.$set(this.numbers, index, 1);
           } else {
             this.$set(this.numbers, index, ++this.numbers[index]);
           }
        }
      }
    });
  </script>
</body>
</html>

 

這樣,我們就可以實現最終的目的了!

 

 

 

第二部分

  上面一部分是指在data下的數組,而如果是在store中的數組,一般可以這樣:

[ADD_ONE] (state, index) {
      if ( typeof state.numbers[index] == "undefined") {
        Vue.set(state.numbers, index, 1)
      } else {
        Vue.set(state.numbers, index, ++state.numbers[index])
      }
    }

 即使用 Vue.set() 的方式來改變、增加。

   注意:這里是確定index的增加和減少,所以用 Vue.set() 的方式

  減的方式如下:

  [REMOVE_ONE] (state, index) {
      Vue.set(state.numbers, index, --state.numbers[index]);
    }

 

 

 

第四部分

 

   如果是在store的actions中我們需要對stroe中的數組進行填充,方法如下:

state內容:

    kindnames: []

 

 

Mutations內容:

    [ADD_KIND_NAME] (state, name) {
      state.kindnames.push(name);
    } 

 

注意: 這里直接使用push的方式

當然,除了push,我們還可以shift等各種方式。

 

actions的內容:

commit(ADD_KIND_NAME, state.items[index++].name);

 

 

 這里,state.items[index++].name獲取到的是一個一個的字符串。

 

最后,看到數據如下:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

注:同樣可以參考文檔 --- 細節與最佳實踐

 

不驕不躁,努力前行。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM