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

响应式是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 中的响应式

当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方法来修复最初的示例,以跟踪哪些组件实例请求了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);
复制代码

在这个例子中,咱们从新定义了 getItemsetItem,以便收集和通知依赖 localStorage 项目的组件。在新的 getItem 中,咱们注意到哪一个组件请求了哪一个项目,在 setItems 中,咱们联系全部请求该项目的组件,并重写它们的数据属性。

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

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

Vue如何收集依赖关系

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

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

可是,他们又是谁?

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

  1. 当它们被建立时,它们会评估函数。这将触发依赖关系的集合。
  2. 当他们被通知他们所依赖的一个值发生变化时,他们会从新运行他们的函数。这将最终从新计算一个计算出的属性或从新渲染整个组件。

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

追踪谁调用了localStorage

咱们没法彻底作到这一点,由于咱们没法使用Vue的内部机制。可是,咱们可使用Vue的想法,即观察者能够在调用其负责的函数以前,将目标设置为静态属性。咱们可否在调用 localStorage 以前设置对组件实例的引用?

若是咱们假设在设置 data 选项时调用了 localStorage,则能够将其插入 beforeCreatecreated 中。这两个挂钩在初始化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,翻译:公众号《前端全栈开发者》

相关文章
相关标签/搜索