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

网站建设3年前发布
90 0 0

20230306104942392b41802a413d0da3c2603efa72490646d67a792,曾经,我每次面试时几乎都会问一个问题:antd 中的 Input 组件是受控组件还是非受控组件?,有些人会毫不犹豫的回答:是受控组件,因为有 value 和 onChange,而另外也有一些人会比较犹豫,因为的确似乎说 Input 是受控组件或非受控组件都说得过去。当然,实际上 Input 组件既可以是受控组件,也可以是非受控组件,这完全取决于业务项目中怎么去使用它。,在这篇文章,我们将一起聊聊怎么去让一个组件像 antd 的 Input 组件这样,既支持受控模式,又支持非受控模式。让我们从最简单和基础的部分出发,一点点来分析和演进,看看会遇到哪些问题,又如何一步步解决。,让我们先来看一个简单的例子,这个 Input 组件有一个内部的状态(State)value,而且它没有任何属性,因此很显然,它是一个非受控的组件,它的组件状态并不受外部环境控制,而是封闭在组件内部。,20230306105020470ccbe782d931bdc008053adf0ecd9d42292647820230306104943d5c7491136aa4d262b9002fa7d1f65460f024e259,而如果我们稍微对它做一点调整,把原本的内部状态 value​ 去掉,放到 props 上去,它就变成了受控组件:,20230306104944b3f42bb6915fc9d04628411860cd8c266db73996220230306104944e1a64ac6691e5c4277e448c6df846803fffd92603,很显然,此时输入框的值是取决于外部传递进来的 props。,如果我们画个图,那可以很清楚的看到受控和非受控的区别:,20230306104944a66cdd835f5d841efd29236e9032ae7ceacb23438202303061050206636fe940fc5761a45c51766e53fe988dccaaf822,图中蓝色的方框表示组件,黄色的圆圈表示组件内的状态。,尽管在业务项目中,我们写的组件都是明确的受控或者非受控,但对于组件库来说,有非常多的组件需要做到既支持受控模式,又支持非受控模式。以 antd-mobile 现在的 5.17 版本为例,几乎全部的涉及到输入值、切换、展开收起的组件,都是需要做到既受控又非受控的。,尽管听起来似乎不难,但实际写起来还是会遇到一些困难的,让我们来试一试。,考虑到实现成本的复杂度,我们需要让组件逻辑在两种模式下,尽可能的保持一致,减少逻辑分支意味着更好的可维护性和可读性。所以,自然而然的,我们可以很容易想到这个方案:,Child 组件内部始终存在一个状态,不管它处于哪种模式,它都直接使用自己内部的状态。而当它处于受控模式时,我们让它的内部状态和 Parent 组件中的状态手动保持同步。,2023030610494602b8a6a32f7e779e7955805649f87415b60e3683820230306104946347134291de2162ac8f758c1bb6c135ac9b4e1280,这套方案听起来是可行的,我们把它写成代码:,20230306104946a19ecbd071513779b2069100ace85bf4eb5b96963,仔细看上面的代码,我们会发现在受控模式下存在两个问题:,明确问题之后,我们来逐个解决:,这个问题其实很好解决,我们其实并不需要 Child 和 Parent 的状态保持非常严格的每时每刻都一致,我们只需要判断,如果组件此时处于受控模式,那么直接使用来自外部的状态就可以了:,20230306105021a7e73c373df694a297c3842111542cfa2256b8641,这样,即便状态的同步是存在延迟的,但是 Child 组件所真正使用到的值一定是最新的。,代码如下:,20230306104947c79dcc219b554c88e9e7003600ce3cd3bf92808052023030610494892a058b84f9d9d3e72b88900135fcc8e5f3af6679,因为我们是在 useEffect 去做状态同步的,所以自然会额外的多触发一次 Child 组件的重渲染。如果 Child 组件比较简单的话,那出现的性能影响可以忽略不计。但是对于一些复杂的组件(例如 Picker),多渲染一次带来的性能问题是比较严重的。,那有没有办法在 Child 组件的 render 阶段就直接更新 value 状态呢?,并不可以,React 不允许我们在 render 过程中调用 setState。,似乎进入了死胡同,但我们可以停下来,重新考虑一下这行 useState 的代码:,20230306104949477f75a1431d34d4c0019997ef004d69d2b38f80220230306104949d6a5777656dc1539cd958004f63067285d84a7250,当我们创建这个 State 时?我们的目的是什么?State 的本质是什么?,如果比较简单粗暴的分析,我们可以把 State 拆成两部分:,如果写一个公式的话,可以写成:,而但就存放数据来看,我们可以直接使用 Ref;同样,如果只是需要触发重新渲染,我们可以使用类似于 setFlag({}) 或者 setCount(v => v + 1) 这样的强制方式(虽然很蠢,但想必 90% 的 React 开发者都曾经这么写过)。,那我们根据这个推断来调整一下上面的公式:,我们已经非常接近了,根据这个公式,我们可以把 Child 组件中的 State 拆成一个 Ref 和一个 forceUpdate 函数:,20230306104950a360cd321460ec1b038044d5514c19bf0fe95298920230306104950e50128d46bfff78f9bd487855e134aa9baaa7c921,这样一来,我们就可以直接在 render 阶段直接更新 ref 的值了:,20230306105021f9d4e67429ccd0f75c81325c944c8c231b1d4774720230306104951092ada04192fa3b11d47757ce58899b2d68042880,再回头看下代码,会发现,为什么还需要判断根据受控和非受控模式来使用不同的值呢?(上面代码块中的第 12 行)。既然 stateRef.current 一定是最新的值,那么完全可以简化成 Child 组件永远使用内部存放的数据(Ref):,2023030610495237b854892506cc2c01c105d629c57bc0bc638b109,除此之外,我们还可以把手动实现的 forceUpdate 替换成 ahooks 的 useUpdate:,20230306104952d38d42a03858053663b7249871df39550dccc1664,到这里,我们已经基本实现了所有的功能,但我们只是实现了一个 Input 组件,在 antd-mobile 这样的组件库中,会有很多很多组件都需要支持能够切换受控和非受控模式。所以,为了更好的可复用性,我们把上面的逻辑抽离成一个自定义 Hook:,20230306104953f6a9499174401f91ed4757048b5e062a6fe2e05282023030610495491cd52306342704ec3b596a84d99b2675cbb48661,这样,在各种组件中,我们可以直接使用 usePropsValue,用法和 useState 非常接近:,20230306105022e8824f86347d495392d395a4f4ee89ba3f2d3892820230306104954a3bee3b073a9c0757ca4213a1b5595a2de99cd562,不过,我们忽略了 defaultValue,在 antd-mobile 中,value onChange defaultValue 总是成组出现的:,20230306105023f5abfad58acf0493ea67140240ca7860eef69681420230306104955d8f6f86106cfe3fb537184f3dba821d62385ce533,接下来,让我们对它再做一点优化,让它变得更像 useState。useState 得到的 setState 函数,支持传入一个更新函数,而 usePropsValue 目前还不支持这种用法,所以我们来改造一下:,20230306104956799812a13fa60676b003585acfa657e0da988674320230306104956c51276e98f324081c3957675b37cd08ddd7730775,我本以为已经完工了,直到某天在 GitHub 上收到了一条 issue:TabBar 的 onChange 为什么在同 key 的情况也会触发 #5409[1]。,这条 issue 揭示了一个隐藏已久的 bug,举个例子:,假如当前的 state 为 1,如果我们用的是 React 的 useState,那执行 setState(1) 不会有任何效果,React 会帮我们过滤掉这次的更新。而 usePropsValue 不会。,对用户来说,点击同一个 Tab 并没有触发切换,也因此不应该触发 onChange 事件,所以我们还需要额外的增加一点判断,来解决这个 bug:,20230306104957b64eac555c04f5334b38977e84ae13cf716c38795,在 antd-mobile 中,我们也有一个这样的 usePropsValue 工具 Hook,和上面文章中所描述的几乎是一样的,如果你想了解更多,可以去这里[2]翻阅代码。,上面“解决问题 2:性能”章节中提到“React 不允许我们在 render 过程中调用 setState”,但经评论区@fenoob[3]。,指正,其实是 React 是允许我们在 render 函数中调用 setState 的,只是限制了只能触发当前组件自己的 state 更新。我在这里写了一个 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]demo:https://codesandbox.io/s/condescending-pare-1utvlt?file=/src/App.js。

© 版权声明

相关文章