【译】你的 React 组件的看起来怎么样?

代码异味 是什么意思? 简言之,就是暗示可能存在着深层次问题的代码结构。

代码异味

  • 太多的 props 传递
  • 矛盾的 props
  • 从 props 派生 state
  • 从函数中返回 JSX
  • 多个布尔类型的 state
  • 单组件中存在太多的 useState
  • 庞大的 useEffect

太多的 props 传递

传递多个 props 到一个组件中暗示着也许这个组件应该被拆分。

你可能会问多少才算太多呢?嗯…“看情况”。你也许会面对这样一种情况,一个组件有着 20 或更多个 props ,但是你依然感到没问题,因为这个组件只做一件事。不过,当你被一个有太多 props 的组件给绊住时,又或者,你急切的想要在已经够长的 props 列表上,不知第几个的 “最后一次” 再加上一个 props ,你可能需要思考以下几个事情:

这个组件是不是做了多件事

和函数一样,组件也应该只做一件事,并做好,所以,时常检查能否把一个组件拆分成多个小组件是一个好习惯。比如说这个组件存在不合适的 props 或 从函数中返回 JSX 。

能进行组合抽象吗?

一个很好却经常被忽略模式是组合组件,而非在一个组件处理所有逻辑。假如说,我们有一个处理用户申请某组织的组件:

<ApplicationForm
  user={userData}
  organization={organizationData}
  categories={categoriesData}
  locations={locationsData}
  onSubmit={handleSubmit}
  onCancel={handleCancel}
  ...
/>

我们可以看到这个组件的所有 props 都和这个组件做什么有关系,但是依然存在优化的空间,可以将组件承担的一些责任迁移到它的 children 里

<ApplicationForm onSubmit={handleSubmit} onCancel={handleCancel}>
  <ApplicationUserForm user={userData} />
  <ApplicationOrganizationForm organization={organizationData} />
  <ApplicationCategoryForm categories={categoriesData} />
  <ApplicationLocationsForm locations={locationsData} />
</ApplicationForm>

现在,我们确保了 ApplicationForm 仅仅处理其应该承担的责任,提交和撤销表单。子组件可以很好在全局上仅仅处理与其自身相关的事情。这同样是一个使用 React Context 来进行父子组件通信的好机会。

我是不是传递了太多“配置” props

在一些场景下,将一些 props 聚合到一个单独的对象上是个好注意,比如说,可以更轻松的交换配置。如果我们有一个战士排序表格的组件:

<Grid
  data={gridData}
  pagination={false}
  autoSize={true}
  enableSort={true}
  sortOrder="desc"
  disableSelection={true}
  infiniteScroll={true}
  ...
/>

这些 props 中除了 data ,都可以认为是一些 配置项 。面对这种情况,将 Grid 改为接受一个 options 为 prop 也许是一个好主意。

const options = {
  pagination: false,
  autoSize: true,
  enableSort: true,
  sortOrder: 'desc',
  disableSelection: true,
  infiniteScroll: true,
  ...
}

<Grid
  data={gridData}
  options={options}
/>

这同样意味着当我们想要切换不同的选项时,很容易排除掉我们不想要的

矛盾的 props

避免传递看起来会互相冲突的 props

举个🌰,我们从创建一个处理文本的 组件开始,但过了一会,我们想要这个组件可以处理手机号的输入。组件的实现可能是这样:

function Input({ value, isPhoneNumberInput, autoCapitalize }) {
  if (autoCapitalize) capitalize(value)

  return <input value={value} type={isPhoneNumberInput ? 'tel' : 'text'} />
}

问题在于 isPhoneNumberInput 和 autoCapitalize 两个 props 在逻辑上是冲突的。我们是无法将手机号进行大写的。

在这种情况下,解决方法是将组件拆分成多个小组件。如果我们仍然有一些逻辑需要复用,我们可以将其移动到 自定义 hook 中:

function TextInput({ value, autoCapitalize }) {
  if (autoCapitalize) capitalize(value)
  useSharedInputLogic()

  return <input value={value} type="text" />
}

function PhoneNumberInput({ value }) {
  useSharedInputLogic()

  return <input value={value} type="tel" />
}

虽然这个例子显得有点刻意了,但找到逻辑上冲突的 props 是一个迹象,意味着,你应该检查组件是否需要被拆分。

从 props 派生 state

不要通过从 props 派生 state 使得数据之间的关联性消失。

思考下这个组件

function Button({ text }) {
  const [buttonText] = useState(text)

  return <button>{buttonText}</button>
}

通过传递 text prop 作为 useState 的初始值,现在这个组件会 忽略 后续发生变化的 text prop。即使 text prop 的发生了变化,这个组件依然只会渲染其第一次得到的值。对于大多数 props 来说,这通常是一种意料之外的行为,会导致组件更容易产生 bug 。
作为这种现象更实际的一个例子是,想要根据 prop 进行一些计算,来派生出 state 。在下面的这个例子中,我们调用 slowlyFormatText 函数来格式化我们的 text prop ,执行这个函数需要花费大量的时间。

function Button({ text }) {
  const [formattedText] = useState(() => slowlyFormatText(text))

  return <button>{formattedText}</button>
}

通过将其放到 state 中,我们避免了函数反复执行的问题,但是我们同样使得组件无法响应 props 的更新。一个更好的方法是通过使用 useMemo hook 来缓存函数结果,来解决这个问题

function Button({ text }) {
  const formattedText = useMemo(() => slowlyFormatText(text), [text])

  return <button>{formattedText}</button>
}

现在 slowlyFormatText 仅仅在 text 改变的时候重新执行,不会导致组件无法响应更新

有时候我们的确需要忽略某个 prop 的所有后续更新,比如说一个颜色选择器,我们通过配置项来设置了所选颜色的初始值后,当用户选了另一个颜色时,我们不想更新覆写用户的选择的初始数据。在这种情况下,从 prop 的值直接复制到 state 是完全没问题的,不过为了表明这种行为模式,大多数开发者一般将加一些前缀到 prop 的名字,比如 initial 或 default ( initialColor / defaultColor ) 。

从函数中返回 JSX

不要从一个组件内部使用函数返回 JSX

这种模式自从函数组件变得流行后已经大规模消失了,但我时而会遇到他们。用一个例子来演示下我的意思:

function Component() {
  const topSection = () => {
    return (
      <header>
        <h1>Component header</h1>
      </header>
    )
  }

  const middleSection = () => {
    return (
      <main>
        <p>Some text</p>
      </main>
    )
  }

  const bottomSection = () => {
    return (
      <footer>
        <p>Some footer text</p>
      </footer>
    )
  }

  return (
    <div>
      {topSection()}
      {middleSection()}
      {bottomSection()}
    </div>
  )
}

也许你刚开始会觉得没什么问题,与好的模式相比,这段代码会使得人们难以快速理解发生了什么,应该避免这种模式。可以通过内联 JSX 解决,因为一个庞大的返回值并不意味着 庞大 的问题,但更合理的做法是将这些部分拆分成不同的组件。

记住,创建一个新组件并不代表你必须将其移动到一个新文件中。将多个相关联的组件放在同一个文件中是同样合理的行为。

多个布尔类型的 state

避免使用多个布尔值来表征组件的状态。

当你正在写一个组件,并经常扩展组件的功能时,很容易就会遇到使用多个布尔值来表征组件当前状态的情况。对于一个被点击时进行网络请求的按钮组件来说,你可能会有这样的实现:

function Component() {
  const [isLoading, setIsLoading] = useState(false)
  const [isFinished, setIsFinished] = useState(false)
  const [hasError, setHasError] = useState(false)

  const fetchSomething = () => {
    setIsLoading(true)

    fetch(url)
      .then(() => {
        setIsLoading(false)
        setIsFinished(true)
      })
      .catch(() => {
        setHasError(true)
      })
  }

  if (isLoading) return <Loader />
  if (hasError) return <Error />
  if (isFinished) return <Success />

  return <button onClick={fetchSomething} />
}

当按钮被点击时,我们将 isLoading 设置为 true ,然后是用 fetch 进行网络请求。如果请求是成功的,我们设置 isLoading 为 false , isFinished 为 true ,否则我们设置 hasError 为 true ,如果存在 error 的话。
尽管组件可以正常的完成工作,却很难去理解当前组件所处的状态,并且比其他方案更容易出错。我们同于可能陷入到 ”非法状态“ 中,假如我们意外的同时将 isLoading 和 isFinished 设置为 true。
解决这种状况的好办法是通过 ”枚举“ 来管理状态。在其他语言中,枚举是一种定义变量的方法,该变量仅允许设置为预定义的常量值集合,严格来讲,枚举在 JavaScript 并不存在,我们可以使用字符串作为枚举值来获得相关的好处

function Component() {
  const [state, setState] = useState('idle')

  const fetchSomething = () => {
    setState('loading')

    fetch(url)
      .then(() => {
        setState('finished')
      })
      .catch(() => {
        setState('error')
      })
  }

  if (state === 'loading') return <Loader />
  if (state === 'error') return <Error />
  if (state === 'finished') return <Success />

  return <button onClick={fetchSomething} />
}

这样做,我们避免了非法状态发生的可能,并使得识别组件当前状态变得容易起来。最终,如果你在使用某种类型系统比如说 TypeScript 那就更好了,因为你可以这样来指示可能的 state

const [state, setState] = useState<'idle' | 'loading' | 'error' | 'finished'>('idle')

太多的 useState

避免在单组件中使用太多的 useState hooks 。
一个带有多个 useState hooks 的组件很可能在做多个事情,可能更好的做法是拆分成多个组件,但是确实也存在一些复杂的情况,我们需要在单组件中管理各种复杂的状态

这里有个例子,演示了一个 autocomplete 组件中的状态和函数是什么样子的:

function AutocompleteInput() {
  const [isOpen, setIsOpen] = useState(false)
  const [inputValue, setInputValue] = useState('')
  const [items, setItems] = useState([])
  const [selectedItem, setSelectedItem] = useState(null)
  const [activeIndex, setActiveIndex] = useState(-1)

  const reset = () => {
    setIsOpen(false)
    setInputValue('')
    setItems([])
    setSelectedItem(null)
    setActiveIndex(-1)
  }

  const selectItem = (item) => {
    setIsOpen(false)
    setInputValue(item.name)
    setSelectedItem(item)
  }

  ...
}

我们有 reset 函数来重置所有状态,和一个 selectItem 函数来更新状态 。这些函数都必须使用一些来自 useState hook 返回的 setter 来完成相关的任务。 现在想象一下,我们还有许多其他的操作需要更新状态,并且很容易看出长期情况下,这种状况很难保证不产生任何 bug 。面对这些场景,通过使用 useReducer hook 来管理状态,能对我们起到很大的帮助。

const initialState = {
  isOpen: false,
  inputValue: "",
  items: [],
  selectedItem: null,
  activeIndex: -1
}
function reducer(state, action) {
  switch (action.type) {
    case "reset":
      return {
        ...initialState
      }
    case "selectItem":
      return {
        ...state,
        isOpen: false,
        inputValue: action.payload.name,
        selectedItem: action.payload
      }
    default:
      throw Error()
  }
}

function AutocompleteInput() {
  const [state, dispatch] = useReducer(reducer, initialState)

  const reset = () => {
    dispatch({ type: 'reset' })
  }

  const selectItem = (item) => {
    dispatch({ type: 'selectItem', payload: item })
  }

  ...
}

通过使用 reducer ,我们将状态管理相关的逻辑封装了起来,并将相关的复杂性与组件隔离了出来。我们可以单独的去思考我们的组件和状态,这使得我们理解到底发生了什么很有帮助。

useState 和 useReducer 有各自的 利弊 和 使用场景。我最喜欢的 reducer 模式的是 state reducer pattern by Kent C. Dodds 。

庞大的 useEffect

避免会同时做很多事情庞大的 useEffect 。这会使得代码容易出错和难以理解。

hooks 发布时我经常犯的一个错误是在一个 useEffect 里面放了太多东西 。 为了演示,这是仅含一个 useEffect 的组件:

function Post({ id, unlisted }) {
  ...

  useEffect(() => {
    fetch(`/posts/${id}`).then(/* do something */)

    setVisibility(unlisted)
  }, [id, unlisted])

  ...
}

尽管这个 effect 并不庞大,但依然同时做了多个事情。当 unlisted prop 更新后,我们同样会去拉取数据,尽管 id 并没有发生变化。
为了捕捉住这样的错误,我尝试写或说 ”当 [dependencies] 改变时做这个“ 来描述 effect ,给自己参考。将这种模式应用到上述的 effect 我们可以得到 ”当 id 或 unlisted 改变后,拉取数据 和 更新状态“。如果这个句子包括字词 ”和“ 或 ”或“ 这通常暗示了有问题存在。

应该将这个 effect 拆分成两个 effect:

function Post({ id, unlisted }) {
  ...

  useEffect(() => { // when id changes fetch the post
    fetch(`/posts/${id}`).then(/* ... */)
  }, [id])

  useEffect(() => { // when unlisted changes update visibility
    setVisibility(unlisted)
  }, [unlisted])

  ...
}

这样做,我们减少了组件的复杂度,使其更容易理解并降低了发生 bug 的可能性。

总结

好了,这就是全部了!记住,这些东西并不是意味着规则,更多是一种信号,指示着也许有些事情可能 ”出错“ 了。你肯定会遇到有充分理由要需要用到以上方式的情况。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目标检测(Object Detection)是计算机视觉领域的一个核心问题,其主要任务是找出图像中所有感兴趣的目标(物体),并确定它们的类别和位置。以下是对目标检测的详细阐述: 一、基本概念 目标检测的任务是解决“在哪里?是什么?”的问题,即定位出图像中目标的位置并识别出目标的类别。由于各类物体具有不同的外观、形状和姿态,加上成像时光照、遮挡等因素的干扰,目标检测一直是计算机视觉领域最具挑战性的任务之一。 二、核心问题 目标检测涉及以下几个核心问题: 分类问题:判断图像中的目标属于哪个类别。 定位问题:确定目标在图像中的具体位置。 大小问题:目标可能具有不同的大小。 形状问题:目标可能具有不同的形状。 三、算法分类 基于深度学习的目标检测算法主要分为两大类: Two-stage算法:先进行区域生成(Region Proposal),生成有可能包含待检物体的预选框(Region Proposal),再通过卷积神经网络进行样本分类。常见的Two-stage算法包括R-CNN、Fast R-CNN、Faster R-CNN等。 One-stage算法:不用生成区域提议,直接在网络中提取特征来预测物体分类和位置。常见的One-stage算法包括YOLO系列(YOLOv1、YOLOv2、YOLOv3、YOLOv4、YOLOv5等)、SSD和RetinaNet等。 四、算法原理 以YOLO系列为例,YOLO将目标检测视为回归问题,将输入图像一次性划分为多个区域,直接在输出层预测边界框和类别概率。YOLO采用卷积网络来提取特征,使用全连接层来得到预测值。其网络结构通常包含多个卷积层和全连接层,通过卷积层提取图像特征,通过全连接层输出预测结果。 五、应用领域 目标检测技术已经广泛应用于各个领域,为人们的生活带来了极大的便利。以下是一些主要的应用领域: 安全监控:在商场、银行
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值