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

11次阅读

共计 4731 个字符,预计需要花费 12 分钟才能阅读完成。

背景

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

正文完
 0