本文寫於 2020 年 8 月 5 日html
相信在不少新人第一次使用 Vue 這種框架的時候,就會被其修改數據便自動更新視圖的操做所震撼。react
Vue 的文檔中也這麼寫道:程序員
Vue 最獨特的特性之一,是其非侵入性的響應式系統。數據模型僅僅是普通的 JavaScript 對象。而當你修改它們時,視圖會進行更新。算法
單看這句話,像我這種菜鳥程序員必然是看不懂的。我只知道,在 new Vue()
時傳入的 data
屬性一旦產生變化,那麼在視圖裏的變量也會隨之而變。設計模式
但這個變化是如何實現的呢?接下來讓咱們,一探究竟。數組
咱們先來新建一個變量:let data = { msg: 'hello world' }
。閉包
接着咱們將這個 data 傳給 Vue 的 data:框架
let data = { msg: 'hello world' } /*****留空處*****/ new Vue({ data, methods: { showData() { console.log(data) } } })
這看似是很是日常的操做,可是咱們在觸發 showData 的時候,會發現打出來 data 不太對勁:異步
msg: (...) __ob__: Observer {value: {…}, dep: Dep, vmCount: 1} get msg: ƒ reactiveGetter() set msg: ƒ reactiveSetter(newVal) __proto__: Object
它不只多了不少沒見過的屬性,還把裏面的 msg: hello world
變成了 msg: (...)
。ide
接下來咱們嘗試在留空處打印出 data,即在定義完 data 以後當即將其打印。
可是很不幸,打印出來依然是上面這個不對勁的值。
但是很明顯,當咱們不去 new Vue()
,而且傳入 data 的時候,data 的打印結果絕對不是這樣。
因此咱們能夠嘗試利用 setTimeout()
將 new Vue()
延遲 3 秒執行。
這個時候咱們就會驚訝的發現:
這說明就是 new Vue()
的過程當中,Vue 偷偷的對 data 進行了修改!正是這個修改,讓 data 的數據,變成了響應式數據。
(...)
的由來爲何好好的一個 msg
屬性會變成 (...)
呢?
這就涉及到了 ES6 中的 getter 和 setter。(若是理解 getter/setter,可跳至下一節)
通常咱們若是須要計算後的值,會定義一個函數,例如:
const obj = { number: 5, double() { return this.number * 2; } };
在使用的時候,咱們寫上 obj.double(obj.number)
便可。
可是函數是須要加括號的,我太懶了,以致於括號都不想要了。
因而就有了 getter 方法:
const obj = { number: 5, get double() { return this.number * 2; } }; const newNumber = obj.double;
這樣一來,就可以不須要括號,就能夠獲得 return 的值。
setter 同理:
const obj = { number: 5, set double(value) { if(this.number * 2 != value;) this.number = value; } }; obj.double = obj.number * 2;
由此咱們能夠看出:經過 setter,咱們能夠達到給賦值設限的效果,例如這裏我就要求新值必須是原值的兩倍才能夠。
但常常的,咱們會用 getter/setter 來隱藏一個變量。
好比:
const obj = { _number: 5, get number() { return this._number; }, set number(value) { this._number = value; } };
這個時候咱們打印出 obj,就會驚訝的發現 (...)
出現了:
number: (...) _number: 5
如今咱們明白了,Vue 偷偷作的事情,就是把 data 裏面的數據全變成了 getter/setter。
Object.defineProperty()
實現代理這個時候咱們想一個問題,原來咱們能夠經過 obj.c = 'c';
來定義 c 的值——即便 c 自己不在 obj 中。
但如何定義一個 getter/setter 呢?答:使用 Object.defineProperty()
。
Object.defineProperty()
方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。
例如咱們上面寫的 obj.c = 'c';
,就能夠經過
const obj = { a: 'a', b: 'b' } Object.defineProperty(obj, 'c', { value: 'c' })
Object.defineProperty()
接收三個參數:第一個是要定義屬性的對象;第二個是要定義或修改的屬性的名稱或 Symbol;第三個則是要定義或修改的屬性描述符。
在第三個參數中,能夠接收多個屬性,value
表明「值」,除此以外還有 configurable
, enumerable
, writable
, get
, set
一共六個屬性。
這裏咱們只看 get
與 set
。
以前咱們說了,經過 getter/setter 咱們能夠把不想讓別人直接操做的數據「藏起來」。
但是本質上,咱們只是在前面加了一個 _
而已,直接訪問是能夠繞過咱們的 getter/setter
的!
那麼咱們怎麼辦呢?
利用代理。這個代理不是 ES6 新增的 Proxy
,而是設計模式的一種。
咱們剛剛爲何能夠去修改咱們「藏起來」的屬性值?
由於咱們知道它的名字呀!若是我不給他名字,天然別人就不可能修改了。
例如咱們寫一個函數,而後把數據傳進去:
proxy({ a: 'a' })
這樣一來咱們的 { a: 'a' }
就根本沒有名字了,無從改起!
接下來咱們在定義 proxy
函數時,能夠新建一個空對象,而後遍歷傳入的值,分別進行 Object.defineProperty()
以將傳入的對象的 keys 做爲 getter/setter 賦給新建的空對象。
最後,咱們 return
這個對象便可。
let data = proxy({ a: 'a', b: 'b' }); function proxy(data) { const obj = {}; const keys = Object.keys(data); for (let i = 0; i < keys.length; i++) { Object.defineProperty(obj, keys[i], { get() { return data[keys[i]]; }, set(value) { if (value < 0) return; data[keys[i]] = value; } }); } return obj; }
這樣一來,咱們一開始聲明的 data
,就是咱們 return
的對象了。在這個對象裏,沒有原始的數據,別人沒法繞過 getter/setter 進行操做!
可是每每並無這麼簡單,若是我必定須要一個變量名呢?
const sourceData = { a: 'a', b: 'b' }; let data = proxy(sourceData);
如此一來,經過直接操做 sourceData.a
,時能夠直接繞過咱們在 proxy
中設置的 set a
進行賦值的。這個時候咱們怎麼處理?
很簡單嘛,當咱們遍歷傳入的數據時,咱們能夠對傳入的數據新增 getter/setter,此後原始的數據就會被 getter/setter 所替代。
在剛剛的代碼中,咱們在循環的剛開始添加這樣一段代碼:
for(/*......*/) { const value = data[keys[i]]; Object.defineProperty(data, keys[i], { get() { return value; }, set(newValue) { if (newValue < 0) return; value = newValue; } }); /*......*/ }
這是什麼意思呢?
咱們利用了閉包,將原始值單獨拎出來,每一次對原始屬性進行讀寫,其實都是 get 和 set 在讀取閉包時被拎出來的值。
那麼無論別人是操做咱們的 let data = proxy(sourceData);
的 data,仍是操做 sourceData,都會被咱們的 getter/setter 所攔截。
咱們剛剛寫的代碼是這樣的:
let data = proxy({ a: 'a' }) function proxy(data) { }
那若是我改爲這樣呢:
let data = proxy({ data: { a: 'a' } }) function proxy({ data }) { // 結構賦值 }
是否是和 Vue 就很是很是像了!
const vm = new Vue({ data: {} })
也是讓 vm
成爲 data
的代理,而且就算你從外部將數據傳給 data,也會被 Vue 所捕捉。
而在每一次捕獲到你操做數據以後,就會對須要改變的 UI 進行從新渲染。
同理,Vue 對 computed 和 watch 也存在着各類偷偷的處理。
若是咱們的數據是這樣:
data: { obj: { a: 'a' } }
咱們在 Vue 的 template 裏卻寫了
{{ obj.b }}
會怎樣?
Vue 對於不存在或者爲 undefined 和 null 的數據是不予以顯示的。可是當咱們往 obj 中新增 b 的時候,他會顯示嗎?
寫法一:
const vm = new Vue({ data: { obj: { a: 'a' } }, methods: { changeObj() { this.obj.b = 'b'; } } })
咱們能夠給一個按鈕綁定 changeObj
事件,可是很遺憾,這樣並不能使視圖中的 obj.b
顯示出來。
回想一下剛剛咱們對於數據的處理,是否是隻遍歷了外層?這就是由於 Vue 並無對 b 進行監聽,他根本不知道你的 b 是如何變化的,天然也就不會去更新視圖層了。
寫法 2:
const vm = new Vue({ data: { obj: { a: 'a' } }, methods: { changeObj() { this.obj.a = 'a2' this.obj.b = 'b'; } } })
咱們僅僅只是新增了一行代碼,在改變 b 以前先改變了 a,竟然就讓 b 實現了更新!
這是爲何?
由於視圖更新實際上是異步的。
當咱們讓 a
從 'a'
變成 'a2'
時,Vue 會監聽到這個變化,可是 Vue 並不能立刻更新視圖,由於 Vue 是使用 Object.defineProperty()
這樣的方式來監聽變化的,監聽到變化後會建立一個視圖更新任務到任務隊列裏。
因此在視圖更新以前,要先把餘下的代碼運行完才行,也就是會運行 b = 'b'
。
最後等到視圖更新的時候,因爲 Vue 會去作 diff 算法,因而 Vue 就會發現 a 和 b 都變了,天然會去更新相對應的視圖。
可是這並非咱們解決問題的辦法,寫法 2 充其量只能算是「反作用」。
Vue 其實提供了方法讓咱們來新增之前沒有生命的屬性:Vue.set()
或者 this.$set()
。
Vue.set(this.obj, 'b', 'b');
會代替咱們進行 obj.b = 'b';
,而後監聽 b 的變化,觸發視圖更新。
那數組怎麼響應呢?
每當咱們往數組裏新增元素的時候,數組就在不斷的變長。對於沒有聲明的數組下標,很明顯 Vue 不會給予監聽呀。
好比 a: [1, 2, 3]
,當我新增一個元素,讓 a === [1, 2, 3, 4]
的時候,a[3]
是不會被監聽的。
總不能每次 push
數組,都要手寫剛剛說的 Vue.set
方法吧。
可實際操做中,咱們發現並無呀,Vue 監聽了新增的數據。
這是由於 Vue 又偷偷的幹了一件事兒,它把你本來的數組方法給改了一些。
在 Vue 中的數組所帶的這七個方法都不是原生的方法了。Vue 考慮到這些操做極爲經常使用,所在中間爲咱們添加了監聽。
講到這裏,相信你們對 Vue 的響應式原理應該有了更深的認識了。
(完)