1. 程式人生 > >Vue資料雙向繫結探究

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來做,效率更高,期待!!!!

參考和學習原文(可能需要翻牆,畢竟是外站啊)

原文https://segmentfault.com/a/1190000017107719