「译」在Vue中如何使localstorage变为响应式

原文做者:Hunor Márton Borbélycss

发布时间:Jun 24, 2020html

原文地址:How to Make localStorage Reactive in Vuevue

响应式是Vue的最伟大的特性之一,若是你不知道它在幕后作了什么,那么它对于你来讲会显得更加神秘。就像为何它只适用于对象和数组,而不适用于其余东西呢,好比咱们今天所说的localstoragereact

接下来,让咱们一块儿来回答这个问题。同时,也让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并保持storelocalstorage中的数据同步。 可是若是咱们须要更简单的东西(例如本例中的东西)怎么办? 那就须要咱们深刻研究Vue的响应式系统是如何工做的。数组

Vue 中的响应式系统

当Vue初始化组件实例时,它会监听data项的变化。这意味着它将遍历 data 中的全部属性,并使用Object.defineProperty将它们转换为 getter/setter,经过为每一个属性设置一个自定义的setter,Vue就能够监测到每一个属性的变化,而且通知那些须要响应变化的依赖项。它是如何将依赖项和属性之间进行建联的呢? 经过利用getters进行注册依赖,当触发 computedwatchrender 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不是响应式的呢? 由于他不是一个具有属性的对象。

可是,咱们也不能用数组定义gettersetter,那为何Vue中的数组仍然是响应式的呢? 这是由于数组是Vue中的特例。为了具有响应式数组,Vue在后台重写了数组的方法,并将它们与vue的响应式系统一块儿打了个补丁

那咱们能够在localstorage上作些相似的事情吗?

重写 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);
复制代码

在此示例中,咱们从新定义getItemsetItem以便收集和通知依赖于localStorage item的组件。在新版本getItem,咱们会记录哪一个组件请求哪一个item,而在setItems中,咱们访问全部请求该项目的组件并重写其data prop

为了使上面的代码起做用,咱们必须将对组件实例的引用传递给getItem并更改其函数签名。咱们也不能再使用箭头功能,由于不然咱们将没法拿到正确的的this值。

若是咱们想作得更好,就必须更深刻地挖掘。例如,咱们如何在不显式传递依赖者的状况下跟踪它们?

Vue如何收集依赖关系

为了得到启发,咱们能够回到Vue的响应式系统。先前咱们看到,访问数据属性时,数据属性的getter将使调用者订阅该属性的进一步更改。可是如何知道是谁调用的呢?当咱们获得一个data时,它的getter函数没有任何关于调用者是谁的输入。Getter函数没有输入。它如何知道将谁注册为依赖项的呢?

每一个data属性维护一个须要在Dep类中进行响应的依赖项列表。若是咱们深刻研究此类,咱们能够看到,只要注册了依赖项,就已经在static target变量中定义了依赖项。这个目标是由一个很是神秘的Watcher设置的。实际上,当数据属性更改时,将实际上通知这些观察程序,而且它们将启动组件的从新呈现或计算属性的从新计算。

当Vue使该data选项observable时,它还会为每一个计算属性函数以及全部watch函数(不该与Watcher类混淆)以及每一个组件实例的render函数建立监视者。观察者就像这些功能的伴侣。他们主要作两件事:

  • 它们在建立时会评估函数。这将触发依赖项的收集。
  • **当通知他们所依赖的值已更改时,他们将从新运行其功能。**这最终将从新计算一个计算属性或从新渲染整个组件。

在观察者调用其负责的功能以前,有一个重要的步骤发生了:他们将本身设置为Dep类中静态变量的目标。这样能够确保在访问反应性数据属性时将它们注册为依赖。

持续追踪谁调用了localstorage

咱们没法彻底作到这一点,由于咱们没法使用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);
复制代码

codepen上的效果预览

结束语

当咱们解决了最初的问题时,请记住这主要是一个思想实验。可是它还缺乏一些功能,例如处理已删除的item和已卸载的组件实例。它还具备一些限制,例如组件实例的属性名称须要与localStorage中存储的item名称相同。也就是说,咱们的主要目标是更好地了解Vue响应式系统在幕后的工做方式,并从中得到最大的收益。但愿你也能有所收益。

若是想要进行数据持久化,可使用vue-persist。 若是您持续监听localStorage是否有所更改,则监听 StorageEvent 是一个更好的主意。