您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

React API 和代码重用的演变!

时间:2023-05-29 13:17:56  来源:前端充电宝  作者:

本文将探究 React API 的演变及其背后的心智模型。从 mixins 到 hooks,再到 RSCs,了解整个过程中的权衡。我们将对 React 的过去、现在和未来有一个更清晰的了解,便于深入研究遗留代码库并评估其他技术如何采用不同的方法并做出不同的权衡。

React API 简史

我们从面向对象的设计模式在 JS 生态系统中流行的时候开始,可以在早期的 React API 中看到这种影响。

Mixins

React.createClass API 是创建组件的原始方式。在 JAVAscript 支持原生类语法之前,React 就有自己的类表示。Mixins 是一种用于代码重用的通用 OOP 模式,下面是一个简化的例子:

function ShoppingCart() {
  this.items = [];
}

var orderMixin = {
  calculateTotal() {
    // 从 this.items 计算
  }
  // .. 其他方法
}

Object.assign(ShoppingCart.prototype, orderMixin)
var cart = new ShoppingCart()
cart.calculateTotal()

JavaScript 不支持多重继承,因此 mixin 是重用共享行为和扩充类的一种方式。那该如何在使用 createClass 创建的组件之间共享逻辑呢?Mixins 是一种常用的模式,它可以访问组件的生命周期方法,允许我们组合逻辑、状态等:

var SubscriptionMixin = {
  getInitialState: function() {
    return {
      comments: DataSource.getComments()
    };
  },
  // 当一个组件使用多个 mixin 时,React 会尝试合并多个mixin的生命周期方法,因此每个都会被调用
  componentDidMount: function() {
    console.log('do something on mount')
  },
  componentWillUnmount: function() {
    console.log('do something on unmount')
  },
}
// 将对象传递给 createClass
var CommentList = React.createClass({
  // 在 mixins 属性下定义它们
  mixins: [SubscriptionMixin, AnotherMixin, SomeOtherMixin],
  render: function() {
    var { comments, ...otherStuff } = this.state
    return (
      <div>
        {comments.map(function(comment) {
          return <Comment key={comment.id} comment={comment} />
        })}
      </div>
    )
  }
})

对于较小的应用,这种方式可以正常运行。但是,当 Mixin 应用到大型项目时,它们也有一些缺点:

  • 名称冲突:Mixin 具有共享的命名空间,当多个 Mixin 使用同一个方法或状态的名称时,会发生冲突。
  • 隐式依赖关系:确定哪个 Mixin 提供了哪些功能或状态比较麻烦。它们使用共享的属性键相互交互,从而创建隐式耦合。
  • 难以理解和调试:Mixin 通常使组件更难以理解和调试。例如,上面多个 Mixin 都可以对 getInitialState 结果有影响,使得跟踪问题变得更加困难。

在感受到这些问题的痛苦之后,React 团队发布了“Mixins Considered Harmful”,不鼓励继续使用这种模式。

高阶组件

当 Javascript 中支持了原生类语法后,React 团队就在 v15.5 中弃用了 createClass API,支持原生类。

在这个转变过程中,我们仍然按照类和生命周期的思路来思考,因此没有进行重大的心智模型转变。现在可以扩展包含生命周期方法的 Reacts Component 类:

class MyComponent extends React.Component {
  constructor(props) {
    // 在组件挂载到 DOM 之前运行
    // super 指的是父 Component 的构造函数
    super(props)
  }
  componentWillMount() {}
  componentDidMount(){}
  componentWillUnmount() {}
  componentWillUpdate() {}
  shouldComponentUpdate() {}
  componentWillReceiveProps() {}
  getSnapshotBeforeUpdate() {}
  componentDidUpdate() {}
  render() {}
}

考虑到 mixin 的缺陷,我们该如何以这种编写 React 组件的新方式来共享逻辑和副作用呢?

这时候,高阶组件 (HOC) 就出现了,它的名字来源于高阶函数的函数式编程概念。它成为了替代 Mixin 的一种流行方式,并出现在像 Redux 这样的库的 API 中,例如它的 connect 函数,用于将组件连接到 Redux 存储。除此之外,还有 React Router 的 withRouter。

// 一个创建增强组件的函数,有一些额外的状态、行为或 props
const EnhancedComponent = myHoc(MyComponent);

// HOC 的简化示例
function myHoc(Component) {
  return class extends React.Component {
    componentDidMount() {
      console.log('do stuff')
    }
    render() {
      // 使用一些注入的 props 渲染原始组件
      return <Component {...this.props} extraProps={42} />
    }
  }
}

高阶组件对于在多个组件之间共享通用行为非常有用。它们使包装的组件保持解耦和通用性,以便可以重用。然而,HOC 遇到了与 mixin 类似的问题:

  • 名称冲突:因为 HOC 需要转发和传播 ...this.props 到包装的组件中,所以嵌套的 HOC 相互覆盖可能会发生冲突。
  • 难以静态类型检查:当多个嵌套的 HOC 将新的 props 注入到包装的组件中时,正确的 props 输入很难保证。
  • 数据流模糊:对于 mixins,问题是“这个状态从哪里来?”;对于 HOC,问题是“这些 props 从哪里来?”。因为它们是在模块级别静态组合的,所以很难跟踪数据流。

除了这些陷阱之外,过度使用 HOC 还导致了深度嵌套和复杂的组件层次结构以及难以调试的性能问题。

Render props

render prop 模式作为 HOC 的替代品出现,这种模式由开源 API 如 React-Motion 和 downshift 以及构建 React Router 的开发人员推广普及。

<Motion style={{ x: 10 }}>
  {interpolatingStyle => <div style={interpolatingStyle} />}
</Motion>

主要思想就是将一个函数作为 props 传递给组件。然后组件会在内部调用该函数,并传递数据和方法,将控制反转回函数以继续渲染它们想要的内容。

与 HOC 不同,组合发生在 JSX 内部的运行时,而不是静态模块范围内。它们没有名称冲突,因为很明确知道是从哪里来的,也更容易进行静态类型检查。

但是,当用作数据提供者时,它们可能会导致深度嵌套,创建一个虚假的组件层次结构:

<UserProvider>
  {user => (
    <UserPreferences user={user}>
      {userPreferences => (
        <Project user={user}>
          {project => (
            <IssueTracker project={project}>
              {issues => (
                <Notification user={user}>
                  {notifications => (
                    <TimeTracker user={user}>
                      {timeData => (
                        <TeamMembers project={project}>
                          {teamMembers => (
                            <RenderThangs renderItem={item => (
                                // ...
                            )}/>
                          )}
                        </TeamMembers>
                      )}
                    </TimeTracker>
                  )}
                </Notification>
              )}
            </IssueTracker>
          )}
        </Project>
      )}
    </UserPreferences>
  )}
</UserProvider>

这时,通常会将管理状态的组件与渲染 UI 的组件分开来处理。随着 Hooks 的出现,“容器”和“展示性”组件模式已经不再流行。但值得一提的是,这种模式在服务逇组件中有所复兴。

目前,render props 仍然是创建可组合组件 API 的有效模式。

Hooks

Hooks 在 React 16.8 版本中成为了官方的重用逻辑的方式,巩固了将函数组件作为编写组件的推荐方式。

Hooks 让在组件中重用和组合逻辑变得更加简单明了。相比于类组件,在其中封装并共享逻辑会更加棘手,因为它们可能分散在各种生命周期方法中的不同部分。

深度嵌套的结构可以被简化和扁平化。搭配 TypeScript,Hook 也很容易进行类型化。

function Example() {
  const user = useUser();
  const userPreferences = useUserPreferences(user);
  const project = useProject(user);
  const issues = useIssueTracker(project);
  const notifications = useNotification(user);
  const timeData = useTimeTracker(user);
  const teamMembers = useTeamMembers(project);
  return (
    <div>
      {/* 渲染内容 */}
    </div>
  );
}

权衡利弊

使用 Hooks 带来了很多好处,它们解决了类中的一些问题,但也需要付出一定的代价,下面来深入了解一下。

类 vs 函数

从组件消费者的角度来看,类组件到函数组件的转变并没有改变渲染 JSX 的方式。不过两种方式的思想是不同的:

  • 类与有状态类的面向对象编程有着紧密联系。
  • 函数则与函数式编程以及纯函数等概念有关联。

React 中的组件概念,以及使用 JavaScript 实现它的方式,以及我们试图使用现有术语来解释它,都增加了学习 React 的开发人员建立准确思维模型的困难度。对理解的漏洞会导致代码出现 bug。在这个过渡阶段中,一些常见的问题包括设置状态或获取数据时的无限循环,以及读取过时的 props 和 state。指令式响应事件和生命周期常常引入了不必要的状态副作用,我们可能并不需要它们。

开发者体验

在使用类组件时,有一套不同的术语,如 componenDid、componentWill、shouldComponent和将方法绑定到实例中。函数和 Hooks 通过移除外部类简化了这一点,使我们能够专注于渲染函数。每次渲染都会重新创建所有内容,因此需要能够在渲染周期之间保留一些内容。useCallback 和 useMemo 这样的 API 被引入就方便定义哪些内容应该在重新渲染之间保留下来。

在 Hooks 中需要明确管理依赖数组,再加上 hooks API 的语法复杂,对一些人来说富有挑战性。对其他人来说,hooks 大大简化了他们对 React 的思维模型和代码的理解。

实验性 React forget 旨在通过预编译 React 组件来改善开发者体验,从而消除手动记忆和管理依赖项数组,强调将事情明确化或尝试在幕后处理事情之间的权衡。

将状态和逻辑耦合到 React 中

许多状态管理库,如 Redux 或 MobX 将 React 应用的状态和视图分开处理。这与 React 最初作为MVC 中的“视图”标语保持一致。随着时间的推移,从全局的单块式存储向更多的位置迁移,特别是使用 render props 的“一切皆为组件”的想法,这也随着转向 hooks 得到了巩固。

React 演进背后的原则

我们可以从这些模式的演变中学到什么呢?哪些启发式可以指导我们做出有价值的权衡?

API 的用户体验

框架和库必须同时考虑开发者体验和最终用户体验。为开发者体验而牺牲用户体验是一种错误的做法,但有时候一个会优先于另一个。

例如,css in JS库 styled-components,在处理大量动态样式时使用起来非常棒,但它们可能以最终用户体验为代价,我们需要对此进行权衡。

我们可以将 React 18 和 RSC 中的并发特性视为追求更好的最终用户体验的创新。这些就意味着更新用来实现组件的 API 和模式。函数的“snapshotting”属性(闭包)使得编写在并发模式下正常工作的代码变得更加容易,服务端的异步函数是表达服务端组件的好方法。

API 优于实现

上面讨论的 API 和模式都是从实现组件内部的角度出发的。虽然实现细节已经从 createClass 发展到了 ES6 类,再到有状态函数。但“组件”这个更高级别的API概念,它可以是有状态的并具有 effect,已经在整个演进过程中保持了稳定性:

return (
  <ImplementedWithMixins>
    <ComponentUsingHOCs>
      <ThisUsesHooks>
        <ServerComponentWoah />
      </ThisUsesHooks>
    </ComponentUsingHOCs>
  </ImplementedWithMixins>
)

专注于正确的原语

在React中,组件模型让我们可以用声明式的方式来编写代码,并且可以方便地在本地进行处理。这使得代码更加易于移植,可以更轻松地删除、移动、复制和粘贴代码,而不会意外破坏其中的任何隐藏的连接。遵循这个模型的架构和模式可以提供更好的可组合性,通常需要保持局部化,让组件捕获相关的关注点,并接受由此带来的权衡。与这个模型不符的抽象化会使数据流变得模糊,并使跟踪和调试变得难以理解和处理,从而增加了隐含的耦合。一个例子就是从类到 hooks 的转换,将分布在多个生命周期事件中的逻辑打包成可组合的函数,可以直接放置在组件中的相应位置。

小结

考虑 React 的一个好方法是将其视为一个库,它提供了一组可在其上构建的低级原语。React非常灵活,可以按照自己的方式来设计架构,这既是一种福音,也可能带来一些问题。这也解释了为什么像 Remix 和 Next 这样的高级应用框架如此受欢迎,它们会在React基础之上添加更强烈的设计意图和抽象化。

React 的扩展心智模型

随着 React 将其范围扩展到客户端之外,它提供了允许开发人员构建全栈应用的原语。在前端编写后端代码开辟了一系列新的模式和权衡。与之前的转变相比,这些转变更多的是对现有心智模型的扩展,而不是需要忘记之前的范式转变。

在混合模型中,客户端和服务端组件都对整体计算架构有所贡献。在服务端做更多的事情有助于提高 web 体验,它允许卸载计算密集型任务并避免通过网络发送臃肿的包。但是,如果我们需要比完整的服务端往返延迟少得多的快速交互,则客户端驱动的方法会更好。React 就是从该模型的仅客户端部分演变而来的,但可以想象 React 首先从服务器开始,然后再添加客户端部分。

了解全栈 React

混合客户端和服务端需要知道边界在模块依赖图中的位置。这样就能够更好地理解代码在何时、何地以及如何运行。

为此,我们开始看到一种新的React模式,即指令(或类似于“use strict”、“use asm”或React Native中的“worklet”的编译指示),它们可以改变其后代码的含义。

理解“use client”

将此代码放置在导入代码之前的文件顶部,可以表明以下的代码是“客户端代码”,标志着与仅在服务端上运行的代码进行区分。其中导入的其他模块(及其依赖项)被认为是客户端包,通过网络传输。

使用“use client”组件也可以在服务端运行。例如,作为生成初始 html 或作为静态网站生成过程的一部分。

“use server”指令

Action 函数是客户端调用在服务端存在的函数的方式。可以将“use server”放置在服务器组件的 Action 函数顶部,以告诉编译器应该在服务端保留它。

// 在服务器组件内部
// 允许客户端引用和调用这个函数
// 不发送给客户端
// server (RSC) -> client (RPC) -> server (Action)
async function update(formData: FormData) {
  'use server'
  awAIt db.post.update({
    content: formData.get('content'),
  })
}

在 Next.js 中,如果一个文件顶部有“use server”,它告诉打包工具所有导出都是服务端  Action 函数,这确保函数不会包含在客户端捆绑包中。

当后端和前端共享同一个模块依赖图时,有可能会意外地发送一堆不想要的客户端代码,或者更糟糕的是,意外将敏感数据导入到客户端捆绑包中。为了确保这种情况不会发生,还有“server-only”包作为标记边界的一种方式,以确保其后的代码仅在服务端组件上使用。这些实验性的指令和模式也正在其他框架中进行探索,超越了 React,并使用类似于server$的语法来标记这种区别。

全栈组合

在这个转变中,组件的抽象被提升到一个更高的层次,包括服务端和客户端元素。这使得可以重用和组合整个全栈功能垂直切片的可能性。

// 可以想象可共享的全栈组件
// 封装了服务端和客户端的细节
<Suspense fallback={<LoadingSkelly />}>
  <AIPoweredRecommendationThing
    apiKey={proccess.env.AI_KEY}
    promptContext={getPromptContext(user)}
  />
</Suspense>

这种强大的能力是建立在 React 之上的元框架中使用的高级打包工具、编译器和路由器的基础上的,因此付出的代价来自于其底层的复杂性。同时,作为前端开发者,我们需要扩展自己的思维模型,以理解将后端代码与前端代码写在同一个模块依赖图中所带来的影响。

总结

本文探讨了很多内容,从mixin到服务端组件,探索了 React 的演变和每种范例的权衡。理解这些变化及其基础原则是构建一个清晰的 React 思维模型的好方法。准确的思维模型使我们能够高效地构建,并快速定位错误和性能瓶颈。



Tags:React   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
从0实现React18
要从零开始实现React 18,需要理解React的核心概念和一些主要特性。以下是一个简要的步骤:1. 了解React的基本概念: 组件: React应用的基本构建块。组件可以是函数组件(Functional...【详细内容】
2024-01-22  Search: React  点击:(47)  评论:(0)  加入收藏
React的核心概念
React是一个开源JavaScript库,用于构建用户界面。它由Facebook开发并维护,已成为构建Web和移动应用程序的流行选择。React的主要特点是组件化架构,它使开发人员能够将应用程序...【详细内容】
2024-01-09  Search: React  点击:(103)  评论:(0)  加入收藏
浅析五种 React 组件设计模式
作为一名 React 开发者,你可能会面临下面几个问题: 如何构建一个高复用度性的组件,使其适应不同的业务场景? 如何构建一个具有简单 API的组件,使其易于使用? 如何构建一个在 UI 和...【详细内容】
2024-01-09  Search: React  点击:(83)  评论:(0)  加入收藏
React与Vue性能对比:两大前端框架的性能
React和Vue是当今最流行的两个前端框架,它们在性能方面都有着出色的表现。React的加载速度:初次加载:由于React使用了虚拟DOM(Virtual DOM)技术,它可以通过比较虚拟DOM树与实际DOM...【详细内容】
2024-01-05  Search: React  点击:(107)  评论:(0)  加入收藏
Vanilla Design,新一代 React UI 库
这几天做需求,一堆 UI 库实在是不知道选哪个,各种角色的同事争论不休;还总有新轮子冒出来,所以我来插一脚,并借此来领悟写代码的哲学:The best way to write secure and reliable...【详细内容】
2024-01-04  Search: React  点击:(89)  评论:(0)  加入收藏
vue3中 ref和 reactive的区别 ?
最近有朋友在面试过程中经常被问到这么一个问题,vue3 中的ref 和 reactive的区别在哪里,为什么 要定义两个API 一个 api不能实现 响应式更新吗??带着这个疑问 ,我们 接下来进行逐...【详细内容】
2024-01-03  Search: React  点击:(38)  评论:(0)  加入收藏
React18 与 Vue3 全方面对比
1. 编程风格 & 视图风格1.1 编程风格 React 语法少、难度大;Vue 语法多,难度小例如指令:Vue<input v-model="username"/><ul> <li v-for="(item,index) in list" :key="inde...【详细内容】
2024-01-03  Search: React  点击:(72)  评论:(0)  加入收藏
使用React微前端的完整指南
译者 | 李睿审校 | 重楼事实表明,前端开发伴随着许多挑战。而寻找简化开发过程和加快任务执行的方法是每个开发团队的目标。在开发大型复杂产品时,让开发团队成员在任务上进行...【详细内容】
2023-12-26  Search: React  点击:(90)  评论:(0)  加入收藏
什么是React的错误边界(Error Boundary)?
React的错误边界(ErrorBoundary)是一种React组件,用于捕获并处理其子组件树中任何位置的JavaScript错误。它允许开发人员在应用程序中定义错误边界,以便在发生错误时显示备用UI...【详细内容】
2023-12-21  Search: React  点击:(125)  评论:(0)  加入收藏
如何设计更优雅的 React 组件?
在日常开发中,团队中每个人组织代码的方式不尽相同。下面我们就从代码结构的角度来看看如何组织一个更加优雅的 React 组件!1. 导入依赖项我们通常会在组件文件顶部导入组件所...【详细内容】
2023-12-21  Search: React  点击:(101)  评论:(0)  加入收藏
▌简易百科推荐
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(6)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(13)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(9)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(5)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(11)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(9)  评论:(0)  加入收藏
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Java技术指北  微信公众号  Tags:HashMap   点击:(11)  评论:(0)  加入收藏
如何从头开始编写LoRA代码,这有一份教程
选自 lightning.ai作者:Sebastian Raschka机器之心编译编辑:陈萍作者表示:在各种有效的 LLM 微调方法中,LoRA 仍然是他的首选。LoRA(Low-Rank Adaptation)作为一种用于微调 LLM(大...【详细内容】
2024-03-21  机器之心Pro    Tags:LoRA   点击:(12)  评论:(0)  加入收藏
这样搭建日志中心,传统的ELK就扔了吧!
最近客户有个新需求,就是想查看网站的访问情况。由于网站没有做google的统计和百度的统计,所以访问情况,只能通过日志查看,通过脚本的形式给客户导出也不太实际,给客户写个简单的...【详细内容】
2024-03-20  dbaplus社群    Tags:日志   点击:(4)  评论:(0)  加入收藏
Kubernetes 究竟有没有 LTS?
从一个有趣的问题引出很多人都在关注的 Kubernetes LTS 的问题。有趣的问题2019 年,一个名为 apiserver LoopbackClient Server cert expired after 1 year[1] 的 issue 中提...【详细内容】
2024-03-15  云原生散修  微信公众号  Tags:Kubernetes   点击:(6)  评论:(0)  加入收藏
站内最新
站内热门
站内头条