当前位置: 首页 > 科技观察

说说我对ReactHooks的理解

时间:2023-03-20 20:05:48 科技观察

日常开发中经常用到的ReactHooks。useEffect和useState会让你感到困惑吗?本篇文章基于《a complete guide to useeffect》和作者的思考。也希望对读者有所启发。0x00useEffectinReactReact中有很多Hooks,其中useEffect使用频率很高,用来包装一些有副作用的函数。使用Hook的好处包括:增强的可重用性、函数组件的有状态数据获取、订阅或手动修改DOM是副作用。效果将在每次React渲染后执行。如果有一些副作用代码需要同步,可以用useLayoutEffect包裹起来。它的用法类似于useEffect。useEffect有两个参数。第一个传递一个函数,第二个参数作为第一个参数中的函数是否执行的判断标准。也就是说,第二个参数数组中的变量是否发生变化决定了函数是否被执行。函数是否执行取决于第二个参数的值是否为Variety。React中的比较是一种浅层相等(shallowcomparison)。对于深层对象嵌套,无法准确判断是否有变化。useEffect使用了JS的闭包机制。可以说第一个参数是一个闭包函数,它在函数组件的范围内,可以访问其中的局部变量和函数。多个useEffects串联起来,根据函数是否执行(依赖值是否变化)依次挂载到执行链上。在类组件中,有一个生命周期的概念。在一些reacthooks的文章中,我们经常看到如何使用useEffect来模拟componentDidmount和componentUnmount的例子,第二个参数是一个空数组[],使得effect在组件挂载时执行一次,返回函数在卸载组件时执行。也是闭包的关系。通过返回一个函数,实现闭包,在React下次运行effect之前执行返回的函数,用于清除副作用。0x01搭建ReactHooks的心智模型刚开始接触reacthooks的时候,感觉代码的执行有点违背常理,花了很多时间为react搭建一个合理的心智模型。函数式组件(FunctionalComponent)没有生命周期的概念,React控制更新,频繁更新但是有的值会变,有的不会变,这让程序的可理解性变差了。但是经过不断的学习和应用,我个人认为hooks其实是一种非常轻量级的方式。在项目构建中,开发自定义钩子,然后在应用程序的任何地方调用钩子,类似插件(Pluggable)开发降低代码耦合。但是也带来了一些麻烦的事情。有的同学一个hook写了很多代码,分离出来的效果也混在一起。再加上多维变量控制,其他同学很难看懂。你在干什么。解决hook内部代码复杂的问题,首先要明确当前hook的工作,工作是否可以拆分,hook中是否可以调用其他hook,多个也可以钩子要分开?或者整理(整理)代码运行逻辑?React中的每个渲染在effectReact中都有自己的钩子更新。我觉得可以算是“快照”,每次更新都是一个“快照”。这个snapshot中的变量值是不变的,每个snapshot都会因为react的更新而产生串行(deductible)的差异,每次生效的函数都是一个新的函数。我心目中的hooks模型,简单来说,就是一个插件化的、有状态的、有序的工具函数。0x02useEffect对于useEffect,React的每次更新都会根据useEffect第二个参数中的依赖判断是否执行被包装的函数。React会记住我们写的effect函数。effect函数每次更新都会作用于DOM,让浏览器绘制屏幕,??然后调用effect函数。整个执行过程可以简单概括如下:1.组件被点击,触发更新计数为1,通知React,“计数值已更新为1”。2.React响应,向组件询问计数为1的UI。组件:GiveThevirtualDOMbwhenthecountis1.告诉react完成渲染时,记得调用effect函数()=>{document.title='youclick'+1+'times!'}4.React通知浏览器绘制DOM,更新UI5.浏览器通知ReactUI屏幕已经更新。6、React收到屏幕绘制完成的消息后,执行effect中的函数,使网页标题变成“youclick1times!”。0x03useRef如果你已经熟悉了上面的思路和流程,那么你也认同“快照”的概念。有时,我们希望获取效果中的最新值而不是捕获事件。官方提供了useRef的hook。useRef是一个处于“生命周期”阶段的“同步”变量,我们可以将值存储在它的current中,保证它的值是最新的。对于上面的描述,为什么捕获的是它的值,而不是最新的,可以通过setState(x=>x+1)来理解。传入的x是之前的值,x+1是新值。在一些setTimeout异步代码中,我们想要获取最新的值,以便同步最新的状态,所以使用ref来帮助存储最新更新的值。这种打破范式的做法让程序有点脏,但确实解决了很多问题。这样做的好处还可以指出哪些代码是脆弱的,需要依赖时间顺序。在类组件中,this.setState()方法每次都获取最新的值。0x04Effectcleaning与前面描述的effectcleaning或多或少有关,只是为了理解,但描述并不完全准确。例如下面的例子:useEffect(()=>{ChatAPI.subscribeToFriendStatus(props.id,handleStatusChange);return()=>{ChatAPI.unsubscribeFromFriendStatus(props.id,handleStatusChange);};});假设第一次渲染时props是{id:10},第二次渲染时是{id:20}。你可能会认为发生了这样的事情:React清除了{id:10}效果。React呈现{id:20}的UI。React运行{id:20}效果。但实际情况并非如此。如果按照这个心智模型来理解,那么在清空的时候,得到的值就是之前的旧值,因为清空是在渲染新的UI之前完成的。这与之前所说的React只在浏览器绘制后才执行effects的说法相矛盾。React这样做的好处是它不会阻塞浏览器的渲染(屏幕更新)。当然,按照这个规则,effect的清除也是延迟到浏览器绘制UI之后。那么正确的执行顺序应该是:React渲染id为20的UI,React清除id为10的effect,React运行id为20的effect,那么为什么旧的在effect中清除呢?组件中的每个函数(包括事件处理函数、效果器、定时器或API调用等)都会在定义它们的渲染器中捕获道具和状态。那么,effect的清空就不会读取“最新”的props,它只能读取定义它的rendering中props的值。在人类发展的过程中,不进取的保守派总会被淘汰。在React中也是如此,或许有些激进,但大多数人总是期待“旧符号换新桃子”。0x05effect的更新取决于useEffect中的第二个参数,可以是参数数组(依赖数组)。React更新了DOM的思想,不管过程是什么,它只向世界展示结果。React更新组件时,会比较props,通过AST等方法进行比较,然后只需要更新变化的DOM。第二个参数相当于告诉useEffect,只要我给你的其中一个参数发生变化,你就可以执行效果。这样可以减少每次render后调用effect的情况,减少无意义的性能浪费。那么在开发过程中,我们会尝试在组件加载时通过api获取远程数据,并将其应用到组件的数据渲染中,所以我们使用下面这个简单的例子:useEffect(()=>{featchData();},[]);由于是空数组,远程数据只能在组件挂载时获取一次,之后不会再执行。如果effect中涉及到局部变量,它们都会根据当前状态发生变化,每次都会创建函数(每次都会创建一个新的匿名函数)。functionCounter(){const[count,setCount]=useState(0);useEffect(()=>{constid=setInterval(()=>{setCount(count+1);},1000);return()=>clearInterval(id);},[]);return

{count}

;}你可能认为上面的例子会在组件加载后每秒在UI上count+1,但实际情况是只会执行一次。为什么?你认为这有点违反直觉吗?因为,effect的依赖没有添加计数,effect只会在第一次渲染时创建一个匿名函数。虽然通过了setInterval包,但是每秒执行一次count。+1,但是count的值一直为0,所以在UI性能上一直渲染为1。当然,通过一些规则,我们可以通过增加count,或者通过useRef,或者通过setState(x=>x+1)来改变它的值,达到最新的值。比如下面的黑科技操作://useReffunctionExample(){const[count,setCount]=useState(0);constcountRef=useRef(count);countRef.current=count;//如果这行代码放在effect函数,会怎么样呢?你可以考虑一下!//answer:在effect中,声明effect匿名函数的时候就有count了,值为0,所以得到的count值自然就是渲染前的count(value0,thisprops中的值)(valueinthisprops),又会重新设置理解snapshot的概念),但是由于dependency数组中没有dependency,所以匿名函数不会被执行两次。//但是由于setInterval的原因,函数会保留setCount,关键是参数,countRef.current=count;得到的值是第一次快照时的值0,所以它的更新值永远是0+1=1。这个结果符合预期的规律。//那为什么放在外面好呢?因为countRef.current同步的是count的最新值,所以每次render前都会获取到新的count值赋值给countRef.current。由于ref的同步特性(及时性和一致性),循环得到的countRef。current也是最新值,所以可以实现计数效果useEffect(()=>{constid=setInterval(()=>{setCount(countRef.current+1);},1000);return()=>clearInterval(id);},[]);return

{count}

;}//setState传入函数functionExample(){const[count,setCount]=useState(0);useEffect(()=>{constid=setInterval(()=>{setCount(x=>x+1);//给函数传递参数时,默认传递的第一个参数是之前的值,正在被useStatehook处理},1000);return()=>clearInterval(id);},[]);return

{count}

;}//使用useReducerfunctionCounter({step}){const[count,dispatch]=useReducer(reducer,0);functionreducer(state,action){if(action.type==='tick'){returnstate+step;}else{thrownewError();}}useEffect(()=>{constid=setInterval(())=>{dispatch({type:'tick'});},1000);return()=>clearInterval(id);},[dispatch]);return

{count}

;}上面的做法其实有点自欺欺人。可以看到下图中的日志。setInterval匿名函数中count变量的值不是发生了变化,可能会给我们的业务demo示例带来一些风险,但总的来说,如果你对业务或程序没有充分的了解,我不建议你这样做。对于依赖,首先要老老实实写好关联参数。其次,可以优化效果,考虑是否真的需要某个参数,是否可以替换?dependencies中dispatch、setState、useRef包裹的值都不变。这些Parameters都可以在dependencies中去掉。Dependency是一个函数,可以将一个函数定义到useEffect中,从而使添加的依赖成为函数的一个参数。这样useEffect就不需要添加xxx函数名作为依赖了。另外,如果只是简单的把函数名放在dependency中,如果这个函数在多个effect中复用,那么每次渲染这个函数的时候都会重新声明这个函数(newfunction),那么effects就会因为newfunction频繁执行,和不加依赖数组一样,没有任何优化作用,那么如何改进呢?方法一:如果函数在组件中不使用任何值,则将函数放在组件外定义,这个函数不在渲染范围内,不受数据流的影响,所以永远不会改变状态是常见于异步请求数据中,先发送、先到达、先发送的问题,称为racestate。如果异步函数支持取消,可以直接取消。更简单的方法是给异步标签值添加一个boolean类型,可以取消异步请求asyncfunctionfetchData(){constarticle=awaitAPI.fetchArticle(id);if(!didCancel){setArticle(article);}}fetchData();return()=>{didCancel=true;};},[id]);//...}根据前面的规则,比如id=19,获取数据的时间是30s,变成id=20,获取数据的时间只有5s,那么执行顺序应该是如下:id=19组件卸载,didCancle=true,当id=19是异步请求收到数据30s后,因为!didCancle===false,不会执行id=20的数据更新,因为id变化,先设置didCancle=false,请求获取数据,5s后获取到数据,然后更新数据,最后将更新后的数据渲染到屏幕0x07。总结hooks的思路值得学习。以结果为导向,以思想为导向,对于React的使用会更加得心应手!参考《使用 Effect Hook》-https://zh-hans.reactjs.org/docs/hooks-effect.html《a complete guide to useeffect》-https://overreacted.io/a-complete-guide-to-useeffect/