背景
最近用 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 > 查看)
源码地址
来源: https://www.cnblogs.com/wswx3219/p/11506954.html