最近在用Vue開發一個後臺管理的demo,有一個很是常規的需求。然而這個常規的需求中,包含了大量的知識點。有一個產品表格,用來顯示不一樣產品的信息。而後表格要有一個內嵌編輯的功能,點擊操做欄的編輯按鈕,對應行的信息列就變成輸入框。初版的代碼大體上像這樣。javascript
<template> <!-- ... --> <el-input v-show="scope.row.edit" size="small" v-model="scope.row.description"></el-input> <span v-show="!scope.row.edit">{{scope.row.description}}</span> <!-- ... --> <el-button @click="scope.row.edit = !scope.row.edit" type="text" size="small">編輯</el-button> <!-- ... --> </template> <script lang="ts"> import { Vue, Component} from "vue-property-decorator"; @Component export default class Project extends Vue { products = []; mounted() { this.$store.dispatch('GET_PRODUCTS').then(() => { this.products = this.$store.getters.products this.products.map((item: any) => { item.edit = false; return item; }); }); } } </script>
邏輯很簡單,我在表格數據數組中,給每個對象都加入一個初始值爲false
的屬性"edit"
,而後根據這個屬性的值,使用v-show
來決定渲染的是文本仍是輸入框,是「編輯」仍是「保存」。
然而運行起來以後的表現並非像我想的同樣,事實上,點擊編輯按鈕後,對應產品的「產品描述」並無變成輸入框,編輯按鈕也沒有變成保存按鈕。而我經過vue-devtool查看數據發現,事實上對應的edit
屬性確實已經變了,只是頁面上的組件沒有正確渲染。這讓我很困惑,說好的雙向綁定呢,爲何model層上的變化沒有響應到view層上呢。html
首先,因爲頁面初始顯示是正確的,把edit
的初始值改爲true
後,也會有輸入框出現,因此確定不是代碼邏輯的問題。當我試着把v-show
的判斷條件改爲數組中的對象本來就有的屬性時,發現編輯狀態的切換忽然變得正常了。而一旦我把判斷條件改回後來插入的edit
時,一切又變得不正常了。所以我推測,必定是數據綁定出了什麼問題。
我在網上查了一下,有些相似的問題,大多數的解決方案是,給el-table
加上一個隨機數key值:key="Math.random()"
。試了一下,發現真的有用。之因此有用是由於,每次對這個表格有操做,key值都會變,這就至關於產生了一個新的table,瀏覽器就會根據model層的數據從新渲染,這時候顯示固然就正確了。但可想而知,這樣也會形成極大的性能浪費,並且這也沒有解決數據綁定的問題。
我又試着對代碼作了一些修改。我把map和賦值操做放到了同一句裏面去,代碼變成了這樣vue
this.$store.dispatch(GET_PRODUCTS).then(() => { this.products = this.$store.getters.products.map((item: any) => { item.edit = false; return item; }); });
神奇的事發生了,竟然一切都恢復正常了。那麼我就知道了,問題出在了數組和map函數上。java
爲了探究這一切的緣由,我再次點開了Vue的官網。在官網很下面的位置,找到了關於響應式原理的說明。這張圖很好地說明了Vue實現雙向綁定的原理。
當一個javscript對象傳入Vue實例的data中時,Vue會遍歷該對象的全部屬性,同時使用 Object.defineProperty
方法將這些屬性全都轉成 getter/setter每一個組件實例都對應一個watcher實例,它會在組件渲染的過程當中把「接觸」過的數據屬性記錄爲依賴。以後當依賴項的數據發生變化,也就是setter觸發時,會通知watcher,從而使它關聯的組件從新渲染。
而因爲javascript的限制,Vue不能檢測到對象的添加或者刪除。而且Vue在初始化實例時就對屬性執行了setter/getter轉化過程,因此屬性必須開始就在對象上,這樣才能讓Vue轉化它。而動態添加的根級別的屬性,則不會轉化成響應式的屬性。也就是說,往已經建立的實例上添加的根級別的屬性,都會是非響應式的。可是,可使用 Vue.set(object, propertyName, value)
或者vm.$set(object, propertyName, value)
方法向嵌套對象添加響應式屬性。
這裏,數組相關的注意事項被額外提了出來。因爲 JavaScript 的限制,Vue 不能檢測如下數組的變更:react
vm.items[indexOfItem] = newValue
vm.items.length = newLength
解決方法也很簡單,使用上面提到的set方法就能夠解決這個問題。與此同時,官網上還有一段專門針對數組的變異方法的說明。
所謂的變異方法,顧名思義,會改變調用了這些方法的原始數組。相比之下,也有非變異 (non-mutating method) 方法,例如 filter()、concat() 和 slice() 。它們不會改變原始數組,而老是返回一個新數組。當使用非變異方法時,能夠用新數組替換舊數組。而且,Vue還很是智能的會對於沒有變化的dom進行重用,並不會整個進行更新。
看到這兒,我終於找到問題的關鍵在哪兒了。其實網上的各類說法都不許確,真正出問題的點在於map函數的使用上。map是一個非變異方法,方法自己並不會改變原數組,而是會返回一個新數組。所以,Vue並無對map方法進行包裝,而是建議替換原數組。然而我在用的時候並無注意到這一點,在使用的時候利用指針特性,把map方法當作變異方法來用,直接改變原數組,這天然就不會被Vue檢測到了。所以,新添加到數組中的對象中的edit屬性,就成了非響應式的屬性了,改變它天然不會讓組件從新渲染。數組
原理都已經搞清楚了,接下來我總結了一下這類數組問題的幾種解決方法。瀏覽器
在el-table標籤上添加:key="Math.random()"
,無論作了什麼,都強制刷新整個表格,很是不推薦,極大的性能消耗。app
在使用數組方法的時候,分清變異方法和非變異方法,用非變異方法的時候,要用新數組替代舊數組,而不是直接變換原數組。dom
我在'vue/src/core/observer/index.js'中找到了set方法的源碼。咱們發現set函數接收三個參數分別爲 target、key、val,其中target的值爲數組或者對象,這正好和官網給出的調用Vue.set()方法時傳入的參數參數對應上。而後往下看實現,我基本上給每一行都加上了註釋。ide
export function set (target: Array<any> | Object, key: any, val: any): any { if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) {//判斷target的類型是否符合要求,若不符合要求,且不在生產環境下,就拋出警告。 warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`) } if (Array.isArray(target) && isValidArrayIndex(key)) {//若是target是數組,且key值合法 target.length = Math.max(target.length, key) target.splice(key, 1, val)//用包裝好的變異方法splice進行賦值。 return val } if (key in target && !(key in Object.prototype)) {//若是key是target中原有的屬性,就直接賦值。 target[key] = val return val } const ob = (target: any).__ob__//響應式屬性的observer對象,有這個對象就表明是響應式的。 if (target._isVue || (ob && ob.vmCount)) {//若是當前的target對象是vue實例對象或者是根數據對象,就拋出警告。 process.env.NODE_ENV !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ) return val } if (!ob) {//若是不存在observer,那就不是響應式對象,直接賦值。 target[key] = val return val } defineReactive(ob.value, key, val)//給新屬性添加依賴,之後直接修改屬性就能從新渲染。 ob.dep.notify()//直接觸發依賴。 return val }
能夠看到,set方法對於數組的處理其實很是簡單,就是調用了包裝好的splice方法。那麼再來看一下包裝Array變異方法的代碼實現,我一樣給每一行加上了註釋。其實作的事情也很少,主要就是給每一個新添加的元素都加上觀察者。
... methodsToPatch.forEach(function (method) { // cache original method const original = arrayProto[method]//保存原方法。 def(arrayMethods, method, function mutator (...args) {//修改方法映射,調用數組方法的時候實際上調用的是對應的mutator方法。 const result = original.apply(this, args)//調用原方法,先把結果求出來 const ob = this.__ob__//獲取observer let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break }//對於往數組中加元素的方法,得到添加的元素。 if (inserted) ob.observeArray(inserted)//給添加的元素添加觀察者。 // notify change ob.dep.notify()//觸發依賴。 return result }) })