实现一个简易版的vuex持久化工具
背景
最近用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://github.com/goblin-pitcher/uniapp-miniprogram
实现一个简易版的vuex持久化工具的更多相关文章
- 手动实现一个简易版SpringMvc
版权声明:本篇博客大部分代码引用于公众号:java团长,我只是在作者基础上稍微修改一些内容,内容仅供学习与参考 前言:目前mvc框架经过大浪淘沙,由最初的struts1到struts2,到目前的主流框 ...
- DI 原理解析 并实现一个简易版 DI 容器
本文基于自身理解进行输出,目的在于交流学习,如有不对,还望各位看官指出. DI DI-Dependency Injection,即"依赖注入":对象之间依赖关系由容器在运行期决定, ...
- .NET Core的文件系统[5]:扩展文件系统构建一个简易版“云盘”
FileProvider构建了一个抽象文件系统,作为它的两个具体实现,PhysicalFileProvider和EmbeddedFileProvider则分别为我们构建了一个物理文件系统和程序集内嵌文 ...
- 依赖注入[5]: 创建一个简易版的DI框架[下篇]
为了让读者朋友们能够对.NET Core DI框架的实现原理具有一个深刻而认识,我们采用与之类似的设计构架了一个名为Cat的DI框架.在<依赖注入[4]: 创建一个简易版的DI框架[上篇]> ...
- 依赖注入[4]: 创建一个简易版的DI框架[上篇]
本系列文章旨在剖析.NET Core的依赖注入框架的实现原理,到目前为止我们通过三篇文章(<控制反转>.<基于IoC的设计模式>和< 依赖注入模式>)从纯理论的角度 ...
- .NET CORE学习笔记系列(2)——依赖注入[4]: 创建一个简易版的DI框架[上篇]
原文https://www.cnblogs.com/artech/p/net-core-di-04.html 本系列文章旨在剖析.NET Core的依赖注入框架的实现原理,到目前为止我们通过三篇文章从 ...
- 如何实现一个简易版的 Spring - 如何实现 Setter 注入
前言 之前在 上篇 提到过会实现一个简易版的 IoC 和 AOP,今天它终于来了...相信对于使用 Java 开发语言的朋友们都使用过或者听说过 Spring 这个开发框架,绝大部分的企业级开发中都离 ...
- 如何实现一个简易版的 Spring - 如何实现 Constructor 注入
前言 本文是「如何实现一个简易版的 Spring」系列的第二篇,在 第一篇 介绍了如何实现一个基于 XML 的简单 Setter 注入,这篇来看看要如何去实现一个简单的 Constructor 注入功 ...
- 如何实现一个简易版的 Spring - 如何实现 @Component 注解
前言 前面两篇文章(如何实现一个简易版的 Spring - 如何实现 Setter 注入.如何实现一个简易版的 Spring - 如何实现 Constructor 注入)介绍的都是基于 XML 配置文 ...
随机推荐
- (通俗易懂小白入门)网络流最大流——EK算法
网络流 网络流是模仿水流解决生活中类似问题的一种方法策略,来看这么一个问题,有一个自来水厂S,它要向目标T提供水量,从S出发有不确定数量和方向的水管,它可能直接到达T或者经过更多的节点的中转,目前确定 ...
- Redhat 离线安装 Docker (Community from binaries)
需求 在离线环境安装Docker (Community版),因为Enterprise版要花钱.当然资金充裕的客户可参考https://docs.docker.com/install/linux/doc ...
- Hystrix超时测试
package com.cookie.test; import com.netflix.hystrix.HystrixCommand; import com.netflix.hystrix.Hystr ...
- Java中的时间二三事
实习过程中对于时间的处理有很多,有的还涉及到从数据库取出时间,所以做一些总结,想到那先写到哪,慢慢补充. 首先最常见的是java.util中的Date类,这个类封装了当前的日期和时间,它实际是计 ...
- nodeJs跨域设置(express,koa2,eggJs)
原生跨域 var http=require('http'); var server = http.createServer(function (req,res) { res.setHeader('Ac ...
- 给debian的docker容器添加crontab定时任务
现在大部分的docke镜像是基于debian # cat /etc/issue Debian GNU/Linux 9 \n \l Docker容器是不支持后台服务的,像systemctl servic ...
- Top 10 顶级项目管理工具
成功的项目都要归功于成功的项目管理.这些工具帮你踏上成功之旅! 项目管理是成功完成项目并使公司变得伟大的秘诀.不,这不是标题党(clickbait) -- 我已经看到两家软件公司(我在那里工作)因为项 ...
- Java String引起的常量池、String类型传参、“==”、“equals”、“hashCode”问题 细节分析
在学习javase的过程中,总是会遇到关于String的各种细节问题,而这些问题往往会出现在Java攻城狮面试中,今天想写一篇随笔,简单记录下我的一些想法.话不多说,直接进入正题. 1.String常 ...
- 集成 Spring Boot 常用组件的后台快速开发框架 spring-boot-plus 国
spring-boot-plus是一套集成spring boot常用开发组件的后台快速开发框架 Purpose 每个人都可以独立.快速.高效地开发项目! Everyone can develop pr ...
- 渗透之路基础 -- 跨站脚本攻击XSS
目录 漏洞原理及防御 XSS 原理分析:输出问题导致js代码被识别执行 XSS 技术分类 Cookie盗取 基于Xss的WebShell箱子的攻击 XSS相关防护过滤及绕过分析(参考链接) 防护: 绕 ...