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


Posted in Javascript onSeptember 11, 2019

背景

最近用uni-app开发小程序项目时,部分需要持久化的内容没法像其他vuex中的state那样调用,所以想着自己实现一下类似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),也就是无论a的哪个后代节点变动了,重新持久化的都是整个object树,所以监测到某个根节点的后代节点变更后,需要先找到根节点,再将根节点对应的项重新持久化。

接下来的第一个问题就是,如何找到变动节点的父节点。

state树的重新构造

如果沿着state向下找到变动的节点,并根据找到节点的路径确认变动项,复杂度太高。

如果在observer的时候,对state中的每一项增添一个指向父节点的指针,在后代节点变动时,是不是就能沿着指向父节点的指针找到相应的根节点了?

为避免新增的指针被遍历到,决定采用Symbol,于是dep部分变动如下:

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

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

function getStoragePath(obj, key) {
 let storagePath = [key]
 while (obj) {
  if (obj[Symbol.for('key')]) {
   key = obj[Symbol.for('key')]
   storagePath.unshift(key)
  }
  obj = obj[Symbol.for('parent')]
 }
 // 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']

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].call(this, ...args)
   setCallBack(this[Symbol.for('parent')], this[Symbol.for('key')], options)
  }
 })
}

function dep(obj, key, options) {
 let data = obj[key]
 if (typeArr.includes(getType(data))) {
  data[Symbol.for('parent')] = obj
  data[Symbol.for('key')] = 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[Symbol.for('parent')] = obj
    data[Symbol.for('key')] = 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 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 getStoragePath(obj, key) {
 let storagePath = [key]
 while (obj) {
  if (obj[Symbol.for('key')]) {
   key = obj[Symbol.for('key')]
   storagePath.unshift(key)
  }
  obj = obj[Symbol.for('parent')]
 }
 return storagePath
}
export function persistedState({state, setItem, getItem, setDelay=0, getDelay=0}) {
 observer(state, {
  set: debounceStorage(state, setItem, setDelay),
  get: debounceStorage(state, getItem, getDelay)
 })
}
/*
vuex自动配置mutation相关方法
*/
export function setMutations(stateReplace, mutationsReplace) {
 Object.keys(stateReplace).forEach(key => {
  let name = key.replace(/\w/, (first) => `update${first.toUpperCase()}`)
  let replaceState = (key, state, payload) => {
   state[key] = payload
  }
  mutationsReplace[name] = (state, payload) => {
   replaceState(key, state, payload)
  }
 })
}
/*
通用方法
*/
export function getType(para) {
 return Object.prototype.toString.call(para)
  .replace(/\[object (.+?)\]/, '$1').toLowerCase()
}

persistent.js中调用:

import {persistedState} from '../common/tools.js'
...
...
// 因为是uni-app小程序,持久化是调用uni.setStorageSync,网页就用localStorage.setItem
persistedState({state, setItem: uni.setStorageSync, setDelay: 1000})

源码地址

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
IE6,IE7,IE8下使用Javascript记录光标选中范围(已补全)
Aug 28 Javascript
JQuery操作表格(隔行着色,高亮显示,筛选数据)
Feb 23 Javascript
php对mongodb的扩展(初识如故)
Nov 11 Javascript
浅析Node在构建超媒体API中的作用
Jul 30 Javascript
js面向对象之静态方法和静态属性实例分析
Jan 10 Javascript
JavaScript学习心得之概述
Jan 20 Javascript
JavaScript函数作用域链分析
Feb 13 Javascript
Javascript中判断对象是否为空
Jun 10 Javascript
js实现常用排序算法
Aug 09 Javascript
JS从非数组对象转数组的方法小结
Mar 26 Javascript
VueQuillEditor富文本上传图片(非base64)
Jun 03 Javascript
JavaScript数组排序的六种常见算法总结
Aug 18 Javascript
浅谈layer弹出层按钮颜色修改方法
Sep 11 #Javascript
layui实现鼠标移动到单元格上显示数据的方法
Sep 11 #Javascript
layer ui插件显示tips时,修改字体颜色的实现方法
Sep 11 #Javascript
layui操作列按钮个数和文字颜色的判断实例
Sep 11 #Javascript
JS模拟浏览器实现全局搜索功能
Sep 11 #Javascript
Vue项目中使用better-scroll实现菜单映射功能方法
Sep 11 #Javascript
使用layui+ajax实现简单的菜单权限管理及排序的方法
Sep 10 #Javascript
You might like
发一个php简单的伪原创程序,配合商城采集用的
2010/10/12 PHP
php检测数组长度函数sizeof与count用法
2014/11/17 PHP
php实现以只读方式打开文件的方法
2015/03/16 PHP
Yii2基于Ajax自动获取表单数据的方法
2016/08/10 PHP
php正则去除网页中所有的html,js,css,注释的实现方法
2016/11/03 PHP
javascript实现日历控件(年月日关闭按钮)
2012/12/12 Javascript
原生JavaScript实现连连看游戏(附源码)
2013/11/05 Javascript
js判断浏览器版本以及浏览器内核的方法
2015/01/20 Javascript
深入剖析JavaScript中的函数currying柯里化
2016/04/29 Javascript
jQuery 获取屏幕高度、宽度的简单实现案例
2016/05/17 Javascript
JS实现根据文件字节数返回文件大小的方法
2016/08/02 Javascript
Bootstrap源码解读媒体对象、列表组和面板(10)
2016/12/26 Javascript
Python实现在线程里运行scrapy的方法
2015/04/07 Python
python中Matplotlib实现绘制3D图的示例代码
2017/09/04 Python
python的构建工具setup.py的方法使用示例
2017/10/23 Python
python tkinter界面居中显示的方法
2018/10/11 Python
python json.loads兼容单引号数据的方法
2018/12/19 Python
Python中三元表达式的几种写法介绍
2019/03/04 Python
简单了解Python matplotlib线的属性
2019/06/29 Python
Python定时发送天气预报邮件代码实例
2019/09/09 Python
Kmeans均值聚类算法原理以及Python如何实现
2020/09/26 Python
python 基于DDT实现数据驱动测试
2021/02/18 Python
html5中去掉input type date默认样式的方法
2018/09/06 HTML / CSS
HTML5实现文件断点续传的方法
2017/01/04 HTML / CSS
Ellos丹麦:时尚和服装在线
2016/09/19 全球购物
Uber Eats台湾:寻找附近提供送餐服务的餐厅
2018/05/07 全球购物
公交公司毕业生求职信
2014/02/15 职场文书
房屋委托书范本
2014/04/04 职场文书
超市创业计划书
2014/09/15 职场文书
四风问题专项整治工作情况报告
2014/10/28 职场文书
2015年手术室工作总结
2015/05/11 职场文书
教师远程研修感悟
2015/11/18 职场文书
python批量更改目录名/文件名的方法
2021/04/18 Python
快速学习Oracle触发器和游标
2021/06/30 Oracle
日本动漫十大公认神作:第五现已全网禁播,《死亡笔记》在榜
2022/03/18 日漫
Nginx流量拷贝ngx_http_mirror_module模块使用方法详解
2022/04/07 Servers