原文做者:Hunor Márton Borbélycss
發佈時間:Jun 24, 2020html
響應式是Vue的最偉大的特性之一,若是你不知道它在幕後作了什麼,那麼它對於你來講會顯得更加神祕。就像爲何它只適用於對象和數組,而不適用於其餘東西呢,好比咱們今天所說的localstorage
。react
接下來,讓咱們一塊兒來回答這個問題。同時,也讓localstorage
變爲響應式的。git
若是你運行下面代碼,則會看到counter
的顯示爲靜態值,不會由於在setInterval
中修改了localstorage
中的值而做用到頁面中:github
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);
複製代碼
如何使localstorage
中的值發生變動時,同步更新頁面中counter
的數據呢?vuex
對此咱們有多重解決方案,最經常使用的方案是使用Vuex
並保持store
和localstorage
中的數據同步。 可是若是咱們須要更簡單的東西(例如本例中的東西)怎麼辦? 那就須要咱們深刻研究Vue的響應式系統是如何工做的。數組
當Vue初始化組件實例時,它會監聽data項的變化。這意味着它將遍歷 data 中的全部屬性,並使用Object.defineProperty將它們轉換爲 getter/setter
,經過爲每一個屬性設置一個自定義的setter
,Vue就能夠監測到每一個屬性的變化,而且通知那些須要響應變化的依賴項。它是如何將依賴項和屬性之間進行建聯的呢? 經過利用getters
進行註冊依賴,當觸發 computed
、watch
和render function
等行爲時。markdown
以上流程簡寫爲代碼的話,以下:app
// 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, key, {
// ...
get() {
// ...
dep.depend()
// ...
},
set(newVal) {
// ...
dep.notify()
}
})
}
複製代碼
那麼, 爲什麼localstorage
不是響應式的呢? 由於他不是一個具有屬性的對象。
可是,咱們也不能用數組定義getter
和setter
,那爲何Vue中的數組仍然是響應式的呢? 這是由於數組是Vue中的特例。爲了具有響應式數組,Vue在後臺重寫了數組的方法,並將它們與vue的響應式系統一塊兒打了個補丁。
那咱們能夠在localstorage
上作些相似的事情嗎?
首先嚐試經過重寫localStorage的方法來修復上面的demo,以追蹤那些組件實例請求了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", key, value);
// 更新依賴vue實例中的值
if (storeItemSubscribers[key]) {
storeItemSubscribers[key].forEach((dep) => {
if (dep.hasOwnProperty(key)) dep[key] = value;
});
}
// 調用原始方法
setItem.call(localStorage, key, value);
};
複製代碼
new Vue({
el: "#counter",
data: function () {
return {
counter: localStorage.getItem("counter", this) // We need to pass 'this' for now
}
},
computed: {
even() {
return this.counter % 2 == 0;
}
},
template: `<div> <div>Counter: {{ counter }}</div> <div>Counter is {{ even ? 'even' : 'odd' }}</div> </div>`
});
複製代碼
setInterval(() => {
const counter = localStorage.getItem("counter");
localStorage.setItem("counter", +counter + 1);
}, 1000);
複製代碼
在此示例中,咱們從新定義getItem
,setItem
以便收集和通知依賴於localStorage item
的組件。在新版本getItem
,咱們會記錄哪一個組件請求哪一個item
,而在setItems
中,咱們訪問全部請求該項目的組件並重寫其data prop
。
爲了使上面的代碼起做用,咱們必須將對組件實例的引用傳遞給getItem
並更改其函數簽名。咱們也不能再使用箭頭功能,由於不然咱們將沒法拿到正確的的this
值。
若是咱們想作得更好,就必須更深刻地挖掘。例如,咱們如何在不顯式傳遞依賴者的狀況下跟蹤它們?
爲了得到啓發,咱們能夠回到Vue的響應式系統。先前咱們看到,訪問數據屬性時,數據屬性的getter將使調用者訂閱該屬性的進一步更改。可是如何知道是誰調用的呢?當咱們獲得一個data時,它的getter函數沒有任何關於調用者是誰的輸入。Getter函數沒有輸入。它如何知道將誰註冊爲依賴項的呢?
每一個data屬性維護一個須要在Dep類中進行響應的依賴項列表。若是咱們深刻研究此類,咱們能夠看到,只要註冊了依賴項,就已經在static target變量中定義了依賴項。這個目標是由一個很是神祕的Watcher設置的。實際上,當數據屬性更改時,將實際上通知這些觀察程序,而且它們將啓動組件的從新呈現或計算屬性的從新計算。
當Vue使該data選項observable時,它還會爲每一個計算屬性函數以及全部watch函數(不該與Watcher類混淆)以及每一個組件實例的render函數建立監視者。觀察者就像這些功能的伴侶。他們主要作兩件事:
在觀察者調用其負責的功能以前,有一個重要的步驟發生了:他們將本身設置爲Dep類中靜態變量的目標。這樣能夠確保在訪問反應性數據屬性時將它們註冊爲依賴。
咱們沒法徹底作到這一點,由於咱們沒法使用Vue的內部機制。可是,咱們可使用Vue 的思想,即觀察者能夠在調用其負責的功能以前,將目標設置爲靜態屬性。在localStorage調用以前,咱們能夠設置對組件實例的引用嗎?
若是咱們假設localStorage在設置data選項時調用了該方法,那麼咱們能夠將其鏈接到beforeCreate和中created。這兩個鉤子函數在初始化該data選項以前和以後都會被觸發,所以咱們能夠設置一個目標變量,而後清除該變量,並引用當前組件實例(咱們能夠在生命週期鉤子函數中訪問該實例)。而後,在咱們的自定義getter
中,咱們能夠將該目標註冊爲依賴項。
咱們要作的最後一點是使這些生命週期鉤子成爲咱們全部組件的一部分。咱們能夠經過整個項目的全局mixins
來作到這一點。
// A map between localStorage item keys and a list of Vue instances that depend on it
const storeItemSubscribers = {};
// The Vue instance that is currently being initialised
let target = undefined;
const getItem = window.localStorage.getItem;
localStorage.getItem = (key) => {
console.info("Getting", key);
// Collect dependent Vue instance
if (!storeItemSubscribers[key]) storeItemSubscribers[key] = [];
if (target) storeItemSubscribers[key].push(target);
// Call the original function
return getItem.call(localStorage, key);
};
const setItem = window.localStorage.setItem;
localStorage.setItem = (key, value) => {
console.info("Setting", key, value);
// Update the value in the dependent Vue instances
if (storeItemSubscribers[key]) {
storeItemSubscribers[key].forEach((dep) => {
if (dep.hasOwnProperty(key)) dep[key] = value;
});
}
// Call the original function
setItem.call(localStorage, key, value);
};
Vue.mixin({
beforeCreate() {
console.log("beforeCreate", this._uid);
target = this;
},
created() {
console.log("created", this._uid);
target = undefined;
}
});
複製代碼
如今,當咱們運行初始示例時,咱們將得到一個計數器,該計數器每秒增長一個數字。
new Vue({
el: "#counter",
data: () => ({
counter: localStorage.getItem("counter")
}),
computed: {
even() {
return this.counter % 2 == 0;
}
},
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", +counter + 1);
}, 1000);
複製代碼
當咱們解決了最初的問題時,請記住這主要是一個思想實驗。可是它還缺乏一些功能,例如處理已刪除的item
和已卸載的組件實例。它還具備一些限制,例如組件實例的屬性名稱須要與localStorage
中存儲的item
名稱相同。也就是說,咱們的主要目標是更好地瞭解Vue響應式系統在幕後的工做方式,並從中得到最大的收益。但願你也能有所收益。
若是想要進行數據持久化,可使用vue-persist。 若是您持續監聽localStorage是否有所更改,則監聽 StorageEvent 是一個更好的主意。