呼应式是Vue的最大特色之一。假如你不知道暗地状况,它也是最奥秘的地方之一。例如,为什么它不能用于方针和数组,不能用于诸如 localStorage 之类的其他东西?

让咱们Q – 9 & x答复这个问题,在处理这个问题时,让V_ ] T R ) : Due呼应式与 localStorage 一同运用。

假如– Y A 7 [ U #运转以下代码,则会看到计数器显现为静态值,而且不会像咱们期望的那样产生变化,这是由于setInterval在 localStorage 中更改了该值。

new Vue({
el: "#counter",
datL $ h 4 #a: () => ({
counter: localG } c Q B x I S HStorage.getItem("counter")
})J 8 : M,
computed: {
even() {
return this.counter % 2 == 0;5 % 5 r % . P
}
},
template: `<div>
<div>Counter: {{ counter }}</div>
<h z 4 T div>Counter is {{ even ? 'even' : 'odd' }}</div&Q * 5 b K 8 { _gt;
</div>`
});
// s$ ^ T Xome-other-file.js
setInterval(() => {
constY 9 K X S l - / counter = localStorage.getItem("counter");
localStorage.setItem("cr } t [ z Dounter6 . d % j v", +counter + 1);
}, 1000);

尽管Vue实例中的 counter 特点是呼应式的,但它不m F # U Q 7会由于咱们更改了它在 localStorage 中的来历而更改。

有多种处理方案,最好的也许是运用Vuex,并保持存储值与 localStD * s ` 9 oorage 同步。但假如咱们需求像本例中那样& T $ + J S e {简略的东~ k J i _ @ 6 W Y西呢?咱们要深化了解一下VueL H % ) 1 J的呼应式体系是怎样工作的。

Vue 中的呼C o G , = R E N k应式

当Vue初始化组件实例时,它将观察data选项。9 L W * {这意味着它将遍历数据中的一切特点,并运用 Object.defineProperty 将它们转换为getter/setter。经过为每个特点设置自界说设置器,Vue能够知道特点何时产生更改,而且能够告诉需求对更改做出反响的依靠者。它怎样知道哪些依靠者依靠于一个特点?经过接入getter# 6 N g * 1 Vs,它能够在核算的特点、观察者函数或烘托函数拜访; u V m数据特点时进行注册。7 F = $ ` R _

// core/instance/state.js
function initData () {
// ...
observe(data)
}
// core/observer/indO 5 Zex.js
e; 8 e export funct6 % I I ion observe (value) {
// ...
new Observer(value)
// ...f ; 4 4 G t 8 W
}
export class Observer {
// ...
cos ? * m 4 Enstructor (value) {
// ...
this.walk(value)
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(objq m ! O U O U |, keys[i])
}
}
}
e} X Q V M -xport function defineReam e q Vctive (obj, key, ...Z a 4 , T F # 3) {
const dep = new Dep()
// ...
Object.dek u 1 ) 3 q LfineProperty(obj, key, {
// ...
get() {
// ...
dep.depend()
// ...
},
set(ne8 ( u l ~ . , .wVal) {
// ...V S k a (
dep.notify()
}
})
}

所以,为什么 localStorage 不呼应?由于它不是具有特点的h n +方针

可是等一下,咱们也不能用数组界说getter和seg M otten h –r,但Vue中的数组仍然是反响式的。这是由于数组在Vue中是一种特殊状况。为了拥有呼应式的数组,Vue在后台重写了G { = W v E数组方法,并与Vue的呼{ N p y F C : 0应式体系进行了修补。

咱们能够对 localStorage 做类似的工作吗?

覆盖localStorage函数

首要尝试经过覆盖localStorage方法来修复最初的示例,以跟踪哪些组件实例恳求了localStorage项目。

// LocalStorageA O 3项目键与依靠它的Vue实例列表@ E 7 * 4 y M之间的映射。
const storI Z I k { % @eItemSubscribers = {};
const getItem = window.localStorage.getItem;q N L V 0 m ;
localStorage.getItem = (key, targo r # G 2 9 ` $et) => {
console.info("Getting", key)E l ) i Y 0 B ;
// 搜集依靠的Vue实例
if (!storeItemSubscribers[key]) storeH W e +ItemSub) U F y 3 x # cscribers[key] = [];
if (tarN k : + u - ; yget) storeItemSubscribers[key].push(target);
// 调用原始函数 
return getItem.call(localStorage, key);
};
const setItem = wi7 Z s Yndow.localStorage.setItem;
localStorage.setItem = (key, value) => {
console.info("Setting", key, value);
// 更新相关Vue实例中的值
if (storeItemSubscribers[kC N Cey]) {
storeItemSubscribers[key].forEach((dep) => {
if (dep.hasOwnProperty(key)) dep[key] = value;
});
}
// 调用原始函数 
se! F q utItem.call(loc% . i f 6 ; * ]alStorage, key, value);
};
new Vue({
el: "#counter",
data: function() {
return {
counter: localStorage.getItem("counter", this) // 咱们] } t y F = K现在需求传递“this”
}
},
computed: {
even() {
return this.counter % 2 == 0;
}
},
template: `<div>
<div>Cou. O r z E J J * xnter: {{ counter }}</div>
<div>Counter is {{ even ? 'even' : 'odd' }}</div>
</div@ $ >`
});
setInterval((k X q N o Z .) => {
const counter = localStorage.getItem("counter");
localStorage.setItem("counter", +counter + 1);
}, 1000);

在这个例子中,咱们从B x E m头界说了 getItemsetItem,以便搜集和告诉依靠 localStorage 项意S N X C E z #图组件。在新的 getItem 中,咱们注意到哪个组件恳求了哪个项目,在 setItems 中,咱们联络一切恳求该项意图组件,并重写它们的数据特点。

为了使上面的代码工作,咱们必须向 getItem 传递一个对组件实例的引证,这就改变了Z Q e它的函数签名。咱们也不能再运用箭头函数了,由F 5 {于不然咱们就不会有正确的 this 值。

假如咱们想做得更好,就必须更深化地挖掘。例如,– E @ O V j Z l B咱们怎样在不显式传递依靠者的状况下跟踪它们?

Vue怎样搜集依靠联系

为了取得启示,咱们能够回到Vue的呼应式体系。咱们之前曾看到,拜访数据特点时,数据特点的 getter 将使调用者订阅该特点的进一步更改。可是它怎样知道是谁做的调用呢?当咱们得到一个数据特点时,它的 getter 函数没有任何关于调用者是谁的输入。Getter函数没有输入,它怎样知道谁要注册为依靠者呢?

每个数据特点维护一个需求在Dep类中进行呼应的依靠项列表。假如咱们在此类中进行更深化的研究,能够看到只要在注册依靠项时就已经在静态方针变量中界说了依靠项。这个方针是由一个十分奥秘的Watche类确定的。实际上,当数据特点更改时,将实际告诉这些观察程序,而且它们将发动组件的从头烘托或H f V 5 p u核算特点的从头核算。K 7 q

可是,他们又是谁?

当Vue使 data 选项可观察时,它还会为每个核算出的特点函数以及一切watch函数(不应与Watcher类混为一谈)以及每个组件实例的render函数创建watcher。观察者就像这些函数的伴侣。他们主要做两件事:

  1. 当它们被创建时,它们会评估函数。这将触发依靠联系的调集。
  2. 当他们被告诉他们所依靠的一个值产生变化时,他们会从头运转他们的函数。这将最终从头核算一个核算出的特点或从头烘托整个组件。

在观察者调用其担任的函数之前,有一个重要的步骤产生了:他们将自己设置为Dep类中静态变量的方针– w W这样能够保证在拜访呼应式数据特点时将它们注册为从属。

追寻谁调用了localStorage

咱们无法完全做到这一点,由于咱们无法运用Vue的内部机制。可是,咱们能够运用Vue的主意,即观察u n *者能够在调用其担任的函数之前,将方针设置为静态特点。咱们能否在调用 localStorage 之前设置对组件实例的引证?

假如咱们假设在设置 data 选项时调用了 localStL h g s worage,则能够将其插入 beforeCreatecreated 中。这两个挂b B ~ x W 4钩在初始化da^ Y : I @ Sta选项之前和之后都会被触发,因而咱们能够设置一个方针变量,然后铲除该变量,并引证当时组件实例(咱们能够在生命周期挂钩中拜访该实例)。然后,在咱们的自界说获取器中,咱们能t ! S ; ? J g L够将该方针注册为依靠项。

咱们要做的最后一点是使这些生命周期挂钩成为咱们一切n ~ g 2 y ^组件的一部分,咱们能够经过整个项意图大局混合来做到这一点。

// LocalStorag# _ ? 1 / Le项目键与依靠它的Vue实例列表之间的映射
co8 5 w 2 & k M T @nst storeItemSubscribers = {};
// 当时正在初始化的Vue实例
let tarO C n : | j fget = undefined;
const getItem = window.localStorage.getItem;
localStorage@ h w R ..getItem = (key) => {
c* . e y p A Y 1 *onsole.info("Getting", key);
// 搜集依N q  Z {靠的Vue实例
if (!storeItemSubscribers[key]) storeItemSubscribers[key] = [];
if (target) storeItemSubscribers[key].push(target);
// 调用原始函数 
return getItem.call(localStorage, key);
};
c, h monst setItem = window.localStorage.setItem;
lo H = f C : E ocalStorage.setItem = (key, value) => {
console.N 2 d z Ninfo("Settd | W } X 2 E O Bing", key, value);
// 更新相关Vue实例4 ) *中的值
if (storeItemSubscribers[key]) {
storeItemSubscribers[key].forEach((dep) => {
if (dep.hasOwnProperty(key)) dep[U a 7 @ ,key] = value;
});
}
/e k 9 n # P t/ 调用原始E M 9 & g ~函数 
setItem.call(localStorage, key, value);
};
Vue.mixin({
beforeCreate() {
console.log("beforeCreate", thiY P ! ms._uid);
target = this;
},
created() {
console.log("created", this._uid);
target = undefinedH A p u Q S x;
}
});

现在,当咱们运转第一个示例时,咱们将取得一个计数器,该计数器每秒~ ! W增加一个数字。

new Vue(3 ` t q | B{
el: "#counter",
data: () => ({
counte+ 2 N 3r: lU b ; o v l y WocalStorage.getItemb # ~ c q x + H {("counte) A x vr")
}),
comput$ H N Z s 9 *ed: {
even() {
return this.counter % 2 == 0;
}
},
template: `<div class="compone] t *nt">
<div>Counter: {{ counter }}&lZ z V i (t;/div>
<div&I m 0 $ D = 9gt;Counter is {{ even ? 'even' : 'odd' } G . % N}</div>
</div>`
});
setInterval(() => {
const counter = localStorage.getItem("counter");
localStorage.setItem("counter", +counter + 1);
}, 1000);

咱们的思维试验结束

当咱们处理了最初的问题时,请记住这主要是一个思维试验。它缺少一些功能,例如处理已删除的项目和未装置的组件实例。它K g : $ e还具有一些限制,例如组件实例的特点称号需求与存T P V y d 9 0 l储在 localStorage 中的项目相同的称z s } : u : E 2 x号。就是说,主要方针是更好地了解Vue呼应式在暗地的工作方式并充分利用这一点,因而,我期望你能从一切这些工作中获益。


来历:c{ f b , & / @ =ss-tricks.com,作者:roberto,翻译:大众号《前端全栈A I 1 : / T K开发者》

思想实验:如何在Vue中使localStorage具有响应式?