当前位置: 首页 > Web前端 > JavaScript

【实际的!】说说React组件状态设计,绝对帮你避坑~

时间:2023-03-27 01:12:28 JavaScript

前言React函数组件写的很灵活,数据传递也很方便,但是如果对React的理解不够深入,你会遇到很多问题,比如数据改变后视图没有改变,父组件的状态改变了子组件的状态没有及时更新等等。对于复杂的组件,可能会出现更多的问题,并且迷惑代码更有可能出现。随着踩的坑越来越多,我越来越意识到合理设计React组件数据状态的重要性。大多数常见问题是由数据传输和修改混乱引起的。根据开发经验和官方文档,我对React组件的状态设计做了一些总结,希望能帮助大家理清思路。组件数据和状态在讲组件状态设计之前,先说说组件状态和数据,因为重点是组件状态设计,这里只简单介绍一些前置知识。如果想了解更多,我在本文中也有推荐文章,有兴趣的同学可以自行浏览。组件中的数据状态源包括state和props。状态由组件自己维护,可以通过调用setState来修改。props是从外部传入的,是只读的。如果要修改它们,只能通过props传入修改后的方法。组件的状态当我们调用setState修改状态时,会触发组件的重新渲染,同步数据和视图。在这个过程中,我们可以思考几个问题:数据是如何同步到视图的?state的状态是如何保存的?为什么重新渲染后状态没有重置为初始值?1、数据是如何同步到视图的?我们先了解下fiber在react16之前。react的虚拟dom树是一个树状结构。该算法利用深度优先的原理递归遍历树,找出变化的节点,对变化的部分操作原始dom。因为是递归遍历,缺点是这个过程的同步不能中断,而且由于js是单线程的,大量的逻辑处理会占用主线程太久,浏览器没有时间重新绘制和重新排列,会出现渲染问题。.React16出现后,对框架进行了优化,引入了时间片和任务调度的机制。JS逻辑处理只占用指定的时间。时间一到,不管逻辑有没有处理完,主线程的控制权都要交还给浏览器。渲染过程执行重绘和重新排列。异步可中断更新需要在内存中有一定的数据结构来存储工作单元的信息,这个数据结构就是Fiber。[1](参考文章链接在文末,推荐)fiber树以链表结构存储元素节点的信息,每个fiber节点存储足够的信息,树比较过程可以中断,currentfibertree就是currentfiber,在renderer阶段生成的fibertree叫做workInProgressfiber。两棵树之间的相应节点由交替指针连接。React会对两棵树进行diff和比较,最后重用、添加、删除和移动节点。调用setState后会发生什么?首先调用函数生成一个update对象,这个对象有task的优先级,fiber实例等,然后把这个对象放入update队列,等待协调。React会按照优先顺序调用方法,创建一个Fiber树并生成一个副作用列表。这个阶段会先判断主线程是否有时间,如果有,先生成一棵workInProgress树并遍历。然后进入调优阶段,将workInProgress树与当前Fiber进行对比,更新真实dom。2.state的状态是怎么保存的?在fiber节点上,保存了memoizedState,即当前组件的hooks按照执行顺序形成的链表。链表存储钩子的信息。每种钩子的价值是不同的。对于useState,该值是当前状态。(Tips:深入理解reacthooks原理,推荐阅读《React Hooks 原理》)函数组件在每次状态变化时重新渲染,属于新函数,有自己独特的不变状态值,即memoizedState上保存的对应状态值。(捕获价值特征)。这就是为什么即使设置了状态也无法获取最新状态的原因。渲染发生在状态更新之前,所以状态是函数执行时的值。这个问题可以通过setState的回调或者ref特性来解决。2、为什么重新渲染后状态没有重置为初始值?为什么组件重新渲染了,数据却不会重新初始化?可以先从业务上理解。比如两个select组件的初始值为未选中。select_A选择该选项后,select_B再次选择它。最初未检查。知道了状态是怎么保存的,其实就很好理解了。划重点——重新渲染≠重新加载,组件还没有卸载,状态值还存在于fiber节点中。而useState只会在组件首次加载时初始化state的值。经常有朋友疑惑什么时候组件没有正常更新。父组件重新渲染子组件,子组件也会重新渲染,但是为什么子组件的status值没有更新呢?这是因为重新渲染只是重新渲染,而不是重载。如果你不人为地更新它的状态,它如何重置/更新?ps:面对一些不受控的组件不更新状态的情况,我们可以通过改变组件的key值,重载来解决。组件props当组件的props改变时,它也会被重新渲染,它的子组件也会被重新渲染。[图1-1]如图1-1所示,组件A的state和props.data作为子组件1的props传入,当组件A的props发生变化时,子组件1的props也会发生变化,这将重新渲染,然而,子组件2是一个不受控制的组件。父组件A重新渲染后也会重新渲染。但是,如果它的数据状态没有改变,它就不需要重新渲染,这就造成了浪费。对于这种不需要重复渲染的组件或状态,有很多优化组件的方法,比如官方的React.Memo、pureComponent、useMemo、useCallback等。React组件状态设计数据和view相互影响。复杂的组件通常有props和state。如何指定组件使用的数据应该是组件自身的state,还是应该从外部传入props,如何规划组件的state,就是要写出优雅的代码。重要的。如何设计数据类型?道具?状态?持续的?先看看react官方文档中的段落:可以通过问自己以下三个问题来逐一检查对应的数据是否属于state:数据是通过props从父组件传递过来的吗?如果是,那么它不应该是状态。你能根据其他状态或道具计算该数据的价值吗?如果是这样,它也不是状态。这些数据是否随时间变化?如果是,那么它也不应该是状态。[2]让我们用代码将这些规则一一可视化,并谈谈不遵守规则的代码可能产生的陷阱。1、数据是通过props从父组件传过来的吗?如果是,那么它不应该是状态。//ComponentTestimportReact,{useState}from'React'exportdefault(props:{something:string//*是父组件维护的状态,会被修改})=>{const[stateOne,setStateOne]=useState(props.something)return(<>{stateOne})}这段代码中useState的使用估计很多新手朋友都会写,把something作为props传入,然后重新将一些东西作为初始值分配给组件Test的状态stateOne。这样做有什么问题?正如我们之前所说,props的变化会触发组件及其子组件的重新渲染。但是,重新渲染不等于重载。useState的初始值只会在组件第一次加载时赋值给state。重新渲染的时候,不能重新赋值,hooks的数据还是保存在当前的fiber树上,也就是当前的state值,所以不管props.something怎么变化,页面上显示的stateOne的值不会相应改变,永远是当前state的一个组件Test值。也就是说,事情从受控走向失控。违背了我们传值的初衷。那么解决方法是什么?——在组件Test中,可以通过useEffect监听props.something的变化,重置State。//ComponentTestimportReact,{useState}from'React'exportdefault(props:{something:string})=>{const[stateOne,setStateOne]=useState()useEffect(()=>{setStateOne(props.something)//如果没有其他副作用,加一层state是不是显得多余},[props.something])return(<>{stateOne})}可能有的朋友will说,“我不是直接拿了props的值直接用了吗?props的数据在使用之前需要修改,这不是需要一个中间变量来接收吗?用state不对吗?”这里我们介绍第二条规则——“你能不能根据其他state或者props来计算这个数据的值?如果可以,那么它就不是state。”2.你能根据其他状态或道具计算出这个数据的价值吗?如果是这样,它也不是状态。state和props最大的区别就是state是可修改的,而props是只读的,所以当我们想在使用props之前修改props的数据时,自然而然会想到把state作为一个中间变量来缓存。但是在这种场景下使用useState就显得有点大材小用了,因为你只需要在props变化的时候使用setState来重置状态值,其他操作都不需要setState,所以这个时候我们不需要使用state。所以在这种场景下,可以直接使用变量来接收数据重新计算的结果,或者,更好的方式是使用useMemo,使用新的变量来接收props.something的计算值。//ComponentTestimportReact,{useState}from'React'exportdefault(props:{something:number[]})=>{//每次重新渲染时都会重新计算方法1//constnewSome=props.something.map((num)=>(num+1))//当props.something改变时,方法2将重新计算constnewSome=useMemo(()={returnprops.something.map((num)=>(num+1))},[props.something])return(<>{newSome.map((num)=>num)})}还有一种情况是props传过来的数据只是Constants,而不是父组件维护的状态,也就是说不会再更新。子组件渲染需要这些数据,就会对这些数据进行操作。这时候就可以按状态接收了。//ComponentTestimportReact,{useState}from'React'exportdefault(props:{something:string//在父组件中作为一个不会改变的常量})=>{const[stateOne,setStateOne]=useState()return(<>{stateOne})}还有一种比较复杂的情况。子组件需要父组件的状态A,根据A重新组织数据,需要改变这些新的父组件的数据对状态A也有自己的影响,不能直接被子组件改变为子组件所需的数据。这种情况也可以通过state接收,因为子组件需要修改state,而不是仅仅依赖props的值来获取新的值。//父组件exportdefault(props)=>{const[staffList,setStaffList]=useState([])//setStaffList(requestresult)异步请求后return(

{/*
{staffList相关展示}
*/}
)}//子组件constComp=({staffList})=>{const[list,setList]=useState(staffList)useEffect(()=>{constnewStaffList=staffList.map((item)=>({...item,isShow:true}))setList()},[staffList])constonHide=useCallBack((index)=>{//...在克隆列表的索引项之后隐藏数据setList(...)},[])//写的时候不要忘记填写依赖项return(
{list.map((staff,index)=>(staff.isShow&&onHide(index)}>{staff.name}
))}
)}3.数据是否随时间变化?如果是,那应该不是state,这个很好理解。它随着时间的推移保持不变,这意味着从加载组件到卸载组件的时间,该值是相同的。对于这样的数据,我们只是用一个常量来声明它。放在组件外的组件里问题不大。该组件用useMemo包装。//ComponentTestimportReact,{useState}from'React'constwriter='AzureC'exportdefault()=>{//constwriter=useMemo(()=>'AzureC',[])return(<>{writer})}补充一点,使用react的应该对受控组件的概念有一定的了解(不懂的可以去看文档),来自我的理解是,当组件中的数据被父组件控制时(数据的来源和修改方法由父组件提供,并作为props传入),就是受控组件。相反,当组件的数据完全由自己维护时,Parent组件既不提供数据也不影响数据的变化。这些组件是不受控制的组件。这是一个很好的概念。我觉得把“受控对象”的粒度细分为单个变量更好理解,因为复杂组件的状态类型往往不止一种,是从parent传递过来的。也有自我维护的。有时候在思考一个组件的状态时,往往会想到这个状态是否应该被控制。React的数据传输是单向的,即从上到下。与父子组件相比,只能控制子组件。如果子组件需要修改父组件的数据,则必须由父组件提供修改数据。方法。顺便推荐一个hook,可以让父组件和子组件都控制同一个状态。阿里的hooks库——ahooks,包含useControllableValue。状态应该放在什么级别的组件中?组件本身?父组件?祖先组件?关于state状态应该放在组件的哪一级,在react官方文档中,有如下两段:父组件共享状态。这称为“状态提升”。[3]对于应用程序中的每个状态:找到所有根据这个状态渲染的组件。找到它们共同的所有者组件(在所有需要该状态的组件之上分层)。共同所有者组件或更高级别的组件应该拥有状态。如果你找不到合适的地方来存储状态,你可以简单地创建一个新组件来存储状态,并将这个新组件放在比共同拥有者组件更高的层次上。[2]描述的很详细,通俗易懂。因为react的数据流向是单向的,兄弟组件之间的通信必须以父组件作为“中间层”。当兄弟组件需要使用相同的状态时,维护状态然后通过父组件相互通知是很麻烦的。缩短求距离的方法当然是将组件的公共状态提给最近的公共父组件,由父组件管理状态。React的Context是针对多层次复杂组件的状态管理解决方案。它将状态提升到顶层,使得每一层组件都可以获得顶层传递的数据和方法。关于这个小标题,强烈推荐阅读React官方文档-ReactPhilosophy,对新手非常友好,适合回顾和整理思路,这里不再赘述。后记以前看同组大佬和同事的代码,总觉得自己是个好厨子(现在也是手动狗头),心想为什么我没有希望代码是这样规划的。但实际上,我们在写代码的时候,往往不能一步到位。我们总是边写边思考,根据需要改进。随着封装的组件越来越多,层级越来越高,我们自然会考虑使用Context……连大佬都一样。文中很多坑都是自己踩的,自己也做了笔记。这次整理的内容,其实主要是针对react函数组件的状态管理。如何写代码更规范,一定程度上可以避免性能问题。浪费和避免可能的隐患,比如一个请求被执行两次,数据交互变得极其复杂和难以维护。一切都是为了分享经验。如果觉得对你有帮助,希望你会喜欢,看完收藏转发~就这些了,谢谢你~下一篇见~引用[1]react源码解析7.Fiber架构[2]React官方文档-React哲学[2]React官方文档——React状态改进