背景
最近写了个很有意思的方法,感觉还蛮不错的就分享一下。起先是我在写calss组件的时候遇到一个问题,我需要监听一个导入的值,导入的值最开始是undefined
,经过异步操作以后会得到一个新的值,而我需要在这个class组件中拿到这个新的值去做操作。如果是在函数式组件中我们可以使用useEffect
去监听这个值的变化,虽然useEffect
监听的是state
和props
,但是经过我的实践发现在本案例中还是可以监听到的,先话不多说开始详细展开说说吧。
发现问题
假设我们有个class组件A,然后我们在组件A中导入一个文件:
import { ets } from "../sse";
在sse.js
文件中我们有创建、关闭eventsource
的方法以及它的实例,然后我们将他们导出这里就简单介绍下不展示代码了,这里的ets
就代表eventsource
的实例。
因为业务需要,我们并不是在这个组件A中去创建的eventsource
连接而是在另外一个组件经过请求后创建的,那么我们什么时候在组件A去获取实例就成了一个很大的问题,因为我不知道实例是什么时候被初始化的。
其实解决问题的方法有很多,例如组件的通讯、创建缓存并监听缓存的变化等等都是可以的,但是或多或少会有一些麻烦和其他问题。这里不管是函数式组件还是Vue都可以通过监听去很方便的完成,但是class组件就很难去完成了,因为class组件只能去监听state和prop的变化,那我们能不能简单实现一个简单的watch去完成一个简单的监听呢?接下来就听我娓娓道来。
解决
创建方法
我们参考Vue的watch
方法,这个方法需要一个依赖和一个依赖更新以后触发的回调函数,并且Vue是通过Proxy代理去监听的,既然有了大致的思路我们就可以开始去完成方法了。
首先我们在sse.js
文件中创建一个watchEventSourceCreate
方法,因为本案例中的依赖就是EventSource
实例,所以我们可以不用封装依赖变量。我们主要的思路就是创建一个proxy,然后再利用set
方法去调用我们传递的回调实现watch方法,具体方法如下:
let proxy;
const watchEventSourceCreate = (func, context) => {
if (ets) {
func(ets);
} else {
proxy = new Proxy(
{},
{
get(target, key) {
return target[key];
},
set(target, key, value) {
if (key === "ets") {
func.bind(context)(value);
}
target[key] = value;
return true;
},
}
);
}
};
首先我创建了一个proxy变量,然后在我们创建的watchEventSourceCreate
方法中创建代理,为什么我们不在申明proxy
变量的时候去创建这个代理呢,原因很简单,我们需要在set方法中去调用我们的目标函数,但是在sse.js
文件中我们只能在watchEventSourceCreate
方法中拿到这个回调函数,所以我们声明这个代理必须要在watchEventSourceCreate
中。
剩下的就比较好理解了,判断是否赋值ets
,如果赋值了就调用回调函数,记得绑定this,虽然在本案例中不绑定也可以,但是这里还是为了保证稳定下绑定了this。
接下来的就是在创建eventsource的方法中去赋值。这一步很简单:
if (proxy) {
proxy.ets= ets;
}
使用
我们在组件A导入我们创建好的方法:
import { ets, watchEventSourceCreate } from "../sse";
因为这个watch需要传入一个触发后的处理函数,所以我们先定义一个处理函数,这个函数会被传入一个参数,这个参数在本案例中就是eventSource
实例,其实要不要这个形参都不重要,因为我们主要是需要判断他的触发时机
handle(ets){
//处理逻辑
、、、
}
然后在componentDidMount
钩子函数中去创建这个watch:
componentDidMount(){
watchEventSourceCreate (this.handle, this)
}
需要注意的是在本案例中handle
会在被创建后立即触发,因为我的业务需要是创建eventsource
的监听,所以我只需要获取到实例就好,如果需要更深逻辑的判断我们可在set中将旧值和新值都传给目标函数,这样就可以在目标函数中去做比较,这样的话就更贴近Vue的Watch一点。
拓展
其实除了使用watch,我们还可以使用一个递归方法去判断ets
的值。
import { ets } from "../sse";
componentDidMount(){
this.handle()
}
handle(){
if(ets){
//逻辑...
}else{
this.handle()
}
}
说说我为什么不这么写,首先就是这么写是很危险的,单纯的通过ets
是否有值去判断是否进行递归肯定是不可信的更何况ets
的赋值过程是位置的,这必然不保险。如果说我们使用定时间去调用咋一看是可以的,但是假设场景是你需要在handle中去创建一个新的EventSource
或者Websocket
,我们需要根据推送的数据去做展示,那么实时性是很难保证的,如果间隔过大很明显是不合理的,间隔过小和递归的区别意义不大,所以监听是更好的选择。
看了上面的代码不知道大家有没有发现一个问题,那就是我第一次获取导入的ets
的值是在什么时候?很明显是componentDidMount
也就是组件挂载完毕的时候对吧,但是如果是解构的话为什么递归以后会拿到在sse.js
文件赋的值呢?
看下面的例子:
let obj1 = {
a:1,
b:2,
c:0,
add(){this.c = this.a + this.b + this.c}
}
let {a,b,c,add}= obj1
我创建了一个obj1
的对象然后解构赋值,这个时候我们初始化打印试试:
这里的初始值都是0,接下来我分别调用obj1的add方法和解构后的方法看看结果:
这里可以很明显的看到,解构出来的值都是独立之前的值的,这里解构更多的像是一种赋值。
那回过头来看我们的案例里面的etc
这个值,在最开始componentDidMount
中拿到的etc
毋庸置疑是undefined
,更新etc
的值的方法只在sse.js
文件中存在,那么为什么我们在递归到etc
有值的时候能正确的拿到etc
的值呢?用上面解构的逻辑按理来说我们无论如何拿到的都是etc
的初始值也就是undefined
。
结论
很明显,我们能得出一个结论:ESmodule的导入并不等同于解构尽管写法和现象都非常相似。
我们可以认为解构就是一个赋值的过程,而模块导入是将导入的值放在了一个对象中,我们导入使用值的时候更多的是像使用这个对象的某一个属性,当这个对象的值被更改时我们使用的值也同样被更改了。这一点其实有点像基本数据和引用类型的数据的区别,可以大致参考这个模板去理解,如果知道js基本数据和引用类型数据的各种特点应该能很好的get到这点