書名: Vue.js從入門到項目實戰作者名: 劉漢偉本章字數: 1871字更新時間: 2019-12-09 14:45:33
2.3 數據響應式原理
Vue中最重要的概念就是響應式數據,一方面指衍生數據和元數據之間的響應,通過數據鏈來實現;另一方面則是指視圖與數據之間的綁定。
本節將深入講解這兩方面的內容。
2.3.1 初識數據鏈
數據鏈在學術上被定義為連通數據的鏈路。在這條鏈路上有一到多個數據起點(元數據),并通過該點不斷衍生拓展新的節點(衍生數據),形成一個龐大的網狀結構。當你修改數據起點時,所有存在在網上的節點值都將同步更新,如圖2.12所示。

圖2.12 單一起點的數據鏈路
圖2.12是只有一個起點的數據鏈,結構比較簡單,當鏈路存在的數據起點越來越多時,結構會變得越來越復雜,如圖2.13所示。

圖2.13 多起點的數據鏈路
得益于數據鏈,在Vue中我們可以通過修改元數據的值來觸發一系列數據的更新。當然,我們在做數據結構的設計時,應該盡量降低數據鏈的復雜度。畢竟,代碼是給機器讀的,但也是給人看的。
2.3.2 函數式編程
在上一小節中,元數據a和b通過變量聲明即可實現:
let a = 3, b = 4
但是衍生數據應該怎樣實現從而保證其值只依賴于元數據而不允許被外界修改呢?這里先介紹一下函數式編程的概念。
函數式編程(Functional Programming)是一種結構化編程方式,力求將運算過程寫成一系列嵌套的函數調用。
源于JS中“萬物皆對象”的理念,函數式編程認定函數是第一等公民,可以賦值給其他變量、用作另一個函數的參數或者作為函數返回值來使用。
由于其作用是處理運算,因此函數體只能包含運算過程,而且必帶返回值(在實際開發中,不做I/O讀寫操作是不可能的,不過要把I/O限制到最小),標準格式如下:
let double = function (num) { return num * 2 }
函數式編程的核心是根據元數據生成新的衍生數據,提供唯一確定的輸入,函數將返回唯一確定的輸出,它并不會修改原有變量的值。這在運用JS閉包概念進行開發時尤為重要,在函數作用域內調用域外或全局的變量時并不會修改它們的值,安全無污染。
最后一點,使用函數式編程(加lambda表達式之后)可以使代碼看起來十分高大上,如以下代碼所示:
let x = (x => (x => x * 9)(x) + 3)(5) let y = y => (y => y * 9)(y) + 3 console.log(x) console.log(y(5))
這樣的編程方式可使代碼變得極其簡潔,但也極其難讀。上面只是一個基本示例,實際開發中能演化出無限復雜的結構,感興趣的同學可以推算一下示例的結果并運行驗證一下。
通過函數式編程,衍生數據也得以實現。實際上,函數式編程就是建立了一條數據流通的鏈路,開發者只需要關注輸入和輸出兩端的內容就可以,這是封裝復用的一種最佳實踐,在高效開發中舉足輕重。
2.3.3 Vue中的數據鏈
Vue實例提供了computed計算屬性選項,以供開發者生成衍生數據對象。雖然計算屬性以函數形式聲明,卻并不接受參數,也只能以屬性的方式調用。由于計算屬性的this指向Vue實例,所以它可以獲取實例上所有已掛載的可見屬性。下面來看一個示例:
<style> #app { font-family: Roboto, sans-serif; color: #363e4f; } .data-label { display: inline-block; width: 160px; } </style> <div id="app"> <p><strong class="data-label">A</strong><input type="text" v-model="a"></p> <p><strong class="data-label">B</strong><input type="text" v-model="b"></p> <p><strong class="data-label">C=A*2+2</strong>{{ c }}</p> <p><strong class="data-label">D=A+B*2</strong>{{ d }}</p> <p><strong class="data-label">E=B/2</strong>{{ e }}</p> <p><strong class="data-label">F=C+D</strong>{{ f }}</p> <p><strong class="data-label">G=D-E</strong>{{ g }}</p> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.min. js"></script> <script type="text/javascript"> let vm = new Vue({ el: '#app', data () { return { a: 3, b: 4 } }, computed: { // 計算屬性 c () { return this.a * 2 + 2 }, d () { return Number(this.a) + this.b * 2 }, e () { return this.b / 2 }, f () { return Number(this.c) + Number(this.d) }, g () { return this.d - this.e } } }) </script>
初始結果如圖2.14所示。

圖2.14 A和B為初始值時
當修改元數據A和B時,運行結果如圖2.15所示。

圖2.15 修改A和B的值之后
當然,開發者也可以以函數的形式創建數據鏈以實現數據之間的響應,如下代碼:
methods: { getC (suf) { return this.a * 2 + (suf || 2) } }
2.3.4 數據綁定視圖
這是一個含有字符串類型屬性profile的對象:
let obj = { pro file: '' }
不過,身為對象屬性的profile僅僅只是個字符串嗎?筆者在控制臺中使用Object API中的getOwnPropertyDescriptor方法將其“內在”打印出來,如圖2.16所示。

圖2.16 深入對象屬性
原來,對象屬性內藏乾坤,我們甚至可以使用Object API的defineProperty方法對其配置,屬性配置項(描述符)如表2.1所示。
表2.1 對象屬性配置表

在這里可以停頓一下,想一想,對象屬性被賦值時調用的set有何妙用呢?下面來看一段有關defineProperty的代碼:
<span id="harry" style="line-height: 32px;"> </span><br> <input id="trigger" type="text"> <script type="text/javascript"> let harry = document.getElementById('harry') let trigger = document.getElementById('trigger') let key = 'pro file' // 對象屬性鍵名 let store = {} // 輔助get取值 let obj = { // 對象 pro file: '' } Object.de fineProperty(obj, key, { set (value) { harry.innerText = value // 重點:修改DOM節點視圖 store[key] = value }, get () { return store[key] } }) trigger.addEventListener('keyup', function () { obj[key] = this.value console.log(obj[key]) }) </script>
上述代碼中,筆者在對象屬性的setter函數中修改文本節點的值,所以當obj.profile被重新賦值時,節點視圖也會同步更新;然后對輸入框添加事件監聽(addEventListener),當用戶事件觸發時,輸入值將被賦于obj.profile。以此方式,我們實現了數據與視圖之間的“雙向綁定”,這也是Vue數據與視圖綁定的實現原理。
代碼運行結果如圖2.17所示。

圖2.17 數據與視圖綁定
在Vue中,當我們把普通的JavaScript對象傳給Vue實例的data選項時,Vue將遍歷對象屬性,并使用Object.defineProperty將其全部轉化為getter/setter,并在組件渲染時將屬性記錄為依賴。之后當依賴項的setter函數被調用時,會通知watcher重新計算并更新其關聯的所有組件。
由于Object.defineProperty是ES5中一個無法shim(自定義拓展)的特性,所以Vue應用無法運行在不支持Object.defineProperty的IE8及其以下版本瀏覽器上。