Vue資料雙向繫結探究
使用過vue的小夥伴都會感覺,哇,這個框架對開發者這麼友好,簡直都要笑出聲了。
確實,使用過vue的框架做開發的人都會感覺到,以前寫一大堆操作dom,bom的東西,現在用不著了,對開發者來說更容易去注重對操作邏輯的思考和實現,省了不少事兒呢!!!
我是直接從原生js,jq的開發用過度到使用vue,對這個框架也是喜愛有加,閒來無事,去看了看它的一些實現原理。
下面來介紹一下vue的一個非常"牛逼"的功能,資料雙向繫結,也就是我們在專案裡用到的v-model指令。
v-model在vue官方文件上是介紹在"表單輸入繫結"那一節。
對於表單,大家肯定用得都已經超級熟練了,對於<input>、<textarea>和<select>標籤在專案裡面使用都已經沒話說了
官方提到的v-model是一個語法糖,為什麼這麼說呢?下面看個例子:
<div id="test1">
<input v-model="input">
<span>input: {{ input }}</span>
</div>
如上,是一個簡單的使用v-model的雙向繫結,我們在改變input這個變數的值,即在輸入框中去寫內容的時候,在span標籤內的插值(mustache)會同步更新我們剛剛輸入的值
其實上面的也可以這樣寫:
<div id="test1"> <input v-on:input="input = $event.target.value" v-bind:value='input'> <span>input: {{ input }}</span> </div>
好了,前面囉裡囉嗦半天,現在進入正題
想對比react和angular的雙向繫結實現,我也不清楚,哈哈哈,直接說vue吧,不扯了
Reactivity 響應式系統
拿尤雨溪大佬做vue測試的的那個例子來說吧(購物車的例子)
<div id='app'> <div> <span>價格:</span> <input v-model.number="price"> </div> <div> <span>數量:</span> <input v-model.number="quantity"> </div> <p>價格:{{ price }}</p> <p>數量:{{ quantity }}</p> <p>總計:{{ total }}</p> </div>
data() {
return {
price: 5,
quantity: 3
}
},
computed: {
total() {
return this.price * this.quantity;
}
}
當我們在使用輸入框的值的時候,下面的total會更新,我們對應輸入值的變數也會更新
哇,好神奇,為什麼呢,這不是JavaScript程式設計常規的工作方式!!!
因為我們用原生js寫的時候是這樣的:
let price = 5;
let quantity = 3;
let total = price * quantity; // 等於15吧
price = 10; // 改變價格;
console.log(total); // bingo,列印的還是15
我們需要在找一種辦法,把要執行計算的total放到別的時候去執行,當我們的價格、數量變化的時候執行
let price = 5;
let quantity = 3;
let total = 0;
let storage = []; // 儲存將要計算的操作,等到變數變化的時候去執行
let target= () => { total = price * quantity;}
function record () {
storage.push(target);
}
function replay() {
storage.forEach(run => run());
}
record();
target();
price = 10;
console.log(total); // 依然是15
replay();
console.log(total); // 執行結果是30
目的達到,但是這樣肯定不是vue用來擴充套件使用的方式,我們用ES6的class類來做一個可維護的擴充套件,實現一個標準的觀察者模式的依賴類
class Depend {
constructor () {
this.subscribers = [];
}
depend() {
if(target && this,this.subscribers.includes(target)) {
this.subscribers.push(target);
}
}
notify() {
this.subscribers.forEach(sub => sub());
}
}
// 來執行上面寫的class
const dep = new Depend();
let price = 5;
let quantity = 3;
let total = 0;
let target = () => { total = price * quantity };
dep.depend();
target();
console.log(total); // total是15
price = 10;
console.log(total); // 因為沒有執行target,依舊是15
dep.notify();
console.log(total); // 執行了存入的target,total為30
為了給每一個變數都設定一個Depend類。並且很好地控制監視更新的匿名函式的行為,我們把上面的程式碼做一些調整:
let target = () => { total = price * quantity };
dep.depend();
target();
修改為:
watcher(() => { total = price * quantity });
然後我們在watcher函式裡面來做剛剛上面的result的設定和執行的功能
function watcher(fn) {
target = fn;
dep.depend();
target();
target = null; // 重置一下,等待儲存和執行下一次
}
這兒就是官方文件提到的訂閱者模式:在每次watcher函式執行的時候,把引數fn設定成為我們全域性目標屬性,呼叫dep.depend()將目標新增為訂閱者,呼叫然後重置
然後再繼續
我們的目標是把每一個變數都設定一個Depend類,但是這兒有個問題:
先存一下資料:
let data = { price: 5, quantity: 3}
假設我們每個屬性都有自己的內部Depend類
當我們執行程式碼時:
watcher(() => { total = data.price * data.quantity})
由於訪問了data.price值,希望price屬性的Depend類將我們的匿名函式(儲存在目標中)推送到其訂閱者陣列(通過呼叫dep.depend())。由於訪問了data.quantity,還希望quantity屬性Depend類將此匿名函式(儲存在目標中)推送到其訂閱者陣列中。
如果有另一個匿名函式,只訪問data.price,希望只推送到價格屬性Depend類。
什麼時候想要在價格訂閱者上呼叫dep.notify()?我希望在設定價格時呼叫它們。為此,我們需要一些方法來掛鉤資料屬性(價格或數量),所以當它被訪問時我們可以將目標儲存到我們的訂閱者陣列中,當它被更改時,執行儲存在我們的訂閱者陣列中的函式。let's go
Object.defineProperty來解決這個問題
Object.defineProperty函式是簡單的ES5 JavaScript。它允許我們為屬性定義getter和setter函式。繼續啃
let data = { price: 5, quantity: 3};
let value = data.price
Object.defineProperty(data, 'price', {
getter() {
console.log(`獲取price的值: ${value}`);
return value;
},
setter(newValue) {
console.log(`更改price的值': ${newValue}`);
value = newValue;
}
})
total = data.price * data.quantity;
data.price = 10; // 更改price的值
上面通過defineProperty方法給price設定了獲取和修改值的操作
如何給data物件所有的都加上這個defineProperty方法去設定值
大家還記得Object.keys這個方法嗎?返回物件鍵的陣列,咱們把上面的程式碼改造一下
let data = { price: 5, quantity: 3 };
Object.keys(data).forEach(key => {
let value = data[key];
Object.defineProperty(data, key, {
getter() {
console.log(`獲取 ${key} 的值: ${value}`);
return value;
},
setter(newValue) {
console.log(`更改 ${key} 值': ${newValue}`);
value = newValue;
}
})
})
total = data.price * data.quantity;
data.price = 10; // 更改price的值
接著上面的東西,在每次執行完獲取key的值,我們希望key能記住這個匿名函式(target),這樣有key的值變化的時候,它將觸發這個函式來重新計算,大致思路是這樣的:
getter函式執行的時候,記住這個匿名函式,當值在發生變化的時候再次執行它
setter函式執行的時候,執行儲存的匿名函式,把當前的值存起來
用上面定義的Depend類來說就是:
getter執行,呼叫dep.depend()來儲存當前的target
setter執行,在key上呼叫dep.notify(),重新執行所有的target
來來來,把上面的東西結合到一起來
let data = { price: 5, quantity: 3 };
let total = 0;
let target = null;
class Depend {
constructor() {
this.subscribers = [];
}
depend() {
if (target && this.subscribers.includes(target)) {
this.subscribers.push(target);
}
}
notify() {
this.subscribers.forEach(sub => sub());
}
}
Object.keys(data).forEach(key => {
let value = data[key];
const dep = new Depend();
Object.defineProperty(data, key, {
getter() {
dep.depend();
return value;
},
setter(newValue) {
value = newValue;
dep.notify();
}
})
});
function watcher(fn) {
target = fn;
target();
target = null;
}
watcher(() => {
total = data.price * data.quantity;
});
至此,vue的資料雙向繫結已經實現,當我們去改變price和quantity的值,total會實時更改
然後咱們來看看vue的文件裡面提到的這個插圖:
是不是感覺這個圖很熟悉了?對比咱們上面研究的流程,這個圖的data和watcher就很清晰了,大致思路如此,可能vue的內部實現和封裝遠比我這個研究流程內容大得多、複雜得多,不過有了這樣的一個流程思路,再去看vue雙向繫結原始碼估計也能看懂個十之八九了。
聽說vue3.0準備把這個資料劫持的操作用ES6提供的proxy來做,效率更高,期待!!!!