前言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(
