只需要50行代码就可以突破Hooks的所有限制。大家好,我是凯森。讨厌Hooks的调用顺序限制(Hooks不能写在条件语句中)?你是否曾经在useEffect中使用过某个state,却忘记将其添加到dependencies中,导致useEffect回调的执行时机出现问题?怪自己粗心大意?怪自己不看文档?答应我,不要自责。根本原因是React没有将Hooks实现为响应式更新。是不是很难实现?本文将用50行代码实现无限版本的Hooks,涉及的知识也是Vue、Mobx等基于响应式更新的库的底层原理。这篇文章的正确吃法是收藏后在电脑上看,和我一起敲代码(完整的在线Demo链接见文末)。被手机党迷惑了也不要怪自己,那是你吃错了。注:本文代码来自RyanCarniato的文章BuildingaReactiveLibraryfromScratch。我的兄弟是SolidJS的作者。首先要做的是实现useState:functionuseState(value){constgetter=()=>value;constsetter=(newValue)=>value=newValue;return[getter,setter];}返回值数组第一项负责取值,第二项负责赋值。与React相比,我们有一个小的变化:返回值的第一个参数是一个函数而不是状态本身。像这样使用它:const[count,setCount]=useState(0);控制台日志(计数());//0设置计数(1);控制台日志(计数());//1没有黑魔法实现下一个useEffect,包括几个关键点:依赖状态变化,useEffect回调执行不需要显式指定依赖(即React中useEffect的第二个参数)例如:const[count,setCount]=useState(0);useEffect(()=>{window.title=count();})useEffect(()=>{console.log('Nothinghappenedtome')})计数变化后,第一个useEffect会执行回调(因为He内部依赖count),但是第二个useEffect不会执行。前端没有黑魔法,这里是怎么实现的?答案是:订阅发布。继续用上面的例子来说明建立订阅-发布关系的时机:const[count,setCount]=useState(0);useEffect(()=>{window.title=count();})当定义了useEffect时,它的回调会立即执行一次,内部会执行:window.title=count();当执行count时,effect和state之间就会建立订阅-发布关系。下次执行setCount(setter)时,会通知订阅了计数变化的useEffect,并执行其回调函数。数据结构之间的关系如图所示:每个useState里面都有一个setsubs,用来保存订阅状态变化的效果。effect是每个useEffect对应的数据结构:consteffect={execute,deps:newSet()}其中:execute:useEffect的回调函数deps:useEffect依赖的state对应subs的集合I知道你有点头晕。看上面的结构图,慢慢来,我们继续。实现useEffect首先需要一个栈来保存当前正在执行的效果。这样状态就知道在调用getter时要关联哪个效果。例如://effect1useEffect(()=>{window.title=count();})//effect2useEffect(()=>{console.log('nothingaboutme')})count需要执行知道了你在effect1(不是effect2)的上下文中允许你连接到effect1。//当前执行的效果栈consteffectStack=[];接下来实现useEffect,包括以下功能点:每次useEffect回调执行前重置依赖(回调内部stategetter会重建依赖关系)确保回调执行时的当前效果从顶部弹出当前效果的代码在effectStack的顶部执行回调后的堆栈如下://将effectStack压入栈顶。推(效果);尝试{回调();}finally{//PopeffectStack.pop();}}consteffect={execute,deps:newSet()}//立即执行一次并建立依赖execute();}cleanup用于移除effect与其依赖的所有状态之间的联系,包括:订阅关系:移除effect订阅的所有状态变化dependency:移除effect依赖的所有状态functioncleanup(effect){//删除效果订阅的所有状态更改for(constdepofeffect.deps){dep.delete(effect);}//移除所有effect依赖的状态effect.deps.clear();}移除后执行useEffect回调会一一重建关系。改造useState接下来改造useState,完成建立订阅-发布关系的逻辑。要点如下:在调用getter时获取当前上下文的effect,在关系建立并调用setter时通知所有订阅状态变化的effect。回调执行函数useState(value){//订阅列表constsubs=newSet();constgetter=()=>{//获取当前上下文的效果consteffect=effectStack[effectStack.length-1];if(effect){//建立连接subscribe(effect,subs);}返回值;}constsetter=(nextValue)=>{value=nextValue;//通知所有订阅状态变化的效果回调执行for(constsubof[...subs]){sub.execute();}}return[getter,setter];}subscribe的实现还包括建立两个关系:functionsubscribe(effect,subs){//建立订阅关系subs.add(effect);//依赖关系建立效果.deps.add(subs);}实验一下:const[name1,setName1]=useState('KaSong');useEffect(()=>console.log('Who'sthere!',name1()))//打印:谁在那里!KaSongsetName1('卡卡松');//打印:谁在那里!KaKaSong实现了useMemo,然后基于已有的两个hook实现了useMemo:functionuseMemo(callback){const[s,set]=useState();使用效果(()=>设置(回调()));返回s;}Automaticdependencytracking这套50行的Hooks还有一个强大的隐藏特性:automaticdependencytracking我们把上面的例子扩展一下:const[name1,setName1]=useState('KaSong');const[name2,setName2]=useState('小明');const[showAll,triggerShowAll]=useState(true);constwhoIsHere=useMemo(()=>{if(!showAll()){returnname1();}return`${name1()}and${name2()}`;})useEffect(()=>console.log('Who'sthere!',whoIsHere()))现在我们有3个状态:name1、name2、showAll。whoIsHere用作备忘录,取决于以上三种状态。最后,当whoIsHere发生变化时会触发useEffect回调。上面代码运行时,会根据初始的3个状态,计算whoIsHere,然后触发useEffect回调,print://print:whoisthere!卡松和小明接下来调用:setName1('卡卡松');//打印:谁在那里!卡卡松和小明triggerShowAll(false);//打印:谁在那里!卡卡松下面这个东西比较有意思,调用的时候:setName2('XiaoHong');并且没有打印日志。这是因为当triggerShowAll(false)导致showAll状态为false时,whoIsHere进入如下逻辑:if(!showAll()){returnname1();}由于name2没有被执行,name2与谁在这儿!只有在triggerShowAll(true)之后,whoIsHere才进入以下逻辑:return`${name1()}and${name2()}`;这时候whoIsHere又要依赖name1和name2了。自动依赖跟踪,是不是很酷?至此,我们基于订阅发布,实现了无限制的Hooks,可以自动追踪依赖关系。近年来是否使用了这个概念?早在2010年初,KnockoutJS就以这种细粒度的方式实现了响应式更新。不知道SteveSanderson(KnockoutJS的作者)是否预见到10年后,细粒度更新将广泛应用于各种库和框架。在这里:完整的在线演示链接
