开始
依稀记得去年的时候,有面试官问我:怎么理解React Hook?当时我还没有使用React开发项目的经验,当然也没有对Hook会有什么深入的了解,所以当时只是应付回答了一下,估计当时的面试官对于我的回答应该是挺失望的(因为这次面试完后就没有啥消息了);好吧,直到现在,我想我已经有了一个还算不错的答案,现在记录下来,方便以后跟面试官吹牛逼。
天使与魔鬼
Hook简直就是天使与魔鬼混血儿,关于Hook的出现在某乎早就争论不休,大体是有的人是激进派,觉得Hook是颠覆性的,全项目切换到Hook方式开发;有的人则是保守派,复杂的组件仍然使用传统类组件,简单的组件则使用hook开发;有的人是反对派,对于Hook完全持否定态度。
总结自己最近的使用感受,完全理解为什么这些人的观点有如此大的差距;首先Hook的出现有它实实在在的好处(后面再分析),但是获得这种好处也是有代价的:要开发者自己维护依赖列表明显是一种心智负担,你在处理复杂的业务的时候,还要小心翼翼检查useCallback/useEffect的依赖列表,只要写少一个依赖项你的代码就有潜在的bug,有时候觉得要么干脆不用useCallback还好,大不了就性能低一点,至少不会引入潜在的bug;但是回头优化性能的时候,useCallback又是完全绕不过去的,真让人纠结;当然有人提到使用一些检查工具可以提供检查,不用人工检查那么麻烦,但是始终觉得怪怪的。另外一个问题就是代码逻辑的抽取,稍加不注意,很容易把所有逻辑都塞到一个function里面,导致函数代码动辄几百上千行代码,所以要用好Hook的前提条件是开发者对组件已经有一个了然于胸的规划(对于习惯代码一把梭的人真的好艰难)。
再说说Hook的天使的一面,从声明组件方面来说,确实简单了不少,也不用记那么多烦人的生命周期函数;然后最核心的地方是,它提供一种新的抽象方式,在传统的类组件,我们抽取出一个新的组件目的一般都是:
- 从复用的角度出发,复用这个组件逻辑
- 从维护的角度出发,简简单单的抽出组件逻辑,不要把所有逻辑都塞到一个组件里面
所以在第二点,一般处理一个复杂的组件会抽取出一个负责业务的Controller组件,一个负责渲染的Dumb组件;但是我们很快就会发现这样以组件为粒度划分逻辑,粒度太大了,当我们另外一个组件想重用Controller组件的一小部分逻辑,我们应该怎么办,若是用继承,会把一些无用的逻辑带入,而且继承也还有其他的问题;所以组合才是最优的选择,或许我们应该再划分出一个公用的逻辑组件,让他们组件间可以自由组合,但是随着大家热火朝天的重构,越来越多的业务组件被创造出来,而整个组件树也会慢慢跟着膨胀,这导致每次更新的时候都要diff一棵越来越大的组件树,明显类组件已经到了一个瓶颈;
而这个时候Hook开始闪亮登上了舞台,那么怎么用它来破解这个问题尼。
问题分解
(温馨提示,下面是个人理解,可能会有错误误导的地方)
先抛开Hook,再来创建一个新的名词Scope,那么Scope的定义如下:
没错Scope好像类的定义一样,是方法和状态的集合体,只不过它是基于Hook;为啥不干脆直接定义一个普通的类,因为Hook还提供了setState,useEffect等能力,这是普通的类做不到的,或者要花费一些心思才能做到,既然官方提供了这种方式,肯定是最好的一种方式。
作为Scope,它是可以保持逻辑的独立并且粒度最小,当然还有能够直接介入到宿主组件的生命周期里面。
其实如果加上Render Function,它也应该算是一个组件了,只是它的生命周期依赖宿主。
那么为啥不再加上Render Function,因为表现形式是多变的,但是逻辑就不一定了,所以保持逻辑抽象即可。
那么现在组件定义如下:
这里就很好理解了,现在问题的关键点就是Scope之间的关系了,如果Scope之间互相独立,没啥依赖那就完美,但是这是不可能,很多时候它们之间的关系还特别强,在很多hook示例里面它们之间的关系都是通过调用useXxx的时候把依赖当做参数传入,但是很容易导致相互依赖的情况,例如平常情况:
TableScope需要能够点击打开对话框,DialogScope需要confirm之后去刷新表格,很明显这两者创建的时候都要依赖对方,到底是useDialog先还是useTable先尼;
我的建议是不要把依赖当做参数传入,个人解法:
function Demo() {
//组件公共状态
const [stateA, setStateA] = useState('');
const [stateB, setStateB] = useState('');
//创建各种Scope
const dialog = useDialog({sateA, setStateA}); //Scope只依赖公共的状态和方法,不允许创建开始互相依赖
const table = useTable({stateB, setStateB});
//通过提供回调,处理Scope之间各种联系
dialog.onConfirm(()=> {
table.reload();
});
table.onClick(()=> {
dialog.open();
});
//组件生命回调
useEffect(()=> {
....
}, []);
return ...
}
Scope之间不允许创建的时候相互依赖,它们之间的联系统一在后面一段代码中处理,这样有什么好处尼?这样的话实现的时候,可以更加专注Scope本身状态和业务,也不需要担心创建的时候会出现相互依赖情况。最后组件代码分段也十分清晰,大体能分成5段:
- 声明公共的状态和行为
- 创建各种Scope
- 建立Scope之间的联系
- 组件生命周期回调
- 返回渲染的节点树
嗯。。最终的目标就是建立一种规范,让组件的结构更加清晰,不会出现一片混乱的情况。
更进一步
前一节,更多是从抽象出发,如何合理利用Hook去组织代码,并且建立一种规范;后面就来想想如何解决Hook很多吐槽的问题:依赖列表。
正如之前说的,例如useCallback的使用场景,如果依赖列表少写一项,就有可能埋下隐藏的bug,而且这种bug极难会被发现,因为依赖列表有可能很长,后面的维护者或者帮你review代码的人不可能会仔细看依赖列表有没有缺漏;所以我对useCallback基本上都是抵触的,宁愿不用,也不想埋下bug,到时候调试得满头大汗,甚至我也开始怀疑是否要继续使用Hook来开发组件。
直到有一天看到Vue3的Composition API(没错Vue又又又借鉴了一次React),感觉Hook遇上Mutable其实也是很美妙。那么我们有没有办法通过依赖搜集来解决依赖列表的问题,实现更加简洁的api,如果可以我们就可以像Vue3那样使用useCallback和useEffect:
const refA = useRef(0);
const refB = useRef(0);
const refC = useRef(0);
useCallbck(()=> {
consoel.log(refA.value + refB.vlue)
});
useEffect(()=> {
refC.value = refA.value - refB.value;
})
...
代码上基本消灭了依赖列表,只是在变量使用上就稍微那麻烦了一丁点,但是依赖列表不用再维护,心智负担瞬间下降了许多。
问题怎么去实现这种代码效果?
下面是一个简单的实现:
const ACTIVE_EFFECT = null;
function useReactiveState(initial) {
const [state, setState] = useState(initial);
const ref = useRef(state);
const effects = useRef(new Set());
return useMemo(()=> {
const memoState = {
get value() {
if(ACTIVE_EFFECT) {
ACTIVE_EFFECT.deps.add(memoState);
effects.current.add(ACTIVE_EFFECT);
}
return ref.current;
},
set value(newValue) {
ref.current = newValue;
setState(newValue);
effects.current.forEach((effect)=> {
effect.update();
})
},
removeEffect(effect) {
effects.current.remove(effect)
}
}
return memoState;
}, [])
}
function useReactiveEffect(call) {
const [flag, setFlag] = useState(0);
const effect = useRef({
deps: new Set(),
update: ()=> {
setFlag((flag)=> flag + 1);
}
});
useEffect(()=> {
const prevEffect = ACTIVE_EFFECT;
effect.current.deps.forEach((dep)=> {
dep.removeEffect(effect);
});
effect.current.deps = [];
const result = call();
ACTIVE_EFFECT = prevEffect;
return reuslt;
}, [flag])
}
useCallback的实现同理,虽然没有怎么深入测试,大体功能还是能够实现的。
结束
好吧,对React Hook的思考暂时到此为止,等以后再有新的感悟再继续写React Hook的相关的文章吧,最后还是那句,如有错漏,请大家能够及时指出,谢谢大家的捧场。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。