开始

依稀记得去年的时候,有面试官问我:怎么理解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的定义如下:
截屏2020-04-04下午4.54.29.png

没错Scope好像类的定义一样,是方法和状态的集合体,只不过它是基于Hook;为啥不干脆直接定义一个普通的类,因为Hook还提供了setState,useEffect等能力,这是普通的类做不到的,或者要花费一些心思才能做到,既然官方提供了这种方式,肯定是最好的一种方式。
作为Scope,它是可以保持逻辑的独立并且粒度最小,当然还有能够直接介入到宿主组件的生命周期里面。
其实如果加上Render Function,它也应该算是一个组件了,只是它的生命周期依赖宿主。
那么为啥不再加上Render Function,因为表现形式是多变的,但是逻辑就不一定了,所以保持逻辑抽象即可。

那么现在组件定义如下:
截屏2020-04-04下午4.52.48.png

这里就很好理解了,现在问题的关键点就是Scope之间的关系了,如果Scope之间互相独立,没啥依赖那就完美,但是这是不可能,很多时候它们之间的关系还特别强,在很多hook示例里面它们之间的关系都是通过调用useXxx的时候把依赖当做参数传入,但是很容易导致相互依赖的情况,例如平常情况:
截屏2020-04-04下午5.24.34.png

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段:

  1. 声明公共的状态和行为
  2. 创建各种Scope
  3. 建立Scope之间的联系
  4. 组件生命周期回调
  5. 返回渲染的节点树

嗯。。最终的目标就是建立一种规范,让组件的结构更加清晰,不会出现一片混乱的情况。

更进一步

前一节,更多是从抽象出发,如何合理利用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的相关的文章吧,最后还是那句,如有错漏,请大家能够及时指出,谢谢大家的捧场。


tain335
576 声望196 粉丝

Keep it simple.