在界面编写方面,我非常喜欢 React Hooks + 函数组件的写法。
但是在写复杂应用的时候,我更喜欢使用面向对象的写法,所以当我用面向对象写法写了一大堆,由于我希望应用与视图剥离,只在实现的时候才绑定视图,甚至可能把渲染方法换成 Vue 等其他方案,所以没有考虑基于 React.Component 的方案来开发。
但是等我终于打算渲染出来看效果的时候却犯了难:我不能在类的方法中使用 hooks ,而使用类组件又非常繁琐。
1. 首次尝试:使用 React Hooks
1.1 直接使用是行不通的
最好可以直接在类的方法里面直接使用 Hook ,当然我自己会做一些 Hook 的绑定工作:
// 注意这里的 ViewComponent 并非继承至 React.Component
class Test extends Editor.ViewComponent {
render() {
const [{ count }, setCount] = useState(this.data);
// 使用自定义的事件机制把 setState 和类的 .data 属性关联起来
// 之所以要这样做是因为 Editor.ViewComponent 有自己的属性管理
// 机制,包括大量不应直接影响渲染的东西
useEffect(() => {
this.onDataChange(setCount);
return () => this.offDataChange(setCount);
}, []);
return (
<button onClick={this.changeData({ count: this.data.count + 1 })}>
点击次数: {count}
</button>
);
}
}
👆然而任何一个熟悉 React Hooks 的人都知道,React Hooks 必须是在纯函数里面使用,而类组件的 this
自带状态,所以这段代码不能通过编译。
1.2 换个法子——使用回调订阅机制
具体的实现方法就是使用一个通信机制,将类实例中的变化传递到一个纯函数里面去,纯函数中搜集的事件也通过此方法回传:
// LayerButtonBus 专门给 LayerButtons 和对应的纯函数“跑腿”
export class LayerButtonBus extends ViewEventBus {
static HIDDEN_LAYER = "HIDDEN_LAYER";
static SHOW_LAYER = "HIDDEN_LAYER";
static DELETE_LAYER = "DELETE_LAYER";
}
// LayerButtonRender 是专门给 LayerButtons 渲染视图的纯函数
function LayerButtonRender({ viewBus }: { viewBus: LayerButtonBus }) {
// …… 细节代码极其繁琐,就不贴出来了
return <div>{
buttons.map((Button, params) => <Button {...params}/>)
}<div/>
}
// LayerButton 本尊
export class LayerButton extends Editor.ViewComponent{
render(){
return <LayerButtonRender viewBus={this.viewEventBus} />;
}
}
👆 这样写是可以运行的,但是每一种有状态的视图类都需要这样“三件套”,心智负担和工作量都相当大,非人哉!
2. 放弃 React Hooks ,使用类组件
类组件是可以直接使用的,只要你不嫌麻烦:
class Test extends Editor.ViewComponent {
render(){
return class View extends React.Component{
// 这里就省略掉实现细节了
render(){
return <button>点击次数:{this.state.count}</button>
}
}
}
}
👆其实也没什么大问题,但是写类组件也算是繁琐事情一大桩,并且不能使用 Hooks 。
3. 另辟蹊径
类组件工厂
上面这种使用类组件的写法是可行的,那么只需要把类组件的实现细节封装起来,就可以了:
const {
bindReactClass
} = Editor.ViewComponent;
class Test extends Editor.ViewComponent {
render(){
const _this = this;
return bindReactClass({
state: _this.data,
// 在 constructor 里执行:
onInit(){
_this.onDataChange(this.setState);
},
// componentWillUnmount
willUnmount(){
_this.offDataChange(this.setState);
},
// 渲染视图
jsx: <button>点击次数:{_this.data.count}</button>
})
}
}
👆虽然看起来像是在写 Vue ,但是比起手撸一个完整的类还是方便了不少。
基于闭包特性的 “React Hooks” 的
React 的函数组件虽然是“纯函数”,但我要说:因为 Hooks 的引入,函数组件其实已经不那么“纯”了,所以我们大可不必继续“装纯”,拥有状态的同时,也要拥抱自主可控的副作用。
那么函数里面什么东西最适合用来搞副作用呢?答案已经呼之欲出了——闭包特性——在那个蛮荒的年代我们曾使用这种特性搞定私有属性,而如今它可以为我们的函数写法维持状态。
我相信大部分习惯 React Hooks 的人可以很快适应这种写法:
const Test = ({ useState }: HooksType) => {
// stateSample 只是一个样子货,不具备响应性,不能直接用来渲染
// 算是一种新的“闭包陷阱”
const [countSample, setCount] = useState(count);
return ({ count }: typeof { count: countSample }) => {
// 这里只用于渲染,不能调用 “Hooks”,不然行为会变得诡异
return <button onClick = {()=>setCount(count + 1)} >点击次数:{count}</button>
}
}
不过具体的实现上, state 使用的是类组件的玩法,也就是一个组件只有一个 state ,使用键值对来维护,主要是因为懒得改。具体的实现如下(注意和上面使用的不是同一套方案):
const hookLikeClousure = (fun: typeof func) => {
return class extends Component {
state = {};
private renderer!: (arg: any) => ReactNode;
private watchedRefs = new Array<[RefObject<any>, (ref: any) => any]>();
private initState!:{};
constructor(props = {}) {
super(props);
let activedState = false;
this.renderer = fun({
getState: (initState) => {
if (activedState) {
throw new Error(
"一个组件只能声明一次状态,多个状态请置为同一个对象的不同属性"
);
}
activedState = true;
this.initState = initState;
return (newState: {}) => {
this.setState(newState);
};
},
awaitRef: <R extends any>(callback: (ref: R) => any) => {
const ref = createRef<R>();
this.watchedRefs.push([ref, callback]);
return ref;
},
});
}
componentDidMount(){
this.setState(this.initState);
this.watchedRefs.forEach(([ref, callback]) => {
callback(ref.current);
});
}
render(): ReactNode {
return <>{this.renderer(this.state)}</>;
}
};
};
👆 把 ref 的行为改了一改,但是隐约觉得有什么坑在里头,所以命名也没有沿袭 useRef
而是使用 awaitRef
,之后遇到坑的话需要就定义其他的 ref 接口来填即可。