实现一个自定义 React Hook:useLocalStorageState
实现一个自定义 React Hook:useLocalStorageState
在需求中,需要将数据保存到 localStorage,并在组件初始化时获取,修改时保存到本地。
创建一个名为 useLocalStorageState 的 Hook,封装读写逻辑。御剑红尘源码
此实现简单,但支持仅字符串格式,需手动序列化和反序列化以扩展数据类型。
增加序列化和反序列化支持,以适应不同数据类型。
扩展序列化反序列化方法,提供自定义序列化反序列化选项。
考虑使用成熟的第三方库,如 ahooks,其提供现成的 useLocalStorageState 实现。
ahooks 的 useLocalStorageState 源码可供参考和学习。
我们应该如何优雅的处理 React 中受控与非受控
了解了 React 中受控与非受控的基本概念后,我们来聊聊 rc-util 中的 useMergedState Hook。这个 Hook 旨在为开发者提供灵活的表单控件处理方式,支持受控与非受控两种模式。
useMergedState 的主要作用在于,允许开发者基于多层属性传递给底层表单控件,提供受控和非受控两种处理方式,以适应不同场景的需求。例如,Ant-Design 中的 Input 组件即可同时接收 value 和 onChange 的组合方式,以及单独的 defaultValue 实现非受控状态。
实现这个功能的核心在于处理输入值的传入逻辑,确保无论外部传入的是 value 还是 defaultValue,内部都以受控状态处理。当外部传入 defaultValue 时,组件表现为非受控状态,而 value 则确保了受控状态的实现。
为解决 Warning 警告问题,我们对 TextField 组件进行了改造,确保无论传入 value 还是 defaultValue,都能满足两种状态的需求。使用 useMergedState Hook,内部 state 通过受控方式处理,同时外部传入的值根据需要分别作为 value 和 defaultValue。
在 useMergedState 的源码中,初始化逻辑与自定义的逻辑基本一致,使用 useState 初始化状态。同步阶段处理外部传入的值更新 state,而更新阶段通过 changeEventPrevRef 保持对 prevValue 的正确引用,确保正确触发 onChange 事件。
通过 useEvent 包裹 onChange 函数,animate课件源码确保在 ReRender 时直接调用 fnRef.current,避免重新生成 onChange 定义。setState 方法额外接收 ignoreDestroy 参数,避免状态被销毁后仍被调用,优化内存使用。
批处理更新处理中,changeEventPrevRef 作用于非受控状态,确保在多次 patch 更新后仍然能获取正确的 prevValue,从而正确触发 onChange 事件。最终,当 mergedValue 的值更新时,会触发对应使用 LayoutEffect 的操作,确保状态更新逻辑的正确执行。
通过 useMergedState,我们能够灵活地在受控与非受控两种模式间切换,以适应不同场景的需要。这种设计不仅简化了组件的使用,同时也提供了更高的灵活性和兼容性。在日常开发中,合理运用 this Hook 可以有效提升表单处理的效率和用户体验。
react 中的网络请求库
在React开发中,网络请求库的选择显得尤为重要。本文旨在探讨几种常用的网络请求库,以及它们在React项目中的应用和区别。
首先,我们关注的是`@umijs/plugin-request`。这一插件是基于`umi-request`和`ahooks`的`useRequest`构建的,提供了一套统一的网络请求和错误处理方案。在项目中,通过引入`umi-request`,可以利用其强大的功能处理各种网络请求,而`ahooks`中的`useRequest`则提供了简洁易用的Hooks方式来管理网络请求。
`umi-request`作为核心组件,其源码位于`github.com/umijs/umi-request`。它的设计旨在简化网络请求的处理,包括请求的发起、响应的解析以及错误的捕捉等。而`ahooks`,一个由阿里团队维护的库,通过`useRequest`实现了高阶函数与React Hooks的结合,使得开发者能够以更优雅的方式管理网络请求。
进一步,我们探讨`@ahooksjs/use-request`。这一组件是`ahooks`提供的用于实现网络请求的钩子函数,其源码位于`github.com/alibaba/hook/use-request`。通过`useRequest`,开发者可以方便地在React组件中执行异步操作,大象直播源码比如发送HTTP请求,并在请求完成时处理响应数据。
在实际应用中,引入`@umijs/plugin-request`意味着你将获得一套完整的网络请求解决方案。这一插件的源码位于`github.com/umijs/plugin-request`,其文档则在`umijs.org/plugins/plugin-request`上提供了详细的使用指导和示例。通过`@umijs/plugin-request`,开发者能够轻松地在React应用中执行网络请求,同时享受到统一的错误处理机制,确保应用在面对网络异常时能够保持稳定。
综上所述,`@umijs/plugin-request`基于`umi-request`和`ahooks`的`useRequest`,为React开发者提供了一套功能强大且易于使用的网络请求和错误处理方案。通过合理选择和应用这些库,可以显著提高React应用的开发效率和用户体验。
Preact(React)核心原理详解
本文作者:字节跳动 - 宝丁
在前端领域,React 是一个广为人知的前端开发框架,它革新了全栈 Web 开发的体验。React 引入了诸如 JSX、虚拟 DOM、组件化与合成事件等概念。然而,探究其源码时,庞大且晦涩的代码体系往往让这一过程变得艰难。为了解答这个挑战,本文将聚焦 Preact,一个轻量级的 React 替代方案,其体积仅为 3KB。Preact 的设计初衷是提升性能,简化复杂性。我们将从以下几个角度深入探讨 Preact 的核心原理。
一、Preact 是什么?
Preact 是一个功能与 React 类似的轻量级前端框架。它的名字中的“P”意味着“Performance”,强调了提高性能的初衷。Preact 旨在提供 React 的核心功能,同时以更简洁的代码实现。
二、Preact 与 React 的区别
1. 事件系统:React 内置了事件合成系统,主要使用 `onChange` 方法处理表单组件的值更新。相比之下,Preact 直接利用浏览器原生的事件系统,使用 `onInput` 方法,因此在体积上更加精简。
2. DOM 规范描述:在描述 DOM 类名时,React 要求使用 `className`。Preact 支持使用 `class` 直接描述 DOM 类名,导航领地源码使其更贴近原生 DOM 规范。
三、Preact 的工作流程
1. JSX:JSX 是 Preact 的模板语法,它允许用类似 HTML 的结构描述 DOM。经过转换,JSX 被编译为函数调用,进而生成原生 DOM 结构。
2. Virtual DOM:Preact 采用 Virtual DOM 技术,将 DOM 更新操作最小化。它通过比较虚拟 DOM 和实际 DOM,仅对发生变化的部分进行更新。
四、Diff 算法详解
1. Diff 儿童节点:比较新旧儿童节点,匹配相同的 key,进行差异比较,对未匹配的节点进行处理。
2. Diff 阶段:根据节点类型(Fragment、Component、DOM node)进行不同处理,如更新组件状态、创建新 DOM 节点或更新现有 DOM 节点。
3. Diff 属性:比较新旧 DOM 节点属性,仅更新变化的部分。
五、结合实际组件
通过一个简单的 Clock 组件,直观展示 Preact 的渲染流程,帮助理解 Preact 的工作原理。
六、Preact Hooks
Preact Hooks 是 React Hooks 的替代方案,提供了性能优化的组件状态管理机制。分为三类:MemoHook、ReducerHook 和 EffectHook,分别用于性能优化、状态管理与副作用操作。
七、结束语
本文深入介绍了 Preact 的核心原理,包括框架设计、与 React 的比较、工作流程、Diff 算法、实际应用以及 Hooks 机制。通过 Preact,前端开发者可以更高效地构建应用,同时理解其与 React 的异同。希望本文能够帮助大家更深入地掌握 Preact 的使用方法与性能优化策略。
没写过复杂 React 组件?来实现下 AntD 的 Space 组件吧
React 开发者在日常工作中经常编写组件,但这些大多为业务组件,KEEP APP源码复杂度并不高。
组件通常通过传入 props 并使用 hooks 组织逻辑来渲染视图,偶尔会用到 context 跨层传递数据。
相对复杂的组件是怎样的呢?antd 组件库中就有许多。
今天,我们将实现antd组件库中的一个组件——Space组件。
首先,我们来了解一下Space组件的使用方法:
Space是一个布局组件,用于设置组件的间距,还可以设置多个组件的对齐方式。
例如,我们可以使用Space组件来包裹三个盒子,设置方向为水平,渲染结果如下:
当然,我们也可以设置为垂直:
水平和垂直的间距可以通过size属性设置,如large、middle、small或任意数值。
多个子节点可以设置对齐方式,如start、end、center或baseline。
此外,当子节点过多时,可以设置换行。
Space组件还可以单独设置行列的间距。
最后,它还可以设置split分割线部分。
此外,你也可以不直接设置size,而是通过ConfigProvider修改context中的默认值。
Space组件会读取context中的size值,这样如果有多个Space组件,就不需要每个都设置,只需要添加一个ConfigProvider即可。
这就是Space组件的全部用法,简单回顾一下几个参数和用法:
Space组件的使用方法很简单,但功能非常强大。
接下来,我们来探讨一下这样的布局组件是如何实现的。
首先,我们来看一下它最终的DOM结构:
每个box都包裹了一层div,并设置了ant-space-item类。
split部分包裹了一层span,并设置了ant-space-item-split类。
最外层包裹了一层div,并设置了ant-space类。
这些看起来很简单,但实现起来却有很多细节。
下面我们来写一下Space组件的实现代码:
首先,我们声明组件props的类型。
需要注意的是,style是React.CSSProperties类型,即可以设置各种CSS样式。
split是React.ReactNode类型,即可以传入jsx。
其余参数的类型根据其取值而定。
Space组件会对所有子组件包裹一层div,因此需要遍历传入的children并做出修改。
props传入的children需要转换为数组,可以使用React.Children.toArray方法。
虽然children已经是数组了,但为什么还要使用React.Children.toArray转换一下呢?
因为toArray可以对children进行扁平化处理。
更重要的是,直接调用children.sort()会报错,而toArray之后就不会了。
因此,我们会使用React.Children.forEach、React.Children.map等方法操作children,而不是直接操作。
但这里我们有一些特殊的需求,比如空节点不过滤掉,依然保留。
因此,我们使用React.Children.forEach自己实现toArray:
这部分比较容易理解,就是使用React.Children.forEach遍历jsx节点,对每个节点进行判断,如果是数组或fragment就递归处理,否则push到数组中。
保不保留空节点可以根据keepEmpty的option来控制。
这样,children就可以遍历渲染item了,这部分是这样的:
我们单独封装了一个Item组件。
然后,我们遍历childNodes并渲染这个Item组件。
最后,我们将所有的Item组件放在最外层的div中:
这样就可以分别控制整体布局和Item布局了。
具体的布局还是通过className和样式来实现的:
className通过props计算而来,使用了classnames包,这是react生态中常用的包,根据props动态生成className基本都会使用这个包。
这个前缀是动态获取的,最终就是ant-space的前缀。
这些class的样式都定义好了:
整个容器使用inline-flex,然后根据不同的参数设置align-items和flex-direction的值。
最后一个direction的css可能大家没用过,是设置文本方向的。
这样,就通过props动态给最外层div添加了相应的className,设置了对应的样式。
但还有一部分样式没有设置,也就是间距。
其实这部分可以使用gap设置,当然,也可以使用margin,但处理起来比较麻烦。
不过,antd这种组件自然要做得兼容性好一点,所以两种都支持,支持gap就使用gap,否则使用margin。
问题来了,antd是如何检测浏览器是否支持gap样式的呢?
antd创建一个div,设置样式,并添加到body下,然后查看scrollHeight的值,最后删除这个元素。
这样就可以判断是否支持gap、column等样式,因为不支持的话高度会是0。
然后antd提供了一个这样的hook:
第一次会检测并设置state的值,之后直接返回这个检测结果。
这样组件里就可以使用这个hook来判断是否支持gap,从而设置不同的样式了。
最后,这个组件还会从ConfigProvider中取值,我们之前见过:
所以,我们再处理一下这部分:
使用useContext读取context中的值,并设置为props的解构默认值,这样如果传入了props.size就使用传入的值,否则使用context中的值。
这里给Item子组件传递数据也是通过context,因为Item组件不一定会在哪一层。
使用createContext创建context对象:
把计算出的size和其他一些值通过Provider设置到spaceContext中:
这样子组件就能拿到spaceContext中的值了。
这里使用了useMemo,很多同学不会用,其实很容易理解:
props变化会触发组件重新渲染,但有时候props并不需要变化却每次都变,这样就可以通过useMemo来避免它不必要的更新。
useCallback也是同样的道理。
计算size时封装了一个getNumberSize方法,为字符串枚举值设置了一些固定的数值:
至此,这个组件我们就完成了,当然,Item组件还没展开讲。
先来欣赏一下这个Space组件的全部源码:
回顾一下要点:
思路理得差不多了,再来看一下Item的实现:
这部分比较简单,直接上全部代码了:
通过useContext从SpaceContext中取出Space组件里设置的值。
根据是否支持gap来分别使用gap或margin、padding的样式来设置间距。
每个元素都用div包裹一下,设置className。
如果不是最后一个元素并且有split部分,就渲染split部分,用span包裹。
这块还是比较清晰的。
最后,还有ConfigProvider的部分没有看:
这部分就是创建一个context,并初始化一些值:
有没有感觉antd里用context简直太多了!
确实。
为什么?
因为你不能保证组件和子组件隔着几层。
比如Form和FormItem:
比如ConfigProvider和各种组件(这里是Space):
还有刚讲过的Space和Item。
它们能用props传数据吗?
不能,因为不知道隔几层。
所以antd里基本都是用context传数据的。
你会你在antd里会见到大量的用createContext创建context,通过Provider修改context值,通过Consumer或useContext读取context值的这类逻辑。
最后,我们来测试一下自己实现的这个Space组件吧:
测试代码如下:
这部分不用解释了。就是ConfigProvider包裹了两个Space组件,这两个Space组件没有设置size值。
设置了direction、align、split、wrap等参数。
渲染结果是正确的:
就这样,我们自己实现了antd的Space组件!
完整代码在github:github.com/QuarkGluonPl...
总结:
一直写业务代码,可能很少写一些复杂的组件,而antd里就有很多复杂组件,我们挑Space组件来写了下。
这是一个布局组件,可以通过参数设置水平、垂直间距、对齐方式、分割线部分等。
实现这个组件的时候,我们用到了很多东西:
很多同学不会封装布局组件,其实就是对整体和每个item都包裹一层,分别设置不同的class,实现不同的间距等的设置。
想一下,这些东西以后写业务组件是不是也可以用上呢?
setState是同步更新还是异步更新?
在React.8之前的版本中,我们更新数据需要用到setState,那么你知道setState是同步还是异步的呢?它内部是如何实现的,你了解吗?今天我们就一起来学习一下关于setState的那些事吧!setState自从React.8添加了Hook后,我们编写React组件基本都是函数组件,很少用到class组件了。我们都知道在函数组件中通过useState这个Hook来修改组件的状态,而在.8之前的版本中,我们都是通过setState来修改组件的状态,因此我们还是很有必要了解一下关于setState相关的知识点。
在面试或者工作中,我们经常会遇到关于组件状态更新的问题,就拿setState来说,在组件更新的时候,setState是同步更新还是异步更新的?当我们更新一个组件的状态后,我们如何才能立即获取到刚才更新的状态?这些就是我们需要学习和了解的地方。要了解setState是同步还是异步,就先需要了解一下React中的合成事件了。
如果了解过React事件相关方面的童鞋,那么就会知道React的事件都是合成事件,而不是js的原生事件。那什么是合成事件呢?简单来说就是React通过给document上挂载一个事件监听函数,通过事件冒泡的方式来完成事件的执行,当DOM元素触发后会冒泡到document,而React就会找到对应的组件生成一个合成事件出来,并按组件树模拟一遍事件冒泡,这就是React中的合成事件。
当然,上述的方法在React之后的版本中进行了修改。React中将事件挂载在了DOM的容器中,也就是挂载在ReactDOM执行的节点上,这样修改的好处是哪怕一个项目中有多个版本的React存在,组件的事件也不会乱套。那么哪些事件会被捕获生成合成事件呢?在React源码的事件快照中所包含的事件才会被捕获到,例如:click、blur、focus等等。
了解完合成事件,我们该继续学习setState是同步还是异步的。一般来说setState是异步的,例如下面这个例子:
clsssTestextendsComponent{ state={ count:0};componentDidMount(){ this.setState({ count:1},()=>{ console.log(this.state.count);//1});console.log(this.state.count);//0}render(){ return(...)}}当我们在componentDidMount生命周期中通过setState修改组件的状态后,我们只有在setState的第二个参数中才能立即获取到当前修改的值,而在外部获取到的值还是未改变之前的值,由此可以证明setState是异步的。我们是否会觉得React中的setState执行像是一个队列,因为React会根据队列逐一执行,并且合并setState的操作,当state数据完成后执行回调,然后根据结果来更新虚拟DOM触发渲染。那么为什么React官方团队要按这样的执行思路来实现呢?为什么不能用同步执行的思路来实现呢?
在React出来后,官方给出的解释的是:为了保持内部的一致性,如果setState是同步的,但是props却不是,就会导致数据的错乱;第二点就是为了后续的升级启用并发更新。那么什么情况下setState是同步的呢?
如果我们将setState放在setTimeout或者setInterval中,那么它们的执行就会跟上面将的完全不同了,大致的代码如下:
clsssTestextendsComponent{ state={ count:0};componentDidMount(){ this.setState({ count:this.state.count+1});console.log(this.state.count);//0setTimeout(()=>{ this.setState({ count:this.state.count+1});console.log('setTimeout',this.state.count);//1},0);}render(){ return(...)}}当setState执行时,会将状态存入padding队列中,然后React会判断当前是否处于batchupdate阶段,如果是,则会将组件存入dirtyComponents中;反之则会遍历所有的dirtyComponents,并调用updateComponent用于更新pending、state或者props。
在React的生命周期事件和合成事件中可以获取到isBatchingUpdates的控制权,用于将状态放入队列中,并控制执行的节奏。而在setTimeout、addEventListener这些原生事件中,无法获取到isBatchingUpdates的控制权,就会导致isBatchingUpdates只会为false,且会一直执行,因此setState就会同步执行并修改组件的状态。
最后setState其实并不是真的异步,只是看起来像是异步执行的,它是通过isBatchingUpdates来判断当前执行是同步还是异步的,如果isBatchingUpdates为true,则按异步执行,反之就是同步执行。要改变isBatchingUpdates,只需要打破React的合成事件,在js的原生事件中执行setState即可,所以你知道setState是同步还是异步的吗?
2024-11-30 15:39
2024-11-30 15:09
2024-11-30 15:08
2024-11-30 14:24
2024-11-30 13:41