響應式是Vue的最大特點之一。若是你不知道幕後狀況,它也是最神祕的地方之一。例如,爲何它不能用於對象和數組,不能用於諸如 localStorage
之類的其餘東西?javascript
讓咱們回答這個問題,在解決這個問題時,讓Vue響應式與 localStorage
一塊兒使用。css
若是運行如下代碼,則會看到計數器顯示爲靜態值,而且不會像咱們指望的那樣發生變化,這是由於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實例中的 counter
屬性是響應式的,但它不會由於咱們更改了它在 localStorage
中的來源而更改。vue
有多種解決方案,最好的也許是使用Vuex,並保持存儲值與 localStorage
同步。但若是咱們須要像本例中那樣簡單的東西呢?咱們要深刻了解一下Vue的響應式系統是如何工做的。java
當Vue初始化組件實例時,它將觀察data選項。這意味着它將遍歷數據中的全部屬性,並使用 Object.defineProperty
將它們轉換爲getter/setter。經過爲每一個屬性設置自定義設置器,Vue能夠知道屬性什麼時候發生更改,而且能夠通知須要對更改作出反應的依賴者。它如何知道哪些依賴者依賴於一個屬性?經過接入getters,它能夠在計算的屬性、觀察者函數或渲染函數訪問數據屬性時進行註冊。react
// 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
作相似的事情嗎?ui
首先嚐試經過覆蓋localStorage方法來修復最初的示例,以跟蹤哪些組件實例請求了localStorage項目。this
// 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) // 咱們如今須要傳遞「this」
}
},
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
項目的組件。在新的 getItem
中,咱們注意到哪一個組件請求了哪一個項目,在 setItems
中,咱們聯繫全部請求該項目的組件,並重寫它們的數據屬性。
爲了使上面的代碼工做,咱們必須向 getItem
傳遞一個對組件實例的引用,這就改變了它的函數簽名。咱們也不能再使用箭頭函數了,由於不然咱們就不會有正確的 this
值。
若是咱們想作得更好,就必須更深刻地挖掘。例如,咱們如何在不顯式傳遞依賴者的狀況下跟蹤它們?
爲了得到啓發,咱們能夠回到Vue的響應式系統。咱們以前曾看到,訪問數據屬性時,數據屬性的 getter
將使調用者訂閱該屬性的進一步更改。可是它怎麼知道是誰作的調用呢?當咱們獲得一個數據屬性時,它的 getter
函數沒有任何關於調用者是誰的輸入。Getter函數沒有輸入,它怎麼知道誰要註冊爲依賴者呢?
每一個數據屬性維護一個須要在Dep類中進行響應的依賴項列表。若是咱們在此類中進行更深刻的研究,能夠看到只要在註冊依賴項時就已經在靜態目標變量中定義了依賴項。這個目標是由一個很是神祕的Watche類肯定的。實際上,當數據屬性更改時,將實際通知這些觀察程序,而且它們將啓動組件的從新渲染或計算屬性的從新計算。
可是,他們又是誰?
當Vue使 data
選項可觀察時,它還會爲每一個計算出的屬性函數以及全部watch函數(不該與Watcher類混爲一談)以及每一個組件實例的render函數建立watcher。觀察者就像這些函數的伴侶。他們主要作兩件事:
在觀察者調用其負責的函數以前,有一個重要的步驟發生了:他們將本身設置爲Dep類中靜態變量的目標。這樣能夠確保在訪問響應式數據屬性時將它們註冊爲從屬。
咱們沒法徹底作到這一點,由於咱們沒法使用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", 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);
};
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);
複製代碼
當咱們解決了最初的問題時,請記住這主要是一個思想實驗。它缺乏一些功能,例如處理已刪除的項目和未安裝的組件實例。它還具備一些限制,例如組件實例的屬性名稱須要與存儲在 localStorage
中的項目相同的名稱。就是說,主要目標是更好地瞭解Vue響應式在幕後的工做方式並充分利用這一點,所以,我但願你能從全部這些事情中受益。
來源:css-tricks.com,做者:roberto,翻譯:公衆號《前端全棧開發者》