思想实验:Vue中如何让localStorage响应式?
响应式是Vue最大的特性之一。如果您不知道幕后发生了什么,它也是最神秘的去处之一。例如,为什么它不适用于对象和数组,但不适用于localStorage等其他东西?让我们回答这个问题,并在解决这个问题的同时让Vue与localStorage响应。如果您运行以下代码,您会看到计数器显示为静态值并且不会像我们预期的那样更改,因为setInterval更改了localStorage中的值。newVue({el:"#counter",data:()=>({counter:localStorage.getItem("counter")}),computed:{even(){returnthis.counter%2==0;}},template:`
Counter:{{counter}}
Counteris{{even?'even':'odd'}}
`});//some-other-file.jssetInterval(()=>{constcounter=localStorage.getItem("counter");localStorage.setItem("counter",+counter+1);},1000);虽然Vue实例中的counter属性是响应式的,但它并没有改变,因为我们改变了它在localStorage中的来源。有多种解决方案,最好的可能是使用Vuex,并保持存储值与localStorage同步。但是,如果我们需要像本例中一样简单的东西怎么办?我们将深入探讨Vue的反应系统是如何工作的。Vue中的Reactive当Vue初始化组件实例时,它会观察数据选项。这意味着它将遍历数据中的所有属性并使用Object.defineProperty将它们转换为getter/setter。通过为每个属性设置自定义设置器,Vue可以知道属性何时发生更改,并可以通知需要对更改做出反应的依赖项。它如何知道哪些依赖项依赖于属性?通过访问getter,可以在计算属性、观察者函数或渲染函数访问数据属性时进行注册。//core/instance/state.jsfunctioninitData(){//...observe(data)}//core/observer/index.jsexportfunctionobserve(value){//...newObserver(value)//...}exportclassObserver{//...constructor(value){//...this.walk(value)}walk(obj){constkeys=Object.keys(obj)for(leti=0;i
{console.info("Getting",key);//收集依赖的Vue实例if(!storeItemSubscribers[key])storeItemSubscribers[key]=[];如果(目标)storeItemSubscribers[key].push(target);//调用原函数returngetItem.call(localStorage,key);};constsetItem=window.localStorage.setItem;localStorage.setItem=(key,value)=>{console.info("Setting",key,value);//更新相关Vue实例中的值}//调用原函数setItem.call(localStorage,key,value);};newVue({el:"#counter",data:function(){return{counter:localStorage.getItem("counter",this)//我们现在需要传递"this"}},computed:{even(){returnthis.counter%2==0;}},template:`Counter:{{counter}}
计数器是{{偶数?'偶数':'奇数'}}
`});setInterval(()=>{constcounter=localStorage.getItem("counter");localStorage.setItem("counter",+counter+1);},1000);在这个例子中,我们重新定义了getItem和setItem来收集和通知依赖于localStorage项的组件。在新的getItem中,我们注意到为了查看哪个组件请求了哪个项目,在setItems中我们联系了所有请求该项目的组件并覆盖它们的数据属性。为了让上面的代码工作,我们必须向getItem传递一个对组件实例的引用,这会改变它的函数签名。我们也不能再使用箭头函数了,否则我们将无法获得正确的this值。如果我们想做得更好,我们必须更深入地挖掘。例如,我们如何在不显式传递它们的情况下跟踪依赖关系?Vue如何收集依赖为了获得灵感,我们可以回到Vue的响应式系统。我们之前看到,当访问数据属性时,数据属性的getter将订阅调用者对该属性的进一步更改。但是它怎么知道是谁打的电话呢?当我们获取数据属性时,它的getter函数没有任何关于调用者是谁的输入。Getter函数没有输入,它怎么知道要将谁注册为依赖呢?每个数据属性在Dep类中维护一个需要响应的依赖项列表。如果我们更深入地研究这个类,我们可以看到无论何时注册,依赖项都已经在静态目标变量中定义。这个目标是由一个非常神秘的Watche类决定的。事实上,当数据属性发生变化时,这些观察者实际上会收到通知,并且它们将启动组件的重新渲染或计算属性的重新计算。但他们是谁?当Vue使数据选项可观察时,它还会为每个计算属性函数以及所有监视函数(不应与Watcher类混淆)创建观察器,并为每个组件实例创建渲染函数。观察者就像这些功能的伙伴。他们主要做两件事:他们在创建函数时对其进行评估。这将触发依赖项的收集。当他们被告知他们所依赖的值之一已经改变时,他们重新运行他们的功能。这将最终重新计算计算属性或重新渲染整个组件。在观察者调用他们负责的函数之前,发生了一个重要的步骤:他们将自己设置为Dep类中静态变量的目标。这确保了反应性数据属性在被访问时被注册为依赖项。跟踪谁调用了localStorage我们不能完全做到这一点,因为我们不能使用Vue的内部机制。但是,我们可以使用Vue的想法,即观察者可以在调用其负责的函数之前将目标设置为静态属性。我们可以在调用localStorage之前设置对组件实例的引用吗?如果我们假设在设置数据选项时调用localStorage,则可以将其插入beforeCreate并创建。初始化数据选项之前和之后都会触发两个挂钩,因此我们可以设置一个目标变量,然后使用对当前组件实例(我们可以在生命周期挂钩中访问)的引用来清除该变量。然后,在我们的自定义getter中,我们可以将该目标注册为依赖项。我们最不想做的就是让这些生命周期钩子成为我们所有组件的一部分,我们可以通过整个项目的全局混合来做到这一点。//LocalStorageitemkey和依赖它的Vue实例列表之间的映射conststoreItemSubscribers={};//当前正在初始化的Vue实例lettarget=undefined;constgetItem=window.localStorage.getItem;localStorage.getItem=(key)=>{console.info("Getting",key);//收集依赖的Vue实例if(!storeItemSubscribers[key])storeItemSubscribers[key]=[];如果(目标)storeItemSubscribers[key].push(target);//调用原函数returngetItem.call(localStorage,key);};constsetItem=window.localStorage.setItem;localStorage.setItem=(key,value)=>{console.info("Setting",key,value);//更新相关Vue实例中的值}//调用原函数setItem.call(localStorage,key,value);};Vue.mixin({beforeCreate(){console.log("beforeCreate",this._uid);target=this;},created(){console.log("created",this._uid);target=undefined;}});现在,当我们运行第一个示例时,我们将得到一个每秒递增1的计数器numbernewVue({el:"#counter",data:()=>({counter:localStorage.getItem("counter")}),computed:{even(){returnthis.counter%2==0;}},template:`
Counter:{{counter}}
Counteris{{even?'even':'odd'}}
`});setInterval(()=>{constcounter=localStorage.getItem("counter");localStorage.setItem("counter",+counter+1);},1000);我们的想法实验结束当我们解决了最初的问题时,请记住这主要是一个思想实验。它缺少一些功能,例如处理已删除的项目和未安装的组件实例。它也有一些限制,例如组件实例的属性名称需要与localStorage中存储的项目同名。也就是说,主要目标是更好地了解Vue反应性如何在幕后工作并充分利用它,所以我希望你能从所有这些事情中受益。