閱完此文,Vue響應式不再話下

  • 2019 年 12 月 10 日
  • 筆記

vue 的雙向數據綁定,眾所周知是基於 Object.defineProperty 這個在瀏覽器的特性 api 來實現的。但是怎麼從視圖到數據,數據到視圖,這個整個大過程,對於很多盆友來說,還有點不是很清楚。

這篇文章,將會特別輕鬆的換個角度讓你明白整個過程。just do it !!! ???

Vue的響應式系統

我們第一次使用 Vue 的時候,會感覺有些神奇,舉個例子:

<div id="app">    <div>價格:¥{{price}}</div>    <div>總價:¥{{price*quantity}}</div>    <div>折扣後:¥{{totlePriceWithTax}}</div>  </div>  <script>  	var vm=new Vue({      el:'#app',  		data:(){      	price:5.00,//單價      	quantity:2//數量    	},      computed:{         totlePriceWithTax(){      			return this.price*this.quantity*1.03    			}      }    })  </script>  

我們使用 vue 的時候,不知道它內部做了什麼。它都能知道 price 這個欄位的值是否發生過變化,如果發生了變化,他會做如下幾件事:

  • 更新頁面顯示的 price 的值
  • 重新計算總價的乘法表達式並且更新顯示結果
  • 重新調用 totlePriceWithTax 函數,並且更新顯示

這兒,咱們就有一個疑問,vue 怎麼就知道 price 變化了之後,都要更新哪些值呢?為什麼,每次一變化,就要更新呢?如何跟蹤的呢?

JavaScript 正常的運行方式

我們把這個例子整理成我們正常的 JavaScript 程式來看看:

let price=5;  let quantity=2;  let total=price*quantity;//計算總價  pice=20;//price欄位發生變更之後  console.log(`變化之後的總價:${total}`);  

這個會輸出列印多少呢?因為我這兒沒有使用 Vue,很明顯,這兒會輸出 10:

>> 變化之後的總價:10  

在咱們經常使用的 Vue 中,我們想要在 price 或者 quantity 這兩個欄位更新時,和它有關的表達式也會更新,和它有關的函數也會執行。

>> 變化之後的總價:40  

但是,javascript 是過程性的,不是響應式的,所以這個程式碼在實際運行的時候是不行的。為了讓 total 在 price 更新的時候,它也跟著更新,我們必須讓 JavaScript 語言具備不同的運行方式。

問題

那麼我們現在就遇到了一個問題,怎麼樣,才能在 price 欄位或者 quantity 更新的時候,total 也重新更新顯示呢?

嘗試一下

首先,我們需要明白 price 和 totle 的關聯是:

let total=price*quantity;  

那麼,在 price 更新之後,需要重新得到新的 total,就需要重新執行這個方法。那麼就需要有一個地方把這個方法儲存起來,在 price 變更的時候,重新運行儲存起來的方法,這樣 total 值就更新了。

那我們就來嘗試一下,把函數記錄下來,後面變更的時候,再次運行。

let price=5;  let quantity=2;  let total=0;  let target=null;    //記錄函數  target=()=>{    total=price*quantity;  }  record();//後面講解,記住這個我們後面想要運行的函數  target();//同時,我們執行一遍這個方法    

record 記錄函數的實現就很簡單了:

let storage=[];//這是要記錄函數的地方,就是上面圖中橢圓的那個東西      //記錄方法的實現,這個時候的target就是我們要記錄的方法  function record(){    storage.push(target)  }  

這一步,將 target 儲存了起來,這樣我們後面就可以運行它。這個時候,我們就需要一個運行所有記錄的內容的函數。那我們就來搞一哈:

  function replay(){    storage.forEach((run)=>{      run();    })  }  

這兒,我們遍歷了所有記錄的內容,並且每一個都執行。

這個時候,我們的程式碼就可以更改一下:

let price=5;  let quantity=2;  let total=0;  let target=null;    function record(){    storage.push(target)  }    function replay(){    storage.forEach((run)=>{      run();    })  }    target=()=>{    total=price*quantity;  }  record();  target();    console.log(total)// 10  price=20;  replay();  console.log(total)//40  

這樣我們就實現了,一個記錄的過程,但是這樣沒有一個很好地管理,我們能不能把記錄這塊的內容,維護成一個類,讓這個類維護一個 tagert 列表,每次需要重新運行的時候,這個類都會得到通知。

年輕人,火力旺,說干就干。維護一個單獨的Dep類,程式碼如下:

class Dep{    constructor(){      this.subscribers=[];//維護所有target的列表,在得到通知的時候,全部都會運行    }    depend(){      if(target&&!this.subscribers.includes(target)){         //只有target有方法,並且沒有被記錄過        this.subscribers.push(target);      }    }    notify(){      this.subscribers.forEach((sub)=>{        sub();      })    }  }  

在這個類中,我們不再使用 storage,使用 subscribers 這個字元來記錄 target 函數的內容,也不再使用 record,使用 depend,也用了 notify 替代了 replay,這個時候要運行,就只需要:

const dep=new Dep();    let price=5;  let quantity=2;  let total=0;  let target=null;    target=()=>{    total=price*quantity;  }  dep.depend();//記錄到subscribers中  target();    console.log(total)// 10  price=20;  dep.notify();//遍歷執行所有target,分發內容  console.log(total)//40  

這樣,整體的過程就會好一點,但是還是會顯得很冗餘,如果能過把匿名函數創建,觀察,更新的這些行為封裝起來,那就更好了。

年輕人,總是衝動,咱們說干就干。把原來的創建和記錄:

target=()=>{    total=price*quantity;  }  dep.depend();//記錄到subscribers中  target();  

這塊內容封裝起來,咱們給封裝起來的函數起名叫做 watcher,封裝起來之後,我們就只需要這樣調用:

watcher(()=>{    total=price*quantity  })  

那我們在實現 watcher 的時候,這麼做就好:

function watcher(myFunc){    target=myFunc;//傳入的函數賦值    dep.depend();//收集    target();//執行一下    target=null;//重置  }  

這兒,咱們看到 watcher 函數接受了一個變數 myFunc, 這個 myFunc 後面接收的是匿名函數,然後賦值給 target 屬性,調用 dep.depend(),將以訂閱者的形式添加 target 到記錄的地方,然後調用 target,並且重置。

現在結合上面的程式碼咱們嘗試一下這個程式碼:

price=20;  console.log(total);  dep.notify();  console.log(total);  

這裡面有一個問題,就是target為什麼要設置成全局變數,而不是將其傳遞給需要的函數。咱們後面會細聊。

現在我們有一個Dep類了,但是我們整整想要實現的情況是,每一個變數都有響應的地方記錄它關聯的變更,每個變數都有自己的Dep。這個可咋整?

年輕人,不怕事,說干就干。咱們首先把所有的變數放到一起:

let data={    price:5,    quantity:2  }  

現在我們假設每一個屬性(price和quantity)都有自己內部的Dep類。

當我們運行watcher這個函數的時候:

wacther(()=>{    total=data.price*data.quantity  })  

因為我們是使用到了data.price的值,那麼我們希望price屬性的Dep類可以將使用它的匿名函數(儲存在target上)放在訂閱數組中,記錄下來(通過調用dep.depend())。同時data.quantity這個變數也被訪問了,所以也希望能夠被記錄下來,放在對應的訂閱數組中:

如果這個時候還有其他的地方也在使用data.price,我們也希望可以把對應的匿名函數放到Dep類中記錄下來。

那麼,什麼時候會調用price對應的Dep中的notify呢?在price賦值,值發生改變的時候。我們最後希望發生的效果是:

>> total  10  >> price=20  >> total  40  

我們希望,當數據被訪問的時候,能夠把對應的target匿名函數儲存到訂閱數組中,當屬性變更的時候,能夠運行對應的儲存在訂閱數組中的匿名函數。

解決方案

這個一眼看過去,訪問時,改變時。腦海中直接就出來了Object.defineProperty,這個允許我們為屬性定義getter和setter函數。在展示如何和Dep結合的之前,先看下用法:

let data={price:5,quantity:2};  Object.defineProperty(data,'price',{    get(){      console.log('被訪問')    },    set(newVal){      console.log('被修改')    }  });  data.price;//輸出:被訪問  data.price=20;//輸出:被修改  

這裡,我們並沒有實際的修改get和set的值,因為功能被覆蓋了。現在,我們希望get的時候能夠返回一個值,set的時候能夠更新值。所以我們先添加一個變數internalValue來儲存當前的price的值。

let data={price:5,quantity:2};    let internalValue=data.price;//初始值    Object.defineProperty(data,'price',{    get(){      console.log('被訪問');      return internalValue    },    set(newVal){      console.log('被修改');      internalValue=newVal    }  });  total=data.price*data.quantity;//調用get  data.price=20;//調用set  

這樣我們就可以把所有我們想要的監聽的數據,全部給處理一下:

let data={price:5,quantity:2};    Object.keys(data).forEach((key)=>{    let internalValue=data[key];//初始值      Object.defineProperty(data,key,{      get(){        console.log('被訪問');        return internalValue      },      set(newVal){        console.log('被修改');        internalValue=newVal      }    });  })    total=data.price*data.quantity;//調用get  data.price=20;//調用set  

這樣所有的數據都變了可監聽的了。

把他們結合起來

total=data.price*data.quantity  

當這個程式碼運行的時候,會觸發price屬性對應的get方法,我們希望price的Dep可以記住這個對應的匿名函數(target)。通過這個方式,如果發生改變,觸發了set,那麼就能夠調用這個屬性對應的儲存起來的匿名函數。

  • Get—記住匿名函數,當值發生變化的時候重新運行。
  • Set—運行保存的匿名函數,對應匿名函數綁定的值就會發生變化

切換到Dep class的模式:

  • price被訪問時—調用dep.depend保存當前target
  • price被改變時—調用price的dep.notify,重新運行所有的target

最後,我們就把這個結合起來,年輕人,不要磨磨蹭蹭,突突兩下就可以了:

let data={price:5,quantity:2};  let target=null;      class Dep{    constructor(){      this.subscribers=[];//維護所有target的列表,在得到通知的時候,全部都會運行    }    depend(){      if(target&&!this.subscribers.includes(target)){         //只有target有方法,並且沒有被記錄過        this.subscribers.push(target);      }    }    notify(){      this.subscribers.forEach((sub)=>{        sub();      })    }  }      Object.keys(data).forEach((key)=>{    let internalValue=data[key];//初始值      Object.defineProperty(data,key,{      get(){        console.log('被訪問');        dep.depend();//添加對應的匿名函數target        return internalValue      },      set(newVal){        console.log('被修改');        internalValue=newVal;        dep.notify();//觸發對應的儲存的函數      }    });  })    function watcher(myFunc){    target=myFunc;//傳入的函數賦值    target();//執行一下    target=null;//重置  }  watcher(()=>{    data.total=data.price*data.quantity;  })  

這就結合了這一塊的東西,price和quantity兩個屬性變成了響應式的情況,可以下來試一下。

直接上架構圖:

最後,Vue2中還有很多東西,Vue3也出來了,我們這塊出了對應的課程。年輕人不要猶猶豫豫。機會和成長總在猶豫的時候就溜走了。

在這樣一個資訊爆炸、知識唾手可得的時代,年輕人一定要做個明白人,懂得篩選和判斷優質內容。

你可能經常會領取到海量前端資料包,收藏起來就再也沒看過。

但今天,我們想給你點真正有品質的內容——【你不知道的Vue.js 性能優化】

  • 本次專題課深度講解 Vue.js 性能優化,以及 Vue3.0 那些值得關注的新特性。在高級前端崗位面試中,性能優化是一個必問的知識點,本課程通過對 Vue 面試核心知識點的拆解,帶你解鎖你可能不知道的 Vue.js 性能優化,直達大廠offer!

它將帶你學到什麼?

1.Vue首屏優化實踐

  • 大廠面試問Vue項目優化時的各種講解
  • 核心工程化知識點講解
  • 不同的核心優化方案剖析
  • 常考Vue知識點串講

2.面試常問的Vue雙向數據深度解析

  • 修正對於Object.defineProperty的錯誤理解
  • Vue2中雙向數據綁定為什麼性能不好?
  • 數組的雙向數據綁定怎麼處理的

3.深度對比 Vue2 & 3,助你直達offer

  • 淺嘗Vue3的使用
  • Vue3的新特性解析
  • Vue3核心雙向數據綁定的實現解析
  • 深度對比Vue2,助你直達offer