在界面编写方面,我十分喜爱 React Hooks + 函数组件的写法。
然而在写简单利用的时候,我更喜爱应用面向对象的写法,所以当我用面向对象写法写了一大堆,因为我心愿利用与视图剥离,只在实现的时候才绑定视图,甚至可能把渲染办法换成 Vue 等其余计划,所以没有思考基于 React.Component 的计划来开发。
然而等我终于打算渲染进去看成果的时候却犯了难:我不能在类的办法中应用 hooks ,而应用类组件又十分繁琐。
1. 首次尝试:应用 React Hooks
1.1 间接应用是行不通的
最好能够间接在类的办法外面间接应用 Hook ,当然我本人会做一些 Hook 的绑定工作:
// 留神这里的 ViewComponent 并非继承至 React.Componentclass 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 接口来填即可。