最近用uni-app開發小程序項目時,部分須要持久化的內容直接使用vue中的持久化插件貌似不太行,因此想着本身實現一下相似vuex-persistedstate插件的功能,想着功能很少,代碼量應該也不會很大html
首先想到的實現方式天然是vue的watcher模式。對須要持久化的內容進行劫持,當內容改變時,執行持久化的方法。
先弄個dep和observer,直接observer須要持久化的state,並傳入get和set時的回調:vue
function dep(obj, key, options) { let data = obj[key] Object.defineProperty(obj, key, { configurable: true, 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),即當某一項的後代節點變更時,咱們須要沿着變更的後代節點找到它的根節點,而後將根節點下的內容所有替換成新的。
接下來的第一個問題就是,如何找到變更節點的祖先節點。git
方案一:沿着state向下找到變更的節點,根據尋找路徑確認變更項的根節點,此方案複雜度過高。
方案二:在observer的時候,對state中的每一項增添一個指向父節點的指針,在後代節點變更時,能夠沿着指向父節點的指針找到相應的根節點,此方案可行。
爲避免新增的指針被遍歷到,決定採用Symbol標記指針,因而dep部分變更以下:github
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項了vuex
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,性能太差app
最後代碼以下:
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() { 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>查看)