Implement a simple version of vuex persistence tool

Keywords: Javascript Vue Android github

background

Recently, when developing applet projects with uni-app, some content that needs to be persisted can't be called like state in other vuex, so think about implementing something like vuex-persistedstate plug-in yourself. It doesn't seem like a lot of code

Preliminary ideas

The first thought is naturally the watcher mode of vue.Hijack content that needs to be persisted, and execute the persistence method when the content changes.
Get a dep and observer first, direct observer needs a persisted state, and pass in callbacks for get and 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 ('Parameter needs to be object')
    Object.keys(obj).forEach(key => {
        dep(obj, key, options)
        if(getType(obj[key]) === 'object') {
            observer(obj[key], options)
        }
    })
}

However, it soon became apparent that if a={b:{c:d:{e:1}} was stored, the operation would normally B e xxstorage('a',a). Next, whether a.b, a.b.c, or a.b.c.d.e was changed, xxstorage('a',a) would need to B e re-executed, that is, the entire object tree would b e re-persisted regardless of which descendant node of a was changed, so it was monitored.After a descendant node of a root node changes, it is necessary to find the root node first, and then re-persist the items corresponding to the root node.
The next first question is how to find the parent of a changing node.

Reconstructing state Tree

It is too complex to find a moving node down the state and confirm the changing item based on the path where the node was found.
If you add a pointer to each item in the state while observer, can you find the corresponding root node along the pointer to the parent node when the descendant node changes?
To avoid the new pointer being traversed, Symbol was chosen, so the dep part changed as follows:

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)
            }
            ...
        }
    })
}

With another way to find the root node, you can change the corresponding storage item

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] is the root node, and storagePath records the path from the root node to the changing node
    return storagePath 
}

But the problem is that object s can be automatically persisted. When an array is operated on by push and pop, the address of the array is unchanged. DefneProperty can't detect this change at all (unfortunately, Proxy compatibility is too poor and Android doesn't support it directly in the applet).Of course, each time you manipulate an array, reassigning the array solves the problem, but it's too inconvenient to use.

Binding when changing arrays

The same way to solve the array problem is to refer to the vue source processing, override the array's'push','pop','shift','unshift','splice','sort','reverse'methods
When an array operates on an array in these seven ways, manually trigger part of the set to update the story content

Add Anti-Shake

When vuex is persisted, it is easy to encounter situations where state s are frequently manipulated. If you keep updating storage, performance will be poor

Implementation Code

The final code is as follows:
tool.js:

/*
Persist related content
*/
// Overridden Array method
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 Need to be function')
        options.set(obj, key)
    }
}

function rewriteArrFunc(arr, options) {
    if (getType(arr) !== 'array') throw ('Parameter needs to be 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 ('Parameter needs to be 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 in state Content persistence failed,Error at[${changeKey}]In parameters[${key}]term`)
            }
        }, 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 Automatically configure mutation-related methods
*/
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)
        }
    })
}
/*
General method
*/
export function getType(para) {
    return Object.prototype.toString.call(para)
        .replace(/\[object (.+?)\]/, '$1').toLowerCase()
}

Called in persistent.js:

import {persistedState} from '../common/tools.js'
...
...
// Because it's a uni-app applet, persistence calls uni.setStorageSync, and the web page uses localStorage.setItem
persistedState({state, setItem: uni.setStorageSync, setDelay: 1000})

Source Address

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

Posted by remal on Tue, 10 Sep 2019 09:10:47 -0700