前言/介绍
-
上一篇打造了一款基于rxjs状态管理库,文章链接在此基于Rxjs打造自己的状态管理库 。 但在开发过程中,发现observable-store这个库在处理大json数据,有性能问题,收集历史state,或setState时,有大量的deepcopy操作,在小对象性能差异不会那么明显,但是大json处理,会有极其不友好的显示体验。其实如果不依赖rxjs,采用Proxy,再配合pubsub模式。可以让代码更简洁。往往现实中,对一些事件或异步事件依赖要求,不那么高的项目。完全可以不引用rxjs。所以是一个仁者见仁,智者见智的方案,rxjs in everything , 我也不太提倡。
-
所以闲暇之余,我从零开始,开发了一款响应式的状态管理库observable-lite,代码量非常少,可以对大json,做深层的观察控制,并且有很好性能。功能不复杂。在此我分享一下,这款库其中开发的技术细节。以便于小伙伴们以后想自己做一些响应式的应用,会有所启发,授人以渔。当然有兴趣小伙伴们,我们可以一起开发。
简单画一画结构
目标和思想
- 首先维持单一数据源的结构不变。
- 不使用deepcopy,对对象进行响应式控制,不管层级有深,确保每个叶子对象都被observe。
- 维持轻量级使用,react框架,提供use函数,方便使用
- 提供decorator函数,方便对class的属性做obervable处理
总的思想是,先进行proxy,之所以选用proxy,是因为对数组的处理过程,proxy要比defineProperty方案精简的多。然后就是pubsub,再最后就是开发对应的hooks函数和decorator函数,供用户方便使用。那么下面首先看一下Proxy对象如何实现?
Proxy如何确保每个子对象被监听?
Proxy是什么东西,我就不介绍了,大家如果在这里就碰到问题,可以先自行去baidu。再回来接着了解。我们知道new Proxy(obj,handler)
这个只能对第一层的Obj对象进行劫持。 比如我们有这样一个对象
const obj = {
a: 1,
b: [{ c: 2 }, { d: 3 }],
f: { g: 3, r: 7 },
h: { q: [{ j: 6 }, { y: 8 }, { cc: { dd: "hao" } }], h: 9 }
}
如果使用proxy只能获取到第一层a,b,f,h
这几个key的对象。 那么里面的对象我们怎么处理呢?
当然有同学就会说,采用BFS(广度优先遍历)。当然我们最初看Vue双向绑定使用defineProperty也是这么做的。起初我也是这样写的。但其实后来我发现有一个更好的方式去做,开销更小。如果采用BFS,我们还需要考虑什么时候我不需要observable,所以要有一个开关,因为在做BFS的时候,需要不断修改对象,这样使得set和get会有重复开销。其实这个问题很简单,我们只需要在Proxy的handler里面的get方法处理对对象的劫持的判断。 其实proxy对象,在访问子属性和修改子属性的时候会自动触发get方法。那么在get的时候,完全可以获得到子对象。所以我们只需要在这里做Obervable即可。
不上代码干说,就是耍流氓,这么low的事,我干不出来,下面请看代码。
const proxyTraps = {
get(target, name) {
//判断是否已经被代理过
if (name === "__isProxy") {
return true
}
let targetProp = target[name];
//asObservable判断对象是否被obervable如果没有就observable
return asObservable(targetProp)
},
set(target, name, value, receiver) {
target[name] = value
return true
},
}
const obj = {
a: 1,
b: [{ c: 2 }, { d: 3 }],
f: { g: 3, r: 7 },
h: { q: [{ j: 6 }, { y: 8 }, { cc: { dd: "hao" } }], h: 9 }
}
const testTarget = new Proxy(obj, proxyTraps)
Observable过程中怎么获取对象的Path值
- 为什么要获取对象的Path,到底有什么作用?
- 主要有两个作用:第一是获取到path可以对对象做精细监听,用在修改一个很深的对象的时候,比如上面代码中的那个obj。
obj.b[0].c=5
这个操作,在proxy中set方法中,我们只能得到key为c。 没有path的话,会很难处理。 我们需要这样一个path结构['b',0,'c']
- 第二个作用是,在pubsub中,我们也需要这个path,这样可以将消息精准推送到每个watcher总,所以代码中维护了一个observable的map。为了方便,key值就是path。
- 那么Path怎么获取呢?
- 其实很简单,在Proxy的get函数中获取,每次触发get函数时,能得到一个关系,一个关系来自上一个对象。我们需要将这个关系延续下来,请看代码
const proxyTraps = {
get(target, name) {
//判断是否已经被代理过
if (name === "__isProxy") {
return true
}
let targetProp = target[name];
//判断是否为对象或数组
if (isPassType(targetProp) && name != $felix) {
let tags: ExtraTags
//判断当前的path,如果有就使用当前的,没有就继承上层的path
if (!targetProp[$felix]) {
tags = { ...target[$felix], path: [...target[$felix].path] }
} else {
tags = { ...targetProp[$felix] }
}
//获取path最后一个key.
let lastKey = tags.path.length > 0 ? tags.path[tags.path.length - 1] : null
//避免path的重复创建
if (lastKey?.toString() !== name) {
if (Array.isArray(target)) {
tags.path.push(parseInt(name as string))
} else {
tags.path.push(name)
}
}
//defineProperty,存储path
extend_(targetProp, $felix, tags)
//asObservable判断对象是否被obervable如果没有就observable
return asObservable(targetProp)
}
return targetProp
},
set(target, name, value, receiver) {
target[name] = value
return true
},
}
- Path怎么存储,存储在哪里?
- 利用Symbol,所以在代码中我们定义这样一段代码
const $felix = Symbol("tags")
$felix
是我们定义的Symbol类型,这样的好处是避免用户不小心,误使用到这个属性。extend_方法是执行了一个Reflect.defineProperty,讲此属性定义不可枚举。这样就不会被迭代。并且随对象一起绑定。所以对象的每个属性都会有一个path,藏匿在$felix
这个属性中。
PubSub模式(发布订阅)
写发布订阅的思路跟Vue同源, 定义observable的依赖管理器,定义change队列,里面存储每个被劫持对象的改变的新值和旧值。定义notify方法,触发observable监听方法。 伪代码如下
class ObsBase{
//依赖收集器
public observables: Map<string, ObsTarget[]> = new Map();
// change队列
public changes: ChangeContext[] = []
}
const proxyTraps = {
get(target, name){
//...
},
set(target, name, value, receiver){
//...判断历史state
//...获取path
//...构造change对象
//..添加change到队列
addchange(change)
notify(path)
}
}
上面讲了一些难点,需要大家自行看代码或消化,如有问题可以评论区留言,也可以加我微信。下面讲一下,关键如何在react中应用。可能对初学者,关注的就是怎么使用。那么我就不废话了。
使用useObservableStore
先看一下我们在react的hooks中怎么用,observable-lite提供一个useObservableStore这个自定义的hook函数。下面我们用一个例子来讲解一下吧。
import React, { useEffect, useState } from 'react'
import { ObsStore } from './observable-lite/obs-store'
import { useObservableStore } from "./observable-lite/hooks";
import { ChangeContext } from './observable-lite/utils/interfaces';
export default function TestUseObs() {
//定义一个值类型的store,用法跟useState相似
const [count, setCount] = useObservableStore(1);
//定义一个对象类型的store
const [obj, _,key] = useObservableStore({
a: 1,
b: 2,
c: [{ d: 3 }, [{ f: 5 }]]
})
return (
<div>
<span>使用useObservableStore,普通值对象的情况,当前数:{count}</span>
<button
onClick={() => {
//值类型的使用set函数,去改变state
setCount(count + 1);
}}
>
点击累加
</button>
<div>
<div>使用useObservableStore,如果初始化的是一个对象,可以直接操作,不用使用set函数</div>
<div>当前对象:{JSON.stringify(obj)}</div>
<button onClick={() => {
//可以直接对对象进行操作和修改
obj.a += 1
obj.b += 1
obj.c[0].d += 1
obj.c[1][0].f += 1
}}>
点击对象累加
</button>
</div>
</div>
);
}
useObservableStore 返回值是一个数组,包含3个对象
[state,setState,key]
第一个state是一个observable对象,如果是对象可以直接修改。第二个是setState方法,可以用此修改对象,也可以用直接赋值操作,只针对对象。值类型,请用setState, 第三个参数key,如果手动监听某个对象,需要依赖此key,否则,这个key也用不到。example地址
使用observable装饰器和connect(类似mobx用法)
import React, { useEffect, useState } from "react";
import { ObsStore } from "./observable-lite/obs-store";
import { observable } from "./observable-lite/observable";
import { connect } from "./observable-lite/hooks";
import { ChangeContext } from "./observable-lite/utils/interfaces";
//定义自己的observable类
//这样的好处,可以将ajax或与数据相关的逻辑放在这里处理,相当一个model层
class MyExpObs<T> extends ObsStore<T> {
//使用observable装饰器,监听对象。这个会返回一个proxy对象
@observable obj: any = {};
@observable testVal: number = 1;
}
export default connect(function ObsClassExp() {
//获取当前store实例
const store = MyExpObs.getInstance() as MyExpObs<any>;
return (
<div>
<span>testVal:{store.testVal}</span>
<button
onClick={() => {
store.testVal += 1;
}}
>
点击修改值
</button>
<p></p>
<span>obj:{JSON.stringify(store.obj)}</span>
<button
onClick={() => {
store.obj = { first: "hui", last: "tang" };
}}
>
修改对象
</button>
<p></p>
{store.obj.last && (
<button
onClick={() => {
store.obj.last = "update tang";
store.obj.newLast = "new tang";
}}
>
修改对象属性
</button>
)}
</div>
);
}, MyExpObs);
connect函数是一个react高阶组件,它第二参数是传递你所使用的store类,它会监听当前store里所有的observable变量,如果有改变,就会重新rending。如果不传递第二个参数,会监听所有的obserable变量,包含其他的store类中的observable变量。 example地址
根据对象Path手动监听对象或子对象
改造第一个hooks的例子,我们来看看如何自己监听某个属性
import React, { useEffect, useState } from 'react'
import { ObsStore } from './observable-lite/obs-store'
import { useObservableStore } from "./observable-lite/hooks";
import { ChangeContext } from './observable-lite/utils/interfaces';
export default function TestUseObs() {
//定义一个对象类型的store
const [obj, _,key] = useObservableStore({
a: 1,
b: 2,
c: [{ d: 3 }, [{ f: 5 }]]
})
const [f,setF] =useState(null)
useEffect(()=>{
//开始手动订阅一个属性的变化
//定义一个对象的path,key名的数组,比如要监听obj.c[1][0].f这个属性的变化,我们可以如下定义
const path = ['c',1,0,'f']
//掉用store实例,调用subscribe方法订阅
const mark= ObsStore.getInstance().subscribe(path,(change:ChangeContext)=>{
console.log(change)
//回调获取新值
setF(change.newValue)
},key)
return function () {
//取消订阅
ObsStore.getInstance().unsubscribe(path,mark)
}
},[])
return (
<div>
<div>
<div>使用useObservableStore</div>
<div>当前对象:{JSON.stringify(obj)}</div>
<button onClick={() => {
obj.a += 1
obj.b += 1
obj.c[0].d += 1
obj.c[1][0].f += 1
}}>
点击对象累加
</button>
</div>
<div>
<p>手动观察某一个具体变量</p>
<span>obj.c[1][0].f:{f}</span>
</div>
</div>
);
}
subscribe 第三个参数需要传递一个key,这是针对hooks的情况,如果是使用@observable装饰器的变量,则不需要传递key。 具体可以看例子 example地址
后续
目前只是beta版本,所以并不建议大家在生产环境使用。后续我将会补一些jtest代码,完善功能,发npm包。当然后续也可以补上在vue中使用的例子。这段时间,欢迎大家也可以在评论区提意见,我们可以一起改进。最后感谢大家的阅读和支持。