一、引子
阴差阳错,造化弄人,面试Vue,入职React,逼不得已也要看一下React文档了。其实React语法和JSX,事件,props等都算简单,重点还是看Hooks,理解到底怎么玩才是正道!语言就是一种宗教,我很认同这句话,框架又何尝不是一种宗教,它制造一些术语,营造一些光环,然后把人的思想都拉拢过来,圈养为自己的信徒!虽然Vue 和 React的开发风格大相径庭,它们思想不同,API迥异,但从应用开发的角度上,肯定还是有最好的标准和规范的,也可以用相同的模式进行项目架构!MVVM的架构风格就是最好的风格,最好的代码实践,抛却组件要细分、高阶组件、props传值,多用Memo,shouldComponentUpdate .......这些扰人心绪的东西,来看看React下的简单的MV HOOK吧!
二、目标----创建清晰的react的VM模型
现在吃React这碗饭了,本着干一行,爱一行的心态,我恶补了一些Hook的知识,香,真香,都来尝尝。(Hook文档这块,官方文档连完整的API都没有,全是靠各个博客文章去理解,😅)
我认为React 作为一个数一数二的MVVM框架,一直强调它的组件能力,Hooks特性,但它好像缺少了 VM的概念了,越来越来偏离MVVM框架了。VM就是把state和所有的action包装到一起的一个对象,不仅代码组织简单,还能有效避免jQuery时期那种基于事件驱动的开发模式中,容易引发的“事件纠缠”现象!
我在之前的Vue3的项目中,构造了一种开发模式,我将其叫为pageState 或者pageCore模式,它其实就是指导如何编写一个页面的VM模型, 参考下图:
这就是一个典型的VM模型,有state和action,为了简化,action写为fn, 上面还有computed这种计算属性,当然这里也可以写watch函数!然后把这个VM模型直接往模板中去绑定即可。
于是我的目标有了--------React是没有这种VM模型的Hook,所以我必须把React Hook揉成我要的姿势才行。
三、成果----编写 Todo页面仅需要2步
经过几天的尝试,终于初见成果。我引入2个新函数: useVM 和 genReducer来解决这个问题。
useVM的底层仍然是useReducer来实现的,它用来把状态和函数封装后,返回一个统一的VM模型
genReducer 只是一个工具函数,把一个fn对象包装成reducer函数。
这2个函数结构如下:
// 将初始值,fn,reducer传入, 返回一个vm模型,即{ state,fn }!
function useVM(rawState, rawFn, reducer){
// 省略.........
return {state,fn}
}
// 辅助函数,用于生成一个reducer函数
// (它其实也可以写到useVM里面, 由于useVM执行多次,抽出来提高性能)
function genReducer(rawFn) {
return function reducer(){
// 省略..........
}
}
有了这两个函数,那我们就利用它们,写一个Todo Demo的页面吧!
第一步,编写useTodo 的自定义Hook,用它来生成一个vm.
useTodo函数其实就是todo页面的VM模型, 先看它书写后的整体结构: (具体代码在下面找)
整体结构一共6个步骤,脉络比较清晰,所有步骤都是为了useTodo这个Hook服务的。
首先编写 ⑴初始state和 ⑵所有的fn方法, 这个fn是一个普通对象,还要把它转为一个 ⑶reducer函数,才能被useReducer所用。
⑴ ⑵ ⑶准备好了,就可以写⑷页面自定义Hook了, 把123传入useVM方法,返回⑹ {todoState, todoFn} 就可以模板可引用的 ,此时一个vm就完成了。
如果需要计算属性和副作用函数,直接写在⑸扩展功能的位置 即可,此处useMemo useEffect useRef等所有的标准Hook都可以使用,任意发挥!
第二步,编写todo组件
todo组件的编写非常简单、直白,首先引入 useTodo(), 获取一个vm模型,此后直接写JSX即可!
能够如此书写JSX,这都是useVM的功劳,极大简化了useReducer的使用模式。看了上面的代码,不知道各位看官们,是否接受这种VM模型+组件JSX的开始方式呢?
四、原理、规范和要点
1、原理
useVM底层是一个useReducer在运行,它巧妙的把dispatch+reducer的代码模式,转换为 一个Js对象+一组 fn 的代码模式,这个清晰的VM模型,就简化了我们的使用方式和心智负担!函数调用的内部流程为:
todoFn.XXX(参数) 的函数调用
dispatch("xxxx", 参数)
reducer执行
原始的fn.XXX(preState,参数)调用
返回一个键值对象给reducer函数
更新state
组件update
2、代码编写规范和要点
- 编写state: 必须是JS对象格式,且只包含页面上原始状态值即可, 计算属性等值写在后面第5步中。
- 编写fn: 必须是JS对象格式,对应页面上所有的事件的地方。 详见示例代码!
fn中的每一个方法,第一个参数必须是preState, 后面可以有任意参数。
fn中方法为同步方法时,必须返回值是state的一部分的对象,
fn中的方法中包含异步逻辑时,必须返回一个Promise对象, 这个对象的reslove值必须是state的一部分的对象。
异步方法的命名必须以 Async 或 $ 结尾, 以区别同步方法。(已经统一同步、异步方法,不需要函数名来区分了) - 生成reducer: 简单用genReducer(fn) 包裹一下即可。
- 调用 useVM : 传入state, fn ,reducer, 最终返回一个vm对象。
- 扩展属性、其它生命周期钩子,第三方hook的编写:
扩展属性:相当于计算属性, 即可用原生的js编写,挂载到vm对象 上即可, 也可以用useMemo包裹一下。
生命周期钩子:用useEffect可以模拟出 mount, update, unmount三种生命周期的逻辑。 这部分知识请多学习React Hook的知识!
其它React内置Hook,第三方Hook: 均可以按需使用,比如useState,useRef,useCallback等。
当然与vm逻辑无关的hook,还是建议写到组件的函数顶部中去,没必要强行写在useVM这里。 - 页面的编写:直接调用 useTodo函数返回vm模型后, 直接编写相应的JSX渲染函数即可!
页面中调用fn方法时,第一个参数preState是不能传递的,这个参数是在内部的reducer中,自动注入为参数的!
见下图示例:
五、Context----与子组件共享vm模型
我是极不鼓励把页面拆分成多个细小的组件的,但是很多人会以拆分组件为荣,不拆分组件就各种不爽的情况。幸好我们借助useVM编写的VM模型其实是很容易跟子组件,深层组件共享的,方法就是使用React内置的Context概念。由于Context这里,我并没有做任何的封装工作,就是最官方的写法,所以我简单贴一下使用方法,供大家参考就算了!
父组件:
深层子组件
六、附录,源码
感谢这几天学习Hook中,读的一些文章的作者们,我就不一一赘述了,因为我只看文章,没看你们的名字。来阅读我文章的人,也没必要看我的名字,能够不同的时空集合中,我们有过短暂的交流与感应已属万幸。
这个Hook的难点其实是处理异步函数, 可恶useReducer只支持同步的dispatch,在这块绕了一些弯路的!
我认真接触React时间短,虽然在React上,我认为有许多槽点,可能是源于我认知误区,以后React就是我的衣食父母了,还是要尊重一下了。
项目的源码上传到gitee的我的仓库中,感兴趣可以看一下。
源码仓库: noonoo/react-Start (gitee.com) 里面有2个分支
master分支:create-react-app创建的模板项目
proj 分支: 使用 vite 创建的项目,且增加辅助功能。
目前proj分支包含: useVm:底层用 state+fn +useReducer + Context 的模式实现的一个结构
useRefVm: 底层使用 Es6 Class + useRef +forceUpdate 实现的一个结构( 推荐)
Todo2示例: 底层使用Mobx 实现state +fn 实现vm
TodoCls示例: 底层使用Mobx + Es6 Class 实现vm( 推荐)