前言
在負責的後臺管理系統中,我的新增人員與編輯人員兩個功能共用了一個元件,但是遇見一個問題.同樣是用v-for去渲染一些標籤,在使用編輯人員功能時,刪除物件陣列元素,對應的標籤在頁面上也會消失.但是在使用新增人員功能時,刪除物件陣列元素,對應的標籤卻不會在頁面上消失.
個人問題遺留:
this.$set(this.numbers,index,++this.numbers[index]);
this.$set(this.numbers,index,1);
貌似不必每次都要用this.$set去接收改變的資料,覺得掛一次就可以啦吧?
複製程式碼
Object.defineProperty()和響應式原理 還是需要細細研究一下,還有Object.defineProperty()的資料屬性和瀏覽器屬性
複製程式碼
問題描述:
- 首先進入新增人員頁面
- 點選新增部門
- 點選確定,用v-for渲染去push到的陣列資料
- 然後就完犢子啦!點選X號,陣列元素減少啦,但是頁面上的部門一個都不減少.因為我在data中只是定義了person為空物件,在確認事件觸發時用了this.person.deparmentList = deparmentListArray去接收了部門選擇頁面的陣列資料,this.object.deparmentList只是個響應式物件的普通屬性,根本不是響應式的,也就不會被vue檢測到,檢視也未能更新.應該用this.$set(this.person, deparmentList, deparmentListArray);我果然是個渣!只會喊資料驅動檢視.
demo1:物件陣列
<!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]++;
}
console.log(this.numbers);
}
}
});
</script>
</body>
</html>
複製程式碼
這裡的實現目的很明確 --- 我希望在點選li時先檢測是否存在,當然是不存在的,所以就將值設定為1, 如果再次點選,就讓數字累加。但是出現的問題是: 點選之後數字並沒有在view層更新,而通過console列印發現資料確實更新了,只是view層沒有及時的檢測到, 而我一直以來的想法就是: 既然vue實現的時資料雙向繫結,那麼在model層發生了變化之後為什麼就沒有在view層更新呢?
首先,我就考慮了這是不是陣列的問題,於是,我測試了下面的例子:
demo2: 非物件陣列
<!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例項的根資料物件:data是Vue 例項的根資料物件,Vue 將會遞迴將 data 的屬性轉換為 getter/setter,從而讓 data的屬效能夠響應資料變化。推薦在建立例項之前,就宣告所有的根級響應式屬性.(那我在data中掛載一個物件,也要把這個物件的屬性整一個初始值嘍?此處有待實踐論證!)
首先,我們要了解Vue是如何實現資料的雙向繫結的!
把一個普通 JavaScript 物件傳給 Vue 例項的 data 選項,Vue 將遍歷此物件所有的屬性,並使用 Object.defineProperty 把這些屬性全部轉為getter/setter。Object.defineProperty 是僅 ES5 支援,且無法 shim 的特性,這也就是為什麼Vue 不支援 IE8以及更低版本瀏覽器的原因。
訪問器屬性不能直接定義,必須是用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}]
複製程式碼
參考文件:Object.ovserve()
推薦閱讀文章:Object.observe() 引爆資料繫結革命
解決方法
使用 Vue.set(object, key, value) 方法將響應屬性新增到巢狀的物件上。 還可以使用 vm.$set 例項方法,這也是全域性 Vue.set 方法的別名。
解決程式碼示例:
<!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() 的方式
如果是在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獲取到的是一個一個的字串。
注:同樣可以參考文件 --- 細節與最佳實踐