写react项目的时候表单联动状态过多,不想频繁的写setState。想像vue那样直接赋值,感觉美滋滋。 研究了一下,感觉使用代理代理set可以实现差不多的效果,试了下可行,废话不多说直接开干。
1、ES6 proxy
proxy 为目标提供一个代理,可以对外界的访问进行过滤和改写。属于一种'元编程',则对编程语言进行编程。
语法:
const proxy = new Proxy(target, ProxyHandler);
target:
Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。 ProxyHandler: 一个通常以函数为属性的对象,个属性中的函数分别定义了在执行各种操作时代理p的行为。 可代理的行为详细见MDN: Proxy - JavaScript | MDN (mozilla.org)
这次创建响应式的state 只需要代理其get方法,并且使用Proxy.revocable来创建可撤销的代理,尽可能的让我们创建的对象被垃圾回收机制回收掉。
2、尝试一下:
const targetObj = { name: '张三' }
const proxyObj = new Proxy(targetObj, {
set: (target, key ,value, receiver) => {
console.log(target, key ,value, receiver)
return true
}
})
proxyObj.name = '李四'
// console: {"name":"张三"},name,李四,{"name":"张三"}
3、reactive hook
了解了proxy 之后开始正式整个hook
import { useEffect, useState } from "react";
const useReactive = (initialState) => {
const [state, setState] = useState(initialState);
const preProxyState = useRef();
const proxyHandler = {
set: (target, key, value) => {
// 不创建新的对象,setState会比较引用地址没有发生变化,不会更新state
setState({ ...target, [key]: value });
return true;
},
};
let stateProxy = Proxy.revocable(state, proxyHandler);
useEffect(() => {
if (preProxyState.current) {
preProxyState.current.revoke();
}
preProxyState.current = stateProxy;
return () => {
// 页面卸载的时候取消代理
stateProxy.revoke();
stateProxy = null;
};
}, [stateProxy]);
return [stateProxy.proxy, setState];
};
export default useReactive;
在页面中引入使用
import useReactive from "./useReactive";
const Test = () => {
const [state, setState] = useReactive({
name: "Petter",
});
function changeState() {
// 直接赋值则可以出发页面变化
state.name = "Mick";
}
return (
<div className="App">
<div> {state.name}</div>
<button onClick={changeState}>toggle state</button>
</div>
);
};
点击toggle state 按钮页面显示的name成功变化,一个简单的响应式state就完成了,弄完之后会有一个问题,响应式的state是同步的还是异步的呢。
import useReactive from "./useReactive";
const Test = () => {
const [state, setState] = useReactive({
name: "Petter",
});
function changeState() {
state.name = "Mick1";
state.name = "Mick2";
state.name = "Mick3";
state.name = "Mick4";
state.name = "Mick5";
}
console.log(state); // 输出了多少次就重新渲染了多少次
return (
<div className="App">
<div> {state.name}</div>
<button onClick={changeState}>toggle state</button>
</div>
);
};
控制台输出一次,可以发现多次赋值,合并成一次执行,可以与useState一样使用。
4、支持数组
该hook使用一个对象作为初始化state,好像不能满足数组作为state同学的需求。接着继续改造一下:
import { useEffect, useState } from "react";
// ProxyHandler 对象
class ProxyHandler {
constructor(state = {}, setState) {
this.state = state;
this.setState = setState;
}
set(target, key, value) {
let result = false;
switch (this.getType()) {
case "Object":
result = this.objectSet(target, key, value);
break;
case "Array":
result = this.arraySet(target, key, value);
break;
}
return result;
}
arraySet(target, key, value) {
const newTarget = target.concat();
newTarget[key] = value;
this.setState(newTarget);
return true;
}
objectSet(target, key, value) {
this.setState({ ...target, [key]: value });
return true;
}
getType() {
return Object.prototype.toString.call(this.state).slice(8, -1);
}
}
const useReactive = (initialState) => {
const [state, setState] = useState(initialState);
const _ProxyHandler = new ProxyHandler(state, setState);
const preProxyState = useRef();
const proxyHandler = {
set: _ProxyHandler.set.bind(_ProxyHandler), // 需要重新绑定this
};
let stateProxy = Proxy.revocable(state, proxyHandler);
useEffect(() => {
if (preProxyState.current) {
preProxyState.current.revoke();
}
preProxyState.current = stateProxy;
return () => {
// 页面卸载的时候取消代理
stateProxy.revoke();
stateProxy = null;
};
}, [stateProxy]);
return [stateProxy.proxy, setState];
};
页面中尝试一下:
const Test = () => {
// const [state, setState] = useReactive({
// name: "Petter",
// });
const [state, setState] = useReactive(["zhangsan"]);
function changeState() {
// state.name = "Mick";
state[0] = "mick";
}
console.log(state);
return (
<div className="App">
<div> {state[0]}</div>
<button onClick={changeState}>toggle state</button>
</div>
);
};
重新赋值页面变化,看起来还不错。
5、一些问题
-
深层级对象赋值。
但是,项目的需求瞬息万变,一个对象下面可能会有多层级的属性。对深层级的对象赋值是不会触发页面的变化的。例如:
state.people.name = 'Mike'
解决办法:拷贝变化后的people对象,重新赋值给state.people即可,数组同样。const _people = {...state.people,name: 'Mike'} state.people = _people
-
替换整个对象
若需要改变整个对象怎么办呢,userReacive同样返回[state, setState],使用setState重新设置state就行了。 -
初始值必须为ES6 Proxy 支持的类型。 源代码: Git