如何在Vue中使localStorage具有響應式(思想實驗)
響應式是Vue.js的最大特色之一。如果你不知道幕後情況,它也是最神祕的地方之一。例如,為什麼它不能用於物件和陣列,而不能用於諸如 localStorage 之類的其他東西?
讓我們回答這個問題,在解決這個問題時,讓Vue響應式與 localStorage 一起使用。
如果執行以下程式碼,則會看到計數器顯示為靜態值,並且不會像我們期望的那樣發生變化,這是因為setInterval在 localStorage 中更改了該值。
new Vue({ el: "#counter",data: () => ({ counter: localStorage.getItem("counter") }),computed: { even() { return this.counter % 2 == 0; } },template: `<div> <div>Counter: {{ counter }}</div> <div>Counter is {{ even ? 'even' : 'odd' }}</div> </div>` });
// some-other-file.js setInterval(() => { const counter = localStorage.getItem("counter"); localStorage.setItem("counter",+counter + 1); },1000);
儘管Vue.js例項中的 counter 屬性是響應式的,但它不會因為我們更改了它在 localStorage 中的來源而更改。
有多種解決方案,最好的也許是使用Vuex,並保持儲存值與 localStorage 同步。但如果我們需要像本例中那樣簡單的東西呢?我們要深入瞭解一下Vue.js的響應式系統是如何工作的。
Vue 中的響應式
當Vue初始化元件例項時,它將觀察data選項。這意味著它將遍歷資料中的所有屬性,並使用 Object.defineProperty 將它們轉換為getter/setter。通過為每個屬性設定自定義設定器,Vue可以知道屬性何時發生更改,並且可以通知需要對更改做出反應的依賴者。它如何知道哪些依賴者依賴於一個屬性?通過接入getters,它可以在計算的屬性、觀察者函式或渲染函式訪問資料屬性時進行註冊。
// core/instance/state.js function initData () { // ... observe(data) }
// core/observer/index.js export function observe (value) { // ... new Observer(value) // ... } export class Observer { // ... constructor (value) { // ... this.walk(value) } walk (obj) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj,keys[i]) } } } export function defineReactive (obj,key,...) { const dep = new Dep() // ... Object.defineProperty(obj,{ // ... get() { // ... dep.depend() // ... },set(newVal) { // ... dep.notify() } }) }
所以,為什麼 localStorage 不響應?因為它不是具有屬性的物件。
但是等一下,我們也不能用陣列定義getter和setter,但Vue中的陣列仍然是反應式的。這是因為陣列在Vue中是一種特殊情況。為了擁有響應式的陣列,Vue在後臺重寫了陣列方法,並與Vue的響應式系統進行了修補。
我們可以對 localStorage 做類似的事情嗎?
覆蓋localStorage函式
首先嚐試通過覆蓋localStorage方法來修復最初的示例,以跟蹤哪些元件例項請求了localStorage專案。
// LocalStorage專案鍵與依賴它的Vue例項列表之間的對映。 const storeItemSubscribers = {}; const getItem = window.localStorage.getItem; localStorage.getItem = (key,target) => { console.info("Getting",key); // 收集依賴的Vue例項 if (!storeItemSubscribers[key]) storeItemSubscribers[key] = []; if (target) storeItemSubscribers[key].push(target); // 呼叫原始函式 return getItem.call(localStorage,key); }; const setItem = window.localStorage.setItem; localStorage.setItem = (key,value) => { console.info("Setting",value); // 更新相關Vue例項中的值 if (storeItemSubscribers[key]) { storeItemSubscribers[key].forEach((dep) => { if (dep.hasOwnProperty(key)) dep[key] = value; }); } // 呼叫原始函式 setItem.call(localStorage,value); };
new Vue({ el: "#counter",data: function() { return { counter: localStorage.getItem("counter",this) // 我們現在需要傳遞“this” } },template: `<div> <div>Counter: {{ counter }}</div> <div>Counter is {{ even ? 'even' : 'odd' }}</div> </div>` });
setInterval(() => { const counter = localStorage.getItem("counter"); localStorage.setItem("counter",1000);
在這個例子中,我們重新定義了 getItem 和 setItem,以便收集和通知依賴 localStorage 專案的元件。在新的 getItem 中,我們注意到哪個元件請求了哪個專案,在 setItems 中,我們聯絡所有請求該專案的元件,並重寫它們的資料屬性。
為了使上面的程式碼工作,我們必須向 getItem 傳遞一個對元件例項的引用,這就改變了它的函式簽名。我們也不能再使用箭頭函數了,因為否則我們就不會有正確的 this 值。
如果我們想做得更好,就必須更深入地挖掘。例如,我們如何在不顯式傳遞依賴者的情況下跟蹤它們?
Vue如何收集依賴關係
為了獲得啟發,我們可以回到Vue的響應式系統。我們之前曾看到,訪問資料屬性時,資料屬性的 getter 將使呼叫者訂閱該屬性的進一步更改。但是它怎麼知道是誰做的呼叫呢?當我們得到一個數據屬性時,它的 getter 函式沒有任何關於呼叫者是誰的輸入。Getter函式沒有輸入,它怎麼知道誰要註冊為依賴者呢?
每個資料屬性維護一個需要在Dep類中進行響應的依賴項列表。如果我們在此類中進行更深入的研究,可以看到只要在註冊依賴項時就已經在靜態目標變數中定義了依賴項。這個目標是由一個非常神祕的Watche類確定的。實際上,當資料屬性更改時,將實際通知這些觀察程式,並且它們將啟動元件的重新渲染或計算屬性的重新計算。
但是,他們又是誰?
當Vue使 data 選項可觀察時,它還會為每個計算出的屬性函式以及所有watch函式(不應與Watcher類混為一談)以及每個元件例項的render函式建立watcher。觀察者就像這些函式的伴侶。他們主要做兩件事:
- 當它們被建立時,它們會評估函式。這將觸發依賴關係的集合。
- 當他們被通知他們所依賴的一個值發生變化時,他們會重新執行他們的函式。這將最終重新計算一個計算出的屬性或重新渲染整個元件。
在觀察者呼叫其負責的函式之前,有一個重要的步驟發生了:他們將自己設定為Dep類中靜態變數的目標。這樣可以確保在訪問響應式資料屬性時將它們註冊為從屬。
追蹤誰呼叫了localStorage
我們無法完全做到這一點,因為我們無法使用Vue的內部機制。但是,我們可以使用Vue的想法,即觀察者可以在呼叫其負責的函式之前,將目標設定為靜態屬性。我們能否在呼叫 localStorage 之前設定對元件例項的引用?
如果我們假設在設定 data 選項時呼叫了 localStorage,則可以將其插入 beforeCreate 和 created 中。這兩個掛鉤在初始化data選項之前和之後都會被觸發,因此我們可以設定一個目標變數,然後清除該變數,並引用當前元件例項(我們可以在生命週期掛鉤中訪問該例項)。然後,在我們的自定義獲取器中,我們可以將該目標註冊為依賴項。
我們要做的最後一點是使這些生命週期掛鉤成為我們所有元件的一部分,我們可以通過整個專案的全域性混合來做到這一點。
// LocalStorage專案鍵與依賴它的Vue例項列表之間的對映 const storeItemSubscribers = {}; // 當前正在初始化的Vue例項 let target = undefined; const getItem = window.localStorage.getItem; localStorage.getItem = (key) => { console.info("Getting",value); // 更新相關Vue例項中的值 if (storeItemSubscribers[key]) { storeItemSubscribers[key].forEach((dep) => { if (dep.hasOwnProperty(key)) dep[key] = value; }); } // 呼叫原始函式 setItem.call(localStorage,value); }; Vue.mixin({ beforeCreate() { console.log("beforeCreate",this._uid); target = this; },created() { console.log("created",this._uid); target = undefined; } });
現在,當我們執行第一個示例時,我們將獲得一個計數器,該計數器每秒增加一個數字。
new Vue({ el: "#counter",template: `<div class="component"> <div>Counter: {{ counter }}</div> <div>Counter is {{ even ? 'even' : 'odd' }}</div> </div>` });
setInterval(() => { const counter = localStorage.getItem("counter"); localStorage.setItem("counter",1000);
我們的思想實驗結束
當我們解決了最初的問題時,請記住這主要是一個思想實驗。它缺少一些功能,例如處理已刪除的專案和未安裝的元件例項。它還具有一些限制,例如元件例項的屬性名稱需要與儲存在 localStorage 中的專案相同的名稱。就是說,主要目標是更好地瞭解Vue響應式在幕後的工作方式並充分利用這一點,因此,我希望你能從所有這些事情中受益。
到此這篇關於如何在Vue中使localStorage具有響應式的文章就介紹到這了,更多相關Vue localStorage響應式內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!