背景

最近用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.setItempersistedState({state, setItem: uni.setStorageSync, setDelay: 1000})

源码地址

https://github.com/goblin-pit...