近些年來,一股 MVVM 之風颳遍全球,你們無不爲之稱讚。關於 MVVM 架構模式的實現,你們討論的最多的算是 Vue.js 了吧!Vue.js 很好的利用 MVVM 中的 VM 聲明式的實現了與數據模型 Model 和視圖 View 的聯通,使得用戶只需對數據模型進行操做,就能響應到對應的視圖上,這其中核心的實現就是「響應式系統」了。「響應式系統」在整個系統中起到了舉足輕重的做用,爲咱們大大減輕了生產力,瞭解其原理與實現就成爲了咱們技術人無盡的追求,一樣也有助於咱們在實際的生產開發中更好的解決相關的問題。javascript
從上面的分析,不難發現視圖 View 和數據模型 Model 算是咱們最熟悉的了,不須要作過多的說明,可是 VM 視圖模型就是咱們須要深挖的了,它的原理實現對整個 MVVM 模型系統很是重要。接下來,大部份內容就是對這個核心的探討了。html
在詳細介紹這個 Object.defineProperty 以前,咱們先拋出幾個問題:vue
ECMAS-262 第5版在定義只有內部採用的特性時,提供了描述屬性特徵的幾種屬性。ECMAScript 對象中目前存在的屬性描述符主要有兩種:數據描述符(數據屬性)和存取描述符(訪問器屬性)。數據描述符是一個擁有可寫或不可寫值的屬性,存取描述符是由一對 getter-setter 函數功能來描述的屬性。java
Object.defineProperty 就是用來定義對象屬性的屬性描述符方法,它會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。api
數據描述符可包含下列的屬性:數組
configurable:默認爲 false。當且僅當該屬性值爲 true 時,該屬性描述符纔可以被改變,同時該屬性也能從對應的對象上被刪除。數據結構
enumerable:默認爲 false。當且僅當該屬性值爲 true 時,該屬性纔可以出如今對象的枚舉屬性中,好比 for-in循環或 Object.keys() 等。架構
writable:默認爲 false。當且僅當該屬性值爲 true 時,value 才能被賦值運算符改變。ide
value:該屬性對應的值。能夠是任何有效的 JavaScript 值(數值,對象,函數等)。默認爲 undefined。函數
示例代碼:
var obj = {}
Object.defineProperty(obj, 'a', {
value: 1
})
// 獲取屬性 a 的值:obj.a => 1
// 獲取 obj 上可遍歷的屬性:Object.keys(obj) => []
// 刪除 obj 上的屬性 a:delete obj.a => false
// 從新定義 obj 上的屬性 a:Object.defineProperty(obj, 'a', { value: 1, configurable: true }) => Cannot redefine property 直接報錯
// 從新賦值 obj 上的屬性 a 的值爲 2:obj.a = 2
// 從新獲取屬性 a 的值:obj.a => 1 ⚠️注意:從新賦值並無成功
複製代碼
上面的示例代碼能夠說明:當經過 Object.defineProperty 爲對象定義或修改屬性時,默認不指定屬性描述符,全部的屬性描述符的值都是 false,這會致使該屬性不會存在於對象可遍歷的屬性列表中,不能對屬性從新配置(包括刪除和從新定義屬性),還有對該屬性的從新賦值也不會成功。若是顯式的將這些描述符設置爲 true,那麼以上描述的全部不可行的操做都會可行(能夠被從新賦值,能夠被刪除,能夠被從新定義,能夠被遍歷等),這裏我就不演示了,你們感興趣能夠實操一下。
存取描述符可包含下列的屬性:
configurable:默認爲 false。當且僅當該屬性值爲 true 時,該屬性描述符纔可以被改變,同時該屬性也能從對應的對象上被刪除。
enumerable:默認爲 false。當且僅當該屬性值爲 true 時,該屬性纔可以出如今對象的枚舉屬性中,好比 for-in循環或 Object.keys() 等。
get:一個給屬性提供 getter 的方法,若是沒有 getter 則爲 undefined。當訪問該屬性時,該方法會被執行,方法執行時沒有參數傳入,可是會傳入 this 對象(因爲繼承關係,這裏的 this 並不必定是定義該屬性的對象)。默認爲 undefined。
set:一個給屬性提供 setter 的方法,若是沒有 setter 則爲 undefined。當屬性值修改時,觸發執行該方法。該方法將接受惟一參數,即該屬性新的參數值。默認爲 undefined。
示例代碼:
var obj = {}
var val = ''
Object.defineProperty(obj, 'a', {
get() {
return val
},
set(newVal) {
val = newVal
}
})
複製代碼
當你運行上面的示例代碼,你會發現 obj 對象變成了上面這樣,是否是有種很熟悉的感受?對,在 Vue.js 項目中,咱們無時不刻不見到這樣的數據結構。這就是 Object.defineProperty 存取描述符的魅力了:它會給定義過的屬性添加 set 和 get 方法。當咱們經過 . 符號或者 [] 給對象的屬性賦值時,就會觸發 set 方法了;當咱們經過 . 符號或者 [] 獲取對象的屬性的對應值時,就會觸發 get 方法了。正由於 Object.defineProperty 有這樣一個能力,因此咱們能夠經過它實現響應式系統,完成 MVVM 模式中 VM 這重要的一環。
Object.defineProperty 的存取描述符中依然能夠包含 configurable 和 enumerable,這兩個屬性的做用咱們在數據描述符中已經提到過了,這裏就再也不贅述了。
上面咱們已經分析過了,咱們能夠經過 Object.defineProperty 存取描述符將對象定義成可觀察的,而後在 set 和 get 方法中加上對應的邏輯。
爲了便於看到效果,這裏先定義一個方法,該方法在調用時會輸出「視圖更新啦~~」
function cb() {
console.log('視圖更新啦~~')
}
複製代碼
爲了實現更好的複用和便於遞歸,咱們將定義一個名爲 defineReactive 的方法,該方法就是對 Object.defineProperty 邏輯的封裝,它會接受對象、須要定義的屬性 key 值和屬性的值,而後根據這些參數定義屬性的 getter、setter 方法,實現響應式。
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true, // 屬性可枚舉
configurable: true, // 屬性可被修改或刪除
get() {
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
observer(val)
cb(newVal);
}
})
}
複製代碼
要想將數據變成深度可觀察的,咱們還須要封裝一層。封裝的邏輯主要是對類型進行判斷,而後就是對深層的屬性進行遍歷並調用 defineReactive 實現徹底數據響應式。
function observer(value) {
if (!value || (typeof value !== 'object')) {
return
}
Object.keys(value).forEach((key) => {
const val = value[key]
observer(val)
defineReactive(value, key, val);
})
}
複製代碼
到了這一步,就來測試一下咱們的成果吧:
// 定義一個 obj 多級嵌套對象
var obj = {a: 1, b: { c: 2 }}
// 將 obj 變成可觀察的
observer(obj)
obj.b = 'lane' => 視圖更新啦~~
複製代碼
成果還不錯,咱們就來趁熱打鐵封裝一個簡單的 Vue 響應式系統吧!先來看一個最簡單的 Vue 使用示例:
const vm = new Vue({
data: {
message: 'I am lane.'
}
})
複製代碼
Vue 會做爲構造函數進行調用並接受一個對象做爲函數。目前在最簡單的狀況下,參數對象只包含一個 data 屬性,咱們的目的就是將這個 data 屬性值變成可觀察的。
// Vue構造類
class Vue {
constructor(options) {
this._data = options.data;
observer(this._data);
}
}
複製代碼
測試簡易封裝的 Vue 示例代碼:
vm._data.message = 'hello, world.' // 視圖更新啦~
複製代碼
固然這還只是 Vue.js 中響應式系統的第一步,爲了更好的進行數據更新處理,系統還須要進行依賴收集,以確保數據更新性能達到更優。
經過 Object.defineProperty 實現的數據響應式邏輯對於數組的許多方法都不能觸發 set 方法(包括 push、pop、shift、unshift、splice、sort、reverse),Vue.js 爲了解決這個問題,從新包裝了這些函數,同時當這些方法被調用的時候,手動去觸發更新操做;還有另外一個問題,官網也有特別的指出:
因爲 JavaScript 的限制,Vue 不能檢測如下變更的數組:
- 當你利用索引直接設置一個項時,例如:vm.items[indexOfItem] = newValue
- 當你修改數組的長度時,例如:vm.items.length = newLength
這個最根本的緣由是由於這兩種狀況下,受制於js自己沒法實現監聽,因此官方建議用他們本身提供的內置 api 來實現,咱們也能夠理解到這裏既不是 defineProperty 能夠處理的,也不是包一層函數就能解決的,這就是 2.x 版本如今的一個問題。
咱們能夠利用咱們以前的定義來實驗一把:
const vm = new Vue({
data: {
userIds: ['01', '02', '03', '04', '05']
}
})
// 都沒有輸出 視圖更新啦~,說明沒有觸發 set
vm._data.userIds.push('06')
vm._data.userIds.length = 2
複製代碼
今天關於數據響應式的初探就到這裏吧,說到的東西也挺多的,首先是 MVVM 模式的架構,而後對 MVVM 的每一個組成都進行詳細的說明,接着說到了目前 Vue.js 經過 Object.defineProperty 實現響應式數據的方式,並對 Object.defineProperty 的用法和數據描述符與存取描述符進行了詳細的講解,最後利用 Object.defineProperty 封裝了一個簡單的 Vue 響應式系統,最後的最後提到了關於 Object.defineProperty 的一些缺陷。固然這還只是走出了第一步,Vue.js 的響應式系統還包括數據劫持、依賴收集等,固然後面咱們也會慢慢的提到。