响应式是 Vue 的最大特色之一。如果你不晓得幕后状况,它也是最神秘的中央之一。例如,为什么它不能用于对象和数组,而不能用于诸如 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 实例中的 counter
属性是响应式的,但它不会因为咱们更改了它在 localStorage
中的起源而更改。
有多种解决方案,最好的兴许是应用 Vuex,并放弃存储值与 localStorage
同步。但如果咱们须要像本例中那样简略的货色呢?咱们要深刻理解一下 Vue 的响应式零碎是如何工作的。
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, key, {
// ...
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", 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 如何收集依赖关系
为了取得启发,咱们能够回到 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", 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 响应式在幕后的工作形式并充分利用这一点,因而,我心愿你能从所有这些事件中受害。