官方仓库: championswimmer/vuex-persist: A Vuex plugin to persist the store. (Fully Typescript enabled)

VuexPersistence

原理就一句话:通过 vuex.subscribe 订阅一个函数,在每次 commit mutation 的时候执 行去更新 persist store。

构造函数:

1
2
3
function VuexPersistence(options) {
  // ...
}

存储源

初始化,并且决定用哪种方式存储数据,主要有三种方式:

  1. 用户定义存到哪里 options.storage

  2. H5 的 storage api localStorage 本地存储

  3. MockStorage 库内部的一个封装,内存存储方案

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function VuexPersistence(options = {}) {
  // 创建一个队列管理器实例
  const _mutex = new SimplePromiseQueue()

  this.key = options.key ?? 'vuex'

  this.subscribed = false
  this.mergeOption = options.mergeOption || 'replaceArrays'


  let localStorateLitmus = true
  // 支不支持 H5 storage api
  try {
    window.localStorage.getItem('')
  } catch (err) {
    localStorageLitmus = false
  }

  // 几种 storage 存储机制
  // 1. 用户定义的
  // 2. H5 storage api localStorage
  // 3. mock storage 内存里的一个全局变量
  // 4. 都不是应该报错
  if (options.storage) {
    this.storage = options.storage
  }
}

[save|restore]State

这两个函数直接操作 store 存储源。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 清空数据
this.restoreState = (key, storage) => {
  const value = storage.getItem(key);
  if (typeof value === "string") {
    return JSON.parse(value || "{}");
  } else {
    return value || {};
  }
};

this.saveState = (key, state, storage) => {
  storage.setItem(key, JSON.stringify(state));
};

plugin(store)

给 vuex 使用的一个函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const vuexLocal = new VuexPersistence({
  storage: window.localStorage,
  modules: ["permission", "film", "settings"],
});

createStore({
  // ...
  plugins: [vuexLocal.plugin],
});

// vuex Store: constructor > 安装插件
// apply plugins
plugins.forEach((plugin) => plugin(this));

实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//  vuex 安装接口
this.plugin = (store) => {
  const savedState = this.restoreState(this.key, this.storage);

  // TODO strict mode
  store.replaceState(merge(store.state, savedState || {}));

  this.subscriber(store)((mutation, state) => {
    if (this.filter(mutation)) {
      this.saveState(this.key, this.reducer(state), this.storage);
    }
  });

  this.subscribed = true;
};

// vuex replaceState,直接理解为替换 state 就行了
function replaceState(state) {
  this._withCommit(() => {
    this._state.data = state;
  });
}

// 订阅啥?
this.subscriber(store)((mutation: MutationPayload, state: S) => {
  if (this.filter(mutation)) {
    this.saveState(this.key, this.reducer(state), this.storage);
  }
});

subscriber(store)

1
2
3
const subscriber = (store: Store<S>) => (
  handler: (mutation: MutationPayload, state: S) => any
) => store.subscribe(handler);

vuex subscribe:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function subscribe(fn, options) {
  return genericSubscribe(fn, this._subscribers, options);
}

function genericSubscribe(fn, subs, options) {
  if (subs.indexOf(fn) < 0) {
    options && options.prepend ? subs.unshift(fn) : subs.push(fn);
  }
  return () => {
    const i = subs.indexOf(fn);
    if (i > -1) {
      subs.splice(i, 1);
    }
  };
}

// commit
function commit(_type, _payload, _options) {
  // ... 省略 commit mutation 操作

  // 注意看这里,说明每次 commit mutation 操作
  // 更新 state 的时候都会执行
  this._subscribers
    .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
    .forEach((sub) => sub(mutation, this.state));

  // ... warn
}

有了上面的 vuex 源码部分,再来看这 vuex-persist 的 subscribe 干了啥

1
2
3
4
5
6
// 订阅啥?
this.subscriber(store)((mutation: MutationPayload, state: S) => {
  if (this.filter(mutation)) {
    this.saveState(this.key, this.reducer(state), this.storage);
  }
});

这里等于是给 vuex this._subscribers 注入了一个函数,这个函数会在每次 vuex commit mutation 的时候去执行来更新 store 里面的 state。

1
2
3
this.saveState = (key, state, storage) => {
  storage.setItem(key, JSON.stringify(state));
};

modules

支持多模块 - 对应了 vuex 的 modules。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
this.reducer =
  options.reducer != null
    ? options.reducer
    : options.modules == null
    ? (state: S) => state
    : (state: any) =>
        (options!.modules as string[]).reduce(
          (a, i) => merge(a, { [i]: state[i] }, this.mergeOption),
          {
            /* start empty accumulator*/
          }
        );

// 使用:当 commit mutation 状态更新,根据是不是有 modules
// 调用 recuder 决定如何存储,比如: modules [a, b]
// 存储的时候 localStorage = { a: xxx, b: xxx }
// 如果没有 modules ,默认用的是 key : 'vuex'
// 所以 localStorage = { vuex: xxx }
this.subscriber(store)((mutation: MutationPayload, state: S) => {
  if (this.filter(mutation)) {
    this._mutex.enqueue(
      this.saveState(this.key, this.reducer(state), this.storage) as Promise<
        void
      >
    );
  }
});

queue 队列

一个简单的队列刷新类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function SimplePromiseQueue() {
  this._queue = []
  this._flushing = false
}

const SPQ = SimplePromiseQueue
const SPGP = SimplePromiseQueue.prototype

// 入列,如果没有任务正在执行,立即 flush
SPGP.enqueue = function enqueue(promise) {
  this._queue.push(promise)
  if (!this._flushing) return this.flushQueue()
  return Promise.resolve()
}

SPGP.flushQueue = function flushQueue() {
  this._flushing = true

  const chain = () => {
    const nextTask = this._queue.shift() // 先进先出
    if (nextTask) {
      // 递归,flush 所有任务
      return nextTask.then(chain)
    }

    this._flushing = false
  }
  return Promise.resolve(chain())
}

任务入列的时候,会检测队列是不是正在刷新,如果是只执行入列操作,这里返回一个 Promise.resolve() 方便后面任务的依次进行。

flushQueue() 将出列行为封装成一个函数,来达到所有任务按入列顺序执行。