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

Antd Mobile 作者教你写 React 受控组件和非受控组件_0

时间:2023-03-18 23:32:56 科技观察

AntdMobile作者手把手教你写React受控组件和非受控组件有人会毫不犹豫的回答:是受控组件,因为有value和onChange,也有人会比较犹豫,因为好像说的有道理Input是受控组件还是不受控组件。当然,其实Input组件既可以是受控组件,也可以是非受控组件,这完全取决于它在业务项目中的使用方式。在这篇文章中,我们将讨论如何让像antd的Input组件这样的组件同时支持受控和非受控模式。让我们从最简单最基础的部分开始,一点一点的分析进化,看看我们会遇到什么问题,一步步解决。什么是受控组件?什么是不受控组件?我们先看一个简单的例子,Input组件有一个内部状态(State)值,它没有任何属性,所以很明显,它是一个不受控的组件,它的组件状态不受外部环境控制的影响。而是封闭在组件内部。而如果我们对它做一点调整,去掉原来的内部state值,放到props上,它就变成了一个受控组件:显然,此时输入框的值取决于从外部传入的props。如果我们画一张图,我们可以很明显的看出受控和非受控的区别:图中的蓝色方框代表组件,黄色圆圈代表组件内部的状态。受控组件和非受控组件?虽然在业务项目中,我们写的组件有明确的受控或非受控,但是对于组件库来说,有很多组件需要同时支持受控和非受控两种模式。以antd-mobile目前的5.17版本为例,几乎所有涉及到输入值、切换、展开、收起的组件都需要同时受控和不受控。虽然听上去不难,但实际写起来还是有难度的,不妨试试吧。如何实现最简单的方案:内外两种状态,手动同步考虑到实现成本的复杂性,我们需要在两种模式下尽可能保持组件逻辑一致,减少逻辑分支意味着更好的可维护性和可读性。那么,自然而然,我们很容易想到这个解决方案:Child组件内部总有一个state,不管它处于哪种模式,它都直接使用自己的内部state。当它处于托管模式时,我们手动使其内部状态与父组件中的状态保持同步。下图中加了两个勾,勾上的状态表示Child组件实际使用的是哪个状态。这个方案听起来可行,那我们写成代码吧:仔细看上面的代码,我们会发现controlledmode存在两个问题:Atomicity:Child的内部状态更新会比Parent组件晚一次在渲染中周期,存在撕裂问题。性能:因为状态同步是通过useEffect中的setState完成的,会触发额外的渲染,存在性能问题。理清问题之后,我们来一一解决:解决问题一:原子性的问题其实很好解决。我们实际上并不需要Child和Parent的状态在任何时候都非常严格和一致。我们只需要判断,如果此时组件处于受控模式,直接从外部使用state就可以了:这样即使state的同步有延迟,实际使用的值子组件必须是最新的。代码如下:解决问题2:性能因为我们在useEffect中做状态同步,自然会触发Child组件额外的重新渲染。如果Child组件比较简单,则性能影响可以忽略不计。但是对于一些复杂的组件(比如Picker),多渲染一次带来的性能问题就比较严重了。有没有办法在Child组件的render阶段直接更新值状态?不,React不允许我们在渲染期间调用setState。看似死胡同,但我们可以停下来重新考虑这行useState:我们什么时候创建这个State?我们的目的是什么?国家的本质是什么?简单粗略的分析,我们可以把State拆分成两部分:State是用来存储数据的,它可以让我们在组件的渲染功能之外“持久化”一些数据。状态更新可以触发重新渲染,因为React知道状态更新。如果写个公式,可以写成:State=storedata+triggerre-rendering但是在存储数据方面,我们可以直接使用Ref;类似地,如果我们只是需要触发重新渲染,我们可以使用类似setFlag({})的方法,或者像setCount(v=>v+1)这样的强制方法(虽然很蠢,但90%的React开发人员一定写过这边走)。那么我们根据这个推断调整上面的公式:State=Ref+forceUpdate()我们就很接近了。根据这个公式,我们可以将Child组件中的State拆分为一个Ref和一个forceUpdate函数:下图中浅色虚线圆圈代表ref,刷新图标代表forceUpdate函数。这样,??我们可以在render阶段直接更新ref的值:回过头来看代码,我们会发现为什么还要根据controlled和non-controlledmode来判断controlledmode来使用不同的值?(上面代码块中的第12行).由于stateRef.current一定是最新的值,所以可以简化为Childcomponentsalwaysusinginternallystoreddata(Ref):另外另外,我们也可以将手动实现的forceUpdate替换成ahooks的useUpdate:抽象复用:usePropsValue到这里,我们基本实现了所有的功能,但是我们只实现了一个Input组件,比如antd-mobile组件库中,会有很多很多需要支持sw能力的组件控制和非控制模式之间的痒。因此,为了更好的复用性,我们将上面的逻辑抽取出来,放到一个自定义的Hook中:这样,在每个组件中,我们就可以直接使用usePropsValue,它和useState非常相似:但是,我们忽略了defaultValue。在antd-mobile中,valueonChangedefaultValue总是成组出现:接下来我们稍微优化一下,让它变得更像useState。useState获取的setState函数支持传入一个update函数,usePropsValue目前不支持这种用法,所以改造一下:一个隐藏的bug我以为已经完蛋了今天在GitHub上收到一个issue:TabBar的onChange为什么会触发#5409[1]即使密钥相同。这个问题揭示了一个长期隐藏的错误。比如:如果当前state是1,如果我们使用React的useState,执行setState(1)是没有效果的,React会帮我们过滤掉这个更新。但是usePropsValue不会。对于用户来说,点击同一个Tab并不会触发切换,所以应该不会触发onChange事件,所以我们需要增加一点额外的判断来解决这个bug:在antd-mobile中,我们也有这样一个usePropsValue工具Hook和上面文章描述的差不多,如果想了解更多,可以到这里[2]阅读代码。上面“解决问题2:性能”部分的勘误表中提到“React不允许我们在渲染过程中调用setState”,但是被@fenoob[3]评论了。更正一下,其实React允许我们在render函数中调用setState,但仅限于只触发当前组件自身的状态更新。我在这里写了一个demo[4]来验证一下。参考[1]TabBar的onChange为什么在同一个key的情况下也会触发#5409:https://github.com/ant-design/ant-design-mobile/issues/5409。[2]此处:https://github.com/ant-design/ant-design-mobile/blob/fae45549bcadb2b3c7f1dea27462543230e3b795/src/utils/use-props-value.ts。[3]@fenoob://www.zhihu.com/people/05bdf67112572afd5f3526f2eaa425c8。[4]演示:https://codesandbox.io/s/condescending-pare-1utvlt?file=/src/App.js。

猜你喜欢