视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
如何实现一个简易版的vuex持久化工具
2020-11-27 21:50:37 责编:小采
文档


背景

最近用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

下载本文
显示全文
专题