全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2023-09-21_React最佳实践之“你可能不需要 Effect”

您的位置:首页 >> 新闻 >> 行业资讯

React最佳实践之“你可能不需要 Effect” 点击关注公众号,回复”福利”即可参与文末抽奖 前言本文思想来自React官方文档You Might Not Need an Effect,保熟,是我近几天读了n遍之后自己的理解,感觉受益匪浅,这里小记一下跟大家分享。 曾经本小白R的水平一直停留在会用React写业务,讲究能跑就行的程度,最近尝试学习一些关于React的最佳实践,感兴趣的朋友一起上车吧!! useEffect痛点概述useEffect的回调是异步宏任务,在React根据当前状态更新视图之后,下一轮事件循环里才会执行useEffect的回调,一旦useEffect回调的逻辑中存在状态修改等操作,就会触发渲染的重新执行(FC函数体重新运行,渲染视图),不光存在一定的性能损耗,而且因为前后两次渲染的数据不同,可能造成用户视角下视图的闪动,所以在开发过程中应该避免滥用useEffect。 如何移除不必要的 Effect对于渲染所需的数据,如果可以用组件内状态(props、state)转换而来,转换操作避免放在Effect中,而应该直接放在FC函数体中。 如果转换计算的消耗比较大,可以用useMemo进行缓存。 对于一些用户行为引起数据变化,其后续的逻辑不应该放在Effect中,而是在事件处理函数中执行逻辑即可。 比如点击按钮会使组件内count加一,我们希望count变化后执行某些逻辑,那么就没必要把代码写成: functionCounter(){ const[count,setCount]=useState(0); functionhandleClick(){ setCount(prev=prev+1); } useEffect(()={ //count改变后的逻辑... },[count]) //... } 上面的demo大家肯定也看出来了,直接把Effect中的逻辑移动到事件处理函数中即可。 根据props或state来更新state(类似于vue中的计算属性)如下Form组件中fullName由firstName与lastName计算(简单拼接)而来,错误使用Effect: functionForm(){ const[firstName,setFirstName]=useState('Taylor'); const[lastName,setLastName]=useState('Swift'); //??避免:多余的state和不必要的Effect const[fullName,setFullName]=useState(''); useEffect(()={ setFullName(firstName+''+lastName); },[firstName,lastName]); //... } 分析一下,按照上面的写法,如果firstName或者lastName改变之后,首先根据新的firstName与lastName与旧的fullName进行渲染,然后才是useEffect回调的执行,最后根据最新的fullName再次渲染视图。 我们要做的是尽可能把渲染的效果进行统一(同步fullName与两个组成state的新旧),并且减少渲染的次数: functionForm(){ const[firstName,setFirstName]=useState('Taylor'); const[lastName,setLastName]=useState('Swift'); //?非常好:在渲染期间进行计算 constfullName=firstName+''+lastName; //... } 缓存昂贵的计算基于上面的经验,我们如果遇到比较复杂的计算逻辑,把它放在FC函数体中可能性能消耗较大,可以使用useMemo进行缓存,如下,visibleTodos这个数据由todos与filter两个props数据计算而得,并且计算消耗较大: import{useMemo}from'react'; functionTodoList({todos,filter}){ //?除非todos或filter发生变化,否则不会重新执行getFilteredTodos() constvisibleTodos=useMemo(()=getFilteredTodos(todos,filter),[todos,filter]); //... } 当 props 变化时重置所有 state比如一个ProfilePage组件,它接收一个userId代表当前正在操作的用户,里面有一个评论输入框,用一个state来记录输入框中的内容。我们为了防止切换用户后,原用户输入的内容被当前的用户发出这种误操作,有必要在userId改变时置空state,包括ProfilePage组件的所有子组件中的评论state。 错误操作: exportdefaultfunctionProfilePage({userId}){ const[comment,setComment]=useState(''); //??避免:当prop变化时,在Effect中重置state useEffect(()={ setComment(''); },[userId]); //... } 为什么避免上诉情况,本质还是避免Effect的痛点,我们可以利用组件**key不同将会完全重新渲染**的特点解决这个问题,只需要在父组件中给这个组件传递一个与props同步的key值即可: exportdefaultfunctionProfilePage({userId}){ return( Profile userId={userId} key={userId} / } functionProfile({userId}){ //?当key变化时,该组件内的comment或其他state会自动被重置 const[comment,setComment]=useState(''); //... } 当 prop 变化时调整部分 state其实说白了还是上面的基于props和state来计算其它所需state的逻辑,如下List组件,当传入的items改变时希望同步selection(被选中的数据),那么我们直接在渲染阶段计算所需内容就好了: functionList({items}){ const[isReverse,setIsReverse]=useState(false); const[selectedId,setSelectedId]=useState(null); //?非常好:在渲染期间计算所需内容 constselection=items.find(item=item.id===selectedId)??null; //... } 在事件处理函数中共享逻辑比如两种用户操作都可以修改某个数据,然后针对数据修改有相应的逻辑处理,这时候有一种错误(不好)的代码逻辑:事件回调——修改state——state修改触发Effect——Effect中执行后续逻辑。 我们不应该多此一举的添加一个Effect,这个Effect就类似于数据改变的监听器一样,完全是多余的,我们只需要在数据改变之后接着写后续的逻辑就好了!! 如下,用户的购买与检查两种行为都可以触发addToCart的逻辑,进而修改product这个数据,然后可能触发后续逻辑showNotification: functionProductPage({product,addToCart}){ //??避免:在Effect中处理属于事件特定的逻辑 useEffect(()={ if(product.isInCart){ showNotification(`已添加${product.name}进购物车!`); } },[product]); functionhandleBuyClick(){ addToCart(product); } functionhandleCheckoutClick(){ addToCart(product); navigateTo('/checkout'); } //... } 我们把Effect中的逻辑提取出来放到事件处理函数中就好了: functionProductPage({product,addToCart}){ //?非常好:事件特定的逻辑在事件处理函数中处理 functionbuyProduct(){ addToCart(product); showNotification(`已添加${product.name}进购物车!`); } functionhandleBuyClick(){ buyProduct(); } functionhandleCheckoutClick(){ buyProduct(); navigateTo('/checkout'); } //... } 发送 POST 请求也有一些典型的需要使用Effect的情景,比如有些数据、逻辑是页面初次渲染,因为组件的呈现而需要的,而不是后续交互触发的,比如异步数据的获取,我们就可以写一个依赖数组为[]的Effect。 如下Form组件,页面加载之际就需要发送一个分析请求,这个行为与后续交互无关,是因为页面的呈现就需要执行的逻辑,所以放在Effect中,而表单提交的行为触发的网络请求,我们直接放在事件回调中即可。 切忌再多写一个state和一个Effect,然后把一部分逻辑写在Effect里面,比如下面handleSubmit中修改firstName与lastName,然后多写一个Effect监听这两个数据发送网络请求,这就是上面我们一直纠正的问题,我就不放代码了。 functionForm(){ const[firstName,setFirstName]=useState(''); const[lastName,setLastName]=useState(''); //?非常好:这个逻辑应该在组件显示时执行 useEffect(()={ post('/analytics/event',{eventName:'visit_form' }, functionhandleSubmit(e){ e.preventDefault(); //?非常好:事件特定的逻辑在事件处理函数中处理 post('/api/register',{firstName,lastName } //... } 链式计算避免通过state将Effect变成链式调用,如下Game组件中,类似于一个卡牌合成游戏,card改变可能触发goldCardCount的改变,goldCardCount的改变可能触发round的改变,最终round的改变可能触发isGameOver的改变,试想如果某次card改变,从而正好所有条件都依次满足,最后isGameOver改变,setCard → 渲染 → setGoldCardCount → 渲染 → setRound → 渲染 → setIsGameOver → 渲染,有三次不必要的重新渲染!! functionGame(){ const[card,setCard]=useState(null); const[goldCardCount,setGoldCardCount]=useState(0); const[round,setRound]=useState(1); const[isGameOver,setIsGameOver]=useState(false); //??避免:链接多个Effect仅仅为了相互触发调整state useEffect(()={ if(card!==nullcard.gold){ setGoldCardCount(c=c+1); } },[card]); useEffect(()={ if(goldCardCount3){ setRound(r=r+1) setGoldCardCount(0); } },[goldCardCount]); useEffect(()={ if(round5){ setIsGameOver(true); } },[round]); useEffect(()={ alert('游戏结束!'); },[isGameOver]); functionhandlePlaceCard(nextCard){ if(isGameOver){ throwError('游戏已经结束了。'); }else{ setCard(nextCard); } } //... 因为Game中所有state改变之后的行为都是可以预测的,也就是说某个卡牌数据变了,后续要不要继续合成更高级的卡牌,或者游戏结束等等这些逻辑都是完全明确的,所以直接把数据修改的逻辑放在同一个事件回调中即可,然后根据入参判断是哪种卡牌然后进行后续的操作即可: functionGame(){ const[card,setCard]=useState(null); const[goldCardCount,setGoldCardCount]=useState(0); const[round,setRound]=useState(1); //?尽可能在渲染期间进行计算 constisGameOver=round5; functionhandlePlaceCard(nextCard){ if(isGameOver){ throwError('游戏已经结束了。'); } //?在事件处理函数中计算剩下的所有state setCard(nextCard); if(nextCard.gold){ if(goldCardCount=3){ setGoldCardCount(goldCardCount+1); }else{ setGoldCardCount(0); setRound(round+1); if(round===5){ alert('游戏结束!'); } } } } //... 初始化应用因为React严格模式&开发模式下: ReactDOM.createRoot(document.getElementById('root')).render( React.StrictMode App/ /React.StrictMode, ) 组件的渲染会执行两次(挂载+卸载+挂载),包括依赖为[]的Effect同样会执行两次,这是React作者为了提醒开发者 cleanup 有意而设计之的(比如一些需要手动清除的原生事件如果没写清除逻辑,事件触发时就会执行两次回调从而引起注意),所以执行两次的逻辑可能会造成一些逻辑问题,我们可以用一个全局变量来保证即使在React严格模式&开发模式下也只执行一次Effect的回调: letdidInit=false; functionApp(){ useEffect(()={ if(!didInit){ didInit=true; //?只在每次应用加载时执行一次 loadDataFromLocalStorage(); checkAuthToken(); } }, //... } 通知父组件有关 state 变化的信息最佳实践的本质还是我们刚刚一直强调的:减少Effect的使用,可以归并到回调函数中的逻辑就不要放在Effect中。 如下,假设我们正在编写一个有具有内部 state isOn 的 Toggle 组件,该 state 可以是 true 或 false,希望在 Toggle 的 state 变化时通知父组件。 错误示范: (事件回调只负责修改 state, Effect中执行通知父组件的逻辑) functionToggle({onChange}){ const[isOn,setIsOn]=useState(false); //??避免:onChange处理函数执行的时间太晚了 useEffect(()={ onChange(isOn); },[isOn,onChange]) functionhandleClick(){ setIsOn(!isOn); } functionhandleDragEnd(e){ if(isCloserToRightEdge(e)){ setIsOn(true); }else{ setIsOn(false); } } //... } 删除Effect: functionToggle({onChange}){ const[isOn,setIsOn]=useState(false); functionupdateToggle(nextIsOn){ //?事件回调中直接通知父组件即可 setIsOn(nextIsOn); onChange(nextIsOn); } functionhandleClick(){ updateToggle(!isOn); } functionhandleDragEnd(e){ if(isCloserToRightEdge(e)){ updateToggle(true); }else{ updateToggle(false); } } //... } 将数据传递给父组件避免在 Effect 中传递数据给父组件,这样会造成数据流的混乱。我们应该考虑把获取数据的逻辑提取到父组件中,然后通过props将数据传递给子组件: 错误示范: functionParent(){ const[data,setData]=useState(null); //... returnChildonFetched={setData}/; } functionChild({onFetched}){ constdata=useSomeAPI(); //??避免:在Effect中传递数据给父组件 useEffect(()={ if(data){ onFetched(data); } },[onFetched,data]); //... } 理想情况: functionParent(){ constdata=useSomeAPI(); //... //?非常好:向子组件传递数据 returnChilddata={data}/; } functionChild({data}){ //... } 订阅外部 store说白了就是React给我们提供了一个专门的hook用来绑定外部数据(所谓外部数据,就是一些环境运行环境里的数据,比如window.xxx) 我们曾经常用的做法是在Effect中编写事件监听的逻辑: functionuseOnlineStatus(){ //不理想:在Effect中手动订阅store const[isOnline,setIsOnline]=useState(true); useEffect(()={ functionupdateState(){ setIsOnline(navigator.onLine); } updateState(); window.addEventListener('online',updateState); window.addEventListener('offline',updateState); return()={ window.removeEventListener('online',updateState); window.removeEventListener('offline',updateState); }, returnisOnline; } functionChatIndicator(){ constisOnline=useOnlineStatus(); //... } 这里可以换成useSyncExternalStore这个hook,关于这个hook,还是有一点理解成本的,我的基于useSyncExternalStore封装一个自己的React状态管理模型吧这篇文章里有详细的解释,下面直接放绑定外部数据最佳实践的代码了: functionsubscribe(callback){ window.addEventListener('online',callback); window.addEventListener('offline',callback); return()={ window.removeEventListener('online',callback); window.removeEventListener('offline',callback); } functionuseOnlineStatus(){ //?非常好:用内置的Hook订阅外部store returnuseSyncExternalStore( subscribe,//只要传递的是同一个函数,React不会重新订阅 ()=navigator.onLine,//如何在客户端获取值 ()=true//如何在服务端获取值 } functionChatIndicator(){ constisOnline=useOnlineStatus(); //... } 获取异步数据比如组件内根据props参数query与一个组件内状态page来实时获取异步数据,下面组件获取异步数据的逻辑之所以没有写在事件回调中,是因为首屏即使用户没有触发数据修改,我们也需要主动发出数据请求(类似于首屏数据获取),总之因为业务场景需求吧,我们把请求逻辑放在一个Effect中: functionSearchResults({query}){ const[results,setResults]=useState([]); const[page,setPage]=useState(1); useEffect(()={ //??避免:没有清除逻辑的获取数据 fetchResults(query,page).then(json={ setResults(json); },[query,page]); functionhandleNextPageClick(){ setPage(page+1); } //... } 上面代码的问题在于,由于每次网络请求的不可预测性,我们不能保证请求结果是根据当前最新的组件状态获取的,也即是所谓的「竞态条件:两个不同的请求 “相互竞争”,并以与你预期不符的顺序返回。」 「所以可以给我们的Effect添加一个清理函数,来忽略较早的返回结果,」 如下,说白了用一个变量ignore来控制这个Effect回调的"有效性",只要是执行了下一个Effect回调,上一个Effect里的ignore置反,也就是让回调的核心逻辑失效,保证了只有最后执行的Effect回调是“有效”的: functionSearchResults({query}){ const[results,setResults]=useState([]); const[page,setPage]=useState(1); useEffect(()={ //说白了用一个ignore变量来控制这个Effect回调的"有效性", letignore=false; fetchResults(query,page).then(json={ if(!ignore){ setResults(json); } return()={ ignore=true; },[query,page]); functionhandleNextPageClick(){ setPage(page+1); } //... }点击小卡片,参与粉丝专属福利!!如果文章对你有帮助的话欢迎「关注+点赞+收藏」 阅读原文

上一篇:2025-06-23_「转」硅谷的AI创业潮,其实是一场大型的资源错配 下一篇:2024-11-20_2024第一波最值得看的8个圣诞广告

TAG标签:

15
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价