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

背景

最近用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>查看)

源码地址

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

posted @ 2019-09-11 16:28  goblin_pitcher  阅读(...)  评论(...编辑  收藏