实现一个简易版的vuex持久化工具

  • 2019 年 10 月 3 日
  • 笔记

背景

最近用uni-app开发小程序项目时,部分需要持久化的内容直接使用vue中的持久化插件貌似不太行,所以想着自己实现一下类似vuex-persistedstate插件的功能,想着功能不多,代码量应该也不会很大

初步思路

首先想到的实现方式自然是vue的watcher模式。对需要持久化的内容进行劫持,当内容改变时,执行持久化的方法。
先弄个dep和observer,直接observer需要持久化的state,并传入get和set时的回调:

function dep(obj, key, options) {      let data = obj[key]      Object.defineProperty(obj, key, {          configurable: true,          get() {              options.get()              return data          },          set(val) {              if (val === data) return              data = val              if(getType(data)==='object') observer(data)              options.set()          }      })  }  function observer(obj, options) {      if (getType(obj) !== 'object') throw ('参数需为object')      Object.keys(obj).forEach(key => {          dep(obj, key, options)          if(getType(obj[key]) === 'object') {              observer(obj[key], options)          }      })  }

然而很快就发现问题,比如将a={b:{c:d:{e:1}}}存入storage,操作一般是xxstorage(‘a’,a),接下来无论是改了a.b还是a.b.c或是a.b.c.d.e,都需要重新执行xxstorage(‘a’,a),即当某一项的后代节点变动时,我们需要沿着变动的后代节点找到它的根节点,然后将根节点下的内容全部替换成新的。
接下来的第一个问题就是,如何找到变动节点的祖先节点。

state树的重新构造

方案一:沿着state向下找到变动的节点,根据寻找路径确认变动项的根节点,此方案复杂度太高。
方案二:在observer的时候,对state中的每一项增添一个指向父节点的指针,在后代节点变动时,可以沿着指向父节点的指针找到相应的根节点,此方案可行。
为避免新增的指针被遍历到,决定采用Symbol标记指针,于是dep部分变动如下:

const pointerParent = Symbol('parent')  const poniterKey = Symbol('key')  function dep(obj, key, options) {      let data = obj[key]      if (getType(data)==='object') {          data[pointerParent] = obj          data[poniterKey] = key      }      Object.defineProperty(obj, key, {          configurable: true,          get() {              ...          },          set(val) {              if (val === data) return              data = val              if(getType(data)==='object') {                  data[pointerParent] = obj                  data[poniterKey] = key                  observer(data)              }              ...          }      })  }

再加个可以找到根节点的方法,就可以改变对应storage项了

function getStoragePath(obj, key) {      let storagePath = [key]      while (obj) {          if (obj[poniterKey]) {              key = obj[poniterKey]              storagePath.unshift(key)          }          obj = obj[pointerParent]      }      // storagePath[0]就是根节点,storagePath记录了从根节点到变动节点的路径      return storagePath  }

但是问题又来了,object是可以实现自动持久化了,数组用push、pop这些方法操作时,数组的地址是没有变动的,defineProperty根本监测不到这种地址没变的情况(可惜Proxy兼容性太差,小程序中安卓直接不支持)。当然,每次操作数组时,对数组重新赋值可以解决此问题,但是用起来太不方便了。

改变数组时的双向绑定

数组的问题,解决方式一样是参照vue源码的处理,重写数组的’push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’, ‘sort’, ‘reverse’方法
数组用这7种方法操作数组的时候,手动触发set中部分,更新storage内容

添加防抖

vuex持久化时,容易遇到频繁操作state的情况,如果一直更新storage,性能太差

实现代码

最后代码如下:
tool.js:

/*  持久化相关内容  */  // 重写的Array方法  const funcArr = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']  const typeArr = ['object', 'array']  // 各级指向父节点和及父节点名字的项  const pointerParent = Symbol('parent')  const poniterKey = Symbol('key')    function setCallBack(obj, key, options) {      if (options && options.set) {          if (getType(options.set) !== 'function') throw ('options.set需为function')          options.set(obj, key)      }  }    function rewriteArrFunc(arr, options) {      if (getType(arr) !== 'array') throw ('参数需为array')      funcArr.forEach(key => {          arr[key] = function(...args) {              this.__proto__[key].apply(this, args)              setCallBack(this[pointerParent], this[poniterKey], options)          }      })  }    function dep(obj, key, options) {      let data = obj[key]      if (typeArr.includes(getType(data))) {          data[pointerParent] = obj          data[poniterKey] = key      }      Object.defineProperty(obj, key, {          configurable: true,          get() {              if (options && options.get) {                  options.get(obj, key)              }              return data          },          set(val) {              if (val === data) return              data = val              let index = typeArr.indexOf(getType(data))              if (index >= 0) {                  data[pointerParent] = obj                  data[poniterKey] = key                  if (index) {                      rewriteArrFunc(data, options)                  } else {                      observer(data, options)                  }              }              setCallBack(obj, key, options)          }      })  }    function observer(obj, options) {      if (getType(obj) !== 'object') throw ('参数需为object')      let index      Object.keys(obj).forEach(key => {          dep(obj, key, options)          index = typeArr.indexOf(getType(obj[key]))          if (index < 0) return          if (index) {              rewriteArrFunc(obj[key], options)          } else {              observer(obj[key], options)          }      })  }    function getStoragePath(obj, key) {      let storagePath = [key]      while (obj) {          if (obj[poniterKey]) {              key = obj[poniterKey]              storagePath.unshift(key)          }          obj = obj[pointerParent]      }      return storagePath  }    function debounceStorage(state, fn, delay) {      if(getType(fn) !== 'function') return null      let updateItems = new Set()      let timer = null      return function setToStorage(obj, key) {          let changeKey = getStoragePath(obj, key)[0]          updateItems.add(changeKey)          clearTimeout(timer)          timer = setTimeout(() => {              try {                  updateItems.forEach(key => {                      fn.call(this, key, state[key])                  })                  updateItems.clear()              } catch(e) {                  console.error(`persistent.js中state内容持久化失败,错误位于[${changeKey}]参数中的[${key}]项`)              }          }, delay)      }  }  export function persistedState({state, setItem, getItem, setDelay=0}) {      if(getType(getItem) === 'function') {          // 初始化时将storage中的内容填充到state          try{              Object.keys(state).forEach(key => {                  if(state[key] !== undefined)                      state[key] = getItem(key)              })          } catch(e) {              console.error('初始化过程中获取持久化参数失败')          }      } else {          console.warn('getItem不是一个function,初始化时获取持久化内容的功能不可用')      }      observer(state, {          set: debounceStorage(state, setItem, setDelay)      })  }  /*  通用方法  */  export function getType(para) {      return Object.prototype.toString.call(para)          .replace(/[object (.+?)]/, '$1').toLowerCase()  }

persistent.js中调用:

import {persistedState} from 'tools.js'  ...  ...  // 因为是uni-app小程序,持久化是调用uni.setStorageSync,网页就用localStorage.setItem  // 1000仅是测试值,实际可设为200以内或直接设为0  persistedState({      state,      setItem: uni.setStorageSync,      getItem: uni.getStorageSync,      setDelay: 1000  })

经测试,持久化的state项中的内容变动时,storage会自动持久化对应的项,防抖也能有效防止state中内容频繁变化时的性能问题。

注:
由于网页的localStorage的setItem需要转换成字符串,getItem时又要JSON.parse一下,网页中使用该功能时tools.js需做如下修改:

function debounceStorage(state, fn, delay) {  ...                  updateItems.forEach(key => {                  fn.call(this, key, JSON.stringify(state[key]))              })  ...  }  function persistedState({state, setItem, getItem, setDelay=0}) {  ...                  if(state[key] !== undefined) {                  try{                      state[key] = JSON.parse(getItem(key))                  }catch(e){                      state[key] = getItem(key)                  }              }  ...  }

在网页中,调用方式如下:

import {persistedState} from 'tools.js'  const _state = {A: '',B: {a:{b:[1,2,3]}}}  persistedState({      state:_state,      setItem: localStorage.setItem.bind(localStorage),      getItem: localStorage.getItem.bind(localStorage),      setDelay: 200  }) 

修改_state.A、_state.B及其子项,可观察localStorage中存入数据的变化
(可直接打开源码地址中的<网页state持久化.html>查看)

源码地址

https://github.com/goblin-pitcher/uniapp-miniprogram