上一篇 https://www.cnblogs.com/jyk/p/14706005.html 介绍了一个自己做的轻量级的状态管理,好多网友说,状态最重要的是跟踪功能,不能跟踪算啥状态管理?

因为vue3的状态都是 reactive 的形式,也就是 proxy,原本以为有自动跟踪的功能,但是后来发现,好像没有。

不管他有没有了,我们自己加上好了。

跟踪的两种方式

  • 不记录调用者

    只记录修改了哪个属性,和属性值,还有时间,其他的不管了。这个比较简单,直接套娃即可。

  • 全套

    不知道是谁(组件)触发的状态改变,总觉得这个跟踪没啥大用处,所以还是希望要做就做全套,把调用者记录下来才是王道。

    目前没啥好办法,还需要组件在使用的时候,多加一行,再设置个参数才能实现。

用 proxy 套娃 reactive,实现跟踪功能

我们先定义一个函数实现一个 proxy,加上跟踪功能。

/**
* 带跟踪的reactive。使用 proxy 套娃
* @param {reactive} _target 要拦截的目标 reactive
* @param {string} flag 使用状态者的标记
* @param {array} log 存放跟踪记录的数组
*/
export default function trackReactive (_target, flag, log) {
const proxy = new Proxy(_target, {
get: function (target, key, receiver) {
log.push({
caller: flag, // 调用者
type: 'get', // 获取或者设置
time: new Date().valueOf(), // 获取时间
target: target, // 目标
key: key, // 读取的属性名称
value: target[key] // 读取得到的值
})
if (log.length > 300) {
log.splice(0, 50) // 避免数组太大
}
// 调用原型方法
return Reflect.get(target, key, receiver)
},
set: function (target, key, value, receiver) {
log.push({
caller: flag, // 调用者
type: 'set', // 获取或者设置
time: new Date().valueOf(), // 获取时间
target: target, // 目标
key: key, // 设置的属性名称
value: value // 设置的值
})
if (log.length > 300) {
log.splice(0, 50) // 避免数组太大
}
// 调用原型方法
return Reflect.set(target, key, value, target)
}
})
// 返回实例
return proxy
}

比较简单的 proxy 的应用,拦截 get 和 set 操作,把需要的信息写入log。

使用方式

如果直接使用,无法记录调用者是谁,所以需要用函数的方式获取。


// 测试全局状态
const { state, track, globalLog } = nfStore.useStore() // 直接获取,无跟踪
const user = state.userOnline
// 带跟踪的获取方式
const user2 = track.userOnline('首页测试')
// 修改测试
setTimeout(() => {
user2.name = '测试修改。。。'
console.log(globalLog)
}, 2000)

看看效果

我们来看看运行效果:

我们可以看到确实记录了状态的变化,但是这个日志似乎有点多。我只是简单的把状态放在模板里,居然get了这么多次。

看来需要做做减法,去掉不需要的记录。

只记录关心的。


import { reactive } from 'vue' /**
* 带跟踪的reactive。使用 proxy 套娃
* @param {reactive} _target 要拦截的目标 reactive
* @param {string} flag 使用状态者的标记
* @param {array} log 存放跟踪记录的数组
*/
export default function trackReactive (_target, flag, log) {
const proxy = new Proxy(_target, {
get: function (target, key, receiver) {
if (typeof key !== 'symbol') {
console.log(`getting ${key}!`, target[key])
switch (key) {
case '__v_isRef':
case 'toString':
case 'toJSON':
// 不记录
break
default:
log.push({
_caller: flag, // 调用者
type: 'get', // 获取或者设置
time: new Date().valueOf(), // 获取时间
target: target, // 目标
_key: key, // 读取的属性名称
_value: target[key] // 读取得到的值
})
if (log.length > 300) {
log.splice(0, 50) // 避免数组太大
}
}
}
// 调用原型方法
return Reflect.get(target, key, receiver)
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}:${value}!`)
console.log('target--', target)
console.log('receiver--', receiver)
// console.log(target == receiver)
log.push({
_caller: flag, // 调用者
type: 'set', // 获取或者设置
time: new Date().valueOf(), // 获取时间
target: target, // 目标
_key: key, // 设置的属性名称
_value: value // 设置的值
})
if (log.length > 300) {
log.splice(0, 50) // 避免数组太大
}
// 调用原型方法
return Reflect.set(target, key, value, target)
}
})
// 返回实例
return proxy
}

加上下划线,是打印的时候自动排序好看一点。

再来看看效果

这下就清凉多了,只有三条记录,第一个是模板获取状态,然后是settimeout里面修改状态,最后是模板更新状态的显示。

  • target 修改了哪个状态
  • time 修改状态的时间戳
  • type 是读取还是设置
  • _caller 调用者的flag(需要手动设置)
  • _key 修改/读取的属性的名称
  • _value 修改/读取的属性值

基本全套了。

嵌套属性怎么办?

虽然 proxy 可以拦截操作,但是并不能拦截嵌套属性的操作。也就是说 proxy 其实是浅层的。

可能你会觉得,不对呀,reactive 明明是深层响应的,怎么就浅层了?

那是因为reactive在get里面做了“手脚”。

看上面我们写的代码,你也许会觉得奇怪,记录状态变化,为啥要拦截get?这个就是为了嵌套属性做准备。

先测试一下嵌套属性的操作

// 测试全局状态
const { state, track, globalLog } = nfStore.useStore() // 直接获取,无跟踪
const user = state.userOnline
// 带跟踪的获取方式
const user2 = track.userOnline('首页测试')
user2.aa = {
aa1: '测试嵌套',
aa2: '还是'
}
// 修改测试
setTimeout(() => {
// user2.name = '测试修改。。。'
user2.aa.aa1 = '测试嵌套属性的修改。。。'
console.log(globalLog)
}, 2000)

修改嵌套属性的运行效果

只记录到一开始设置 aa 属性的操作,后面就都是 get 的操作,没有 set 的操作。

果然不支持深层操作。

再看看 reactive 内部是如何实现的。

function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
if (key === "__v_isReactive" /* IS_REACTIVE */) {
return !isReadonly;
}
else if (key === "__v_isReadonly" /* IS_READONLY */) {
return isReadonly;
}
else if (key === "__v_raw" /* RAW */ &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap).get(target)) {
return target;
}
const targetIsArray = isArray(target);
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver);
}
const res = Reflect.get(target, key, receiver);
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res;
}
if (!isReadonly) {
track(target, "get" /* GET */, key);
}
if (shallow) {
return res;
}
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key);
return shouldUnwrap ? res.value : res;
}
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res);
}
return res;
};
}

简单的说,先获取属性值,然后判断类型,如果是对象,那么套上 reactive 再 return。这样就变成深层响应了。

改进代码支持嵌套属性的修改

我们效仿一下,在 get 里面继续套娃:

// 调用原型方法
const res = Reflect.get(target, key, receiver)
if (isReactive(res)) {
return trackReactive(res, flag, log)
}
return res

还是要判断一下类型,只是不用那么复杂的判断,只需要判断是不是 reactive:

  • 是reactive,套个娃再返回。
  • 不是 reactive,那么大概是基础类型直接返回就好。

嵌套属性的跟踪记录效果

现在可以记录全套了,嵌套属性也逃不掉。

能想到的都实现了,如果有啥遗漏的地方,还请大家补充。

one more thing

最后还是没有搂住,又加了一个小功能,就是修改嵌套属性的时候,如何根据日志看出来到底修改的是哪个状态。

看看上面的日志,虽然可以得到要修改的属性的名称,

但是这个属性是第几层的?上一级的属性名称是啥?能不能依次找到最上一级的属性?

各种折腾终于加上这个功能。

设计一个三层的状态,然后修改cc1看看效果:

// 带跟踪的获取方式
const user2 = track.userOnline('首页测试')
user2.aa = {
aa1: '测试嵌套',
aa2: {
cc1: '第三层属性'
}
}
// 修改测试
setTimeout(() => {
// user2.name = '测试修改。。。'
user2.aa.aa1 = '测试嵌套属性的修改。。。'
console.log('path - aa1 :', globalLog[globalLog.length - 2].targetPath, globalLog[globalLog.length - 1]._key)
user2.aa.aa2.cc1 = '属性3的修改'
console.log('path - cc1 :', globalLog[globalLog.length - 2].targetPath, globalLog[globalLog.length - 1]._key)
console.log(globalLog)
}, 2000)

然后我们看看日志:

跟踪记录里面增加了一个path的功能,可以记录各级属性名称。

  • targetBase: 最上级的状态
  • targetName: target 对应的属性名称
  • target:当前要获取的属性
  • _key:当前要获取的属性名称
  • targetPath:从 targetBase 开始的属性名组成的路径

加上定位功能

感谢知乎好友的帮助,增加了一个可以直接定位到修改属性的代码的功能。

在 set 里面 加上 const stack = new Error() ,然后记入 targetPath 即可。

原本 set 的 log 里没有记录这个 path,现在正好用上了。

定位效果

可以遍历找到 set 的记录,找到 targetPath 属性然后 console.log 就可以定位了。

开发阶段可以直接定位,生产模式(客户)可以上传日志进行各种分析。

源码

如果感兴趣的话,可以到这来看完整代码

https://gitee.com/naturefw/nf-plat-vite2-vue3

https://gitee.com/naturefw/nf-plat-vite2-vue3/tree/master/packages/nf-state

vue3 自己做一个轻量级状态管理,带跟踪功能,知道是谁改的,还能定位代码。的更多相关文章

  1. '用Roslynpad做一个轻量级的C#编辑器'

    博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:'用Roslynpad做一个轻量级的C#编辑器'.

  2. 借鉴redux,实现一个react状态管理方案

    react状态管理方案有很多,其中最简单的最常用的是redux. redux实现 redux做状态管理,是利用reducer和action实现的state的更新. 如果想要用redux,需要几个步骤 ...

  3. 轻量级状态管理库Pinia试吃

      最近连续看了几个GitHub上的开源项目,里面都用到了 Pinia 这个状态管理库,于是研究了一下,发现确实是个好东西!那么,Pinia 的特点: 轻量化 -- Pinia 体积约1KB,十分轻巧 ...

  4. (Demo分享)利用JavaScript(JS)做一个可输入分钟的倒计时钟功能

    利用JavaScript(JS)实现一个可输入分钟的倒计时钟功能本文章为 Tz张无忌 原创文章,转载请注明来源,谢谢合作! 网络各种利用JavaScript做倒计时的Demo对新手很不友好,这里我亲手 ...

  5. IT兄弟连 JavaWeb教程 Servlet 状态管理 会话跟踪

    HTTP协议是无状态的,我们的客户端与服务器的每一次请求与响应,我们服务器都没有记忆能力将客户端与服务器的多次交互数据进行存储与管理共有两种技术实现: ●  基于客户端实现:Cookie,将状态保存在 ...

  6. 12月16日 增加一个购物车内product数量的功能, 自定义method,在helper中定义,计算代码Refactor到Model中。

    仿照Rails实战:购物网站 教材:5-6 step5:计算总价,做出在nav上显示购物车内product的数量. 遇到的❌: 1. <% sum = 0 %> <% current ...

  7. 制作一个轻量级的状态管理插件:Vue-data-state

    Vuex 是不是有点繁琐? Vuex 是针对 Vue2 来设计的,因为 option API 本身有很多缺点,所以 Vuex 只好做各种补丁弥补这些缺点,于是变得比较"复杂". 现 ...

  8. 结合 Vuex 和 Pinia 做一个适合自己的状态管理 nf-state

    一开始学习了一下 Vuex,感觉比较冗余,就自己做了一个轻量级的状态管理. 后来又学习了 Pinia,于是参考 Pinia 改进了一下自己的状态管理. 结合 Vuex 和 Pinia, 保留需要的功能 ...

  9. 告别Vuex,发挥compositionAPI的优势,打造Vue3专用的轻量级状态

    Vuex 的遗憾 Vuex 是基于 Vue2 的 option API 设计的,因为 optionAPI 的一些先天问题,所以导致 Vuex 不得不用各种方式来补救,于是就出现了 getter.mut ...

随机推荐

  1. 深入理解 PHP7 中全新的 zval 容器和引用计数机制

    深入理解 PHP7 中全新的 zval 容器和引用计数机制 最近在查阅 PHP7 垃圾回收的资料的时候,网上的一些代码示例在本地环境下运行时出现了不同的结果,使我一度非常迷惑. 仔细一想不难发现问题所 ...

  2. 安装PyTorch后,又安装TensorFlow,CUDA相关问题思考

    下面的话是我的观察和思考,请多多批评. TensorFlow 要用 CUDA.CUDA toolkit.CUDNN,看好版本的对应关系再安装,磨刀不误砍柴工. 1)NVIDIA Panel 里显示的N ...

  3. CF1539A Contest Start[题解]

    Contest Start 题目大意 有 \(n\) 个人报名参加一个比赛,从 \(0\) 时刻开始每隔 \(x\) 分钟有一个人开始比赛,每个人参赛时间相同,均为 \(t\) .定义一个选手的不满意 ...

  4. 锐捷RG-S2951G-EV3 ACL 禁止端口

    (config)ip access-list extended 199 (config)10 deny tcp any any eq 2425 (config)20 deny udp any any ...

  5. 高校表白App-团队冲刺第一天

    今天要做什么 今天要再次重新的好好学一下Activity的生命周期,简单的写一个Activity,熟悉Activity的线程. 遇到的问题 在点击事件发生时,在activity进行finish()后, ...

  6. [003] - JavaSE面试题(三):JavaSE语法(1)

    第一期:Java面试 - 100题,梳理各大网站优秀面试题.大家可以跟着我一起来刷刷Java理论知识 [003] - JavaSE面试题(三):JavaSE语法(1) 第1问:& 和 & ...

  7. java 利用Calendar进行日期更改

    //建立1个日期实例 Date tomorrow= new Date(); //获取今天日期 Date nowDate = Calendar.getInstance().getTime(); // 构 ...

  8. odoo里的开发案例

    1.模块命名[驼峰命名方法] res开头的是:resources   常见模型:res.users,   res.company,    res.partner,   res.config.setti ...

  9. Windows Server创建域控制器

    推荐选择系统镜像为windows server2016(2019有诡异的bug不能安装域控.) 1.本地域安装设置 (1)连接到windows server2016 打开服务器管理器(Server M ...

  10. 深入刨析tomcat 之---第14篇 对应19章,使用manager管理 web应用

    writedby 张艳涛 第19章讲的是管理程序,当一个tomcat启动的时候,能通过远程浏览器能访问tomcat,启动web应用,关闭web应用,查看web应用 怎么实现的呢? 在webapp 文件 ...