如何实现一个简易版的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 相关文章推荐
jquery checkbox 勾选的bug问题解决方案与分析
Nov 13 Javascript
js带闹铃功能的倒计时代码
Sep 29 Javascript
javascript实现获取图片大小及图片等比缩放的方法
Nov 24 Javascript
jquery实现自适应banner焦点图
Feb 16 Javascript
JavaScript Date对象应用实例分享
Oct 30 Javascript
VueJs 搭建Axios接口请求工具
Nov 20 Javascript
微信小程序基于本地缓存实现点赞功能的方法
Dec 18 Javascript
微信小程序滑动选择器的实现代码
Aug 10 Javascript
微信小程序 授权登录详解(附完整源码)
Aug 23 Javascript
Vue数字输入框组件的使用方法
Oct 19 Javascript
通过js实现压缩图片上传功能
Feb 25 Javascript
原生js实现照片墙效果
Oct 13 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 for 循环语句使用方法详细说明
2010/05/09 PHP
PHP用正则匹配form表单中所有元素的类型和属性值实例代码
2017/02/28 PHP
PHP有序表查找之二分查找(折半查找)算法示例
2018/02/09 PHP
Ajax一统天下之Dojo整合篇
2007/03/24 Javascript
jquery tools 系列 scrollable(2)
2009/09/06 Javascript
jQuery技巧大放送 学习jquery的朋友可以看下
2009/10/14 Javascript
jquery实用代码片段集合
2010/08/12 Javascript
JavaScript正则表达式中的ignoreCase属性使用详解
2015/06/16 Javascript
jQuery获取多种input值的简单实现方法
2016/06/20 Javascript
JavaScript实现Java中Map容器的方法
2016/10/09 Javascript
Vue2.0组件间数据传递示例
2017/03/07 Javascript
利用vueJs实现图片轮播实例代码
2017/06/03 Javascript
js 获取元素的具体样式信息getcss(实例讲解)
2017/07/05 Javascript
Vue学习笔记进阶篇之vue-cli安装及介绍
2017/07/18 Javascript
Angular在模板驱动表单中自定义校验器的方法
2017/08/09 Javascript
JavaScript实现单击网页任意位置打开新窗口与关闭窗口的方法
2017/09/21 Javascript
vue通过指令(directives)实现点击空白处收起下拉框
2018/12/06 Javascript
微信小程序 简易计算器实现代码实例
2019/09/02 Javascript
在vue中阻止浏览器后退的实例
2019/11/06 Javascript
[01:02:30]Mineski vs Secret 2019国际邀请赛淘汰赛 败者组 BO3 第三场 8.22
2019/09/05 DOTA
Python 3.6 -win64环境安装PIL模块的教程
2019/06/20 Python
python算法与数据结构之单链表的实现代码
2019/06/27 Python
Python循环中else,break和continue的用法实例详解
2019/07/11 Python
python实现全排列代码(回溯、深度优先搜索)
2020/02/26 Python
Jupyter notebook设置背景主题,字体大小及自动补全代码的操作
2020/04/13 Python
通过python 执行 nohup 不生效的解决
2020/04/16 Python
解决numpy矩阵相减出现的负值自动转正值的问题
2020/06/03 Python
css背景图片的背景裁切、背景透明度、背景变换等效果运用
2012/12/24 HTML / CSS
css3实现文字扫光渐变动画效果的示例
2017/11/07 HTML / CSS
加拿大消费电子和手机购物网站:The Source
2017/01/28 全球购物
先进集体事迹材料范文
2014/12/25 职场文书
汽车4S店前台接待岗位职责
2015/04/03 职场文书
python如何做代码性能分析
2021/04/26 Python
原生JS实现飞机大战小游戏
2021/06/09 Javascript
Python用tkinter实现自定义记事本的方法详解
2022/03/31 Python
怎么禁用Win11输入法 最新Win11输入法关闭教程
2022/08/05 数码科技