Better

Ethan的博客,欢迎访问交流

React 代码逻辑复用的发展

复习 & 总结

背景

在开发 React 组件时,我们要尽可能避免在每个组件中编写重复的代码。而是应该将这部分逻辑抽象出来,形成通用的功能,并在不同组件之间共享这些功能。

Mixin

Mixin 是 React 初期用来解决代码复用问题而提出的,后来因为隐式依赖和命名冲突的问题,逐步被 HOC 和 Hooks 所取代。

Mixin 并不是 React 独有的概念,而是一种普遍适用于面向对象编程领域的设计模式。

继承分为单继承和多继承问题,多继承是相当危险的,会带来继承歧义的问题。比如『钻石问题』。但只使用单继承,又无法实现一些繁琐的抽象。Mixin 就是实现多继承的一种模式。

为什么不推荐使用 Mixin

  • 组件和 Mixin 的关系是非显示的,混乱的,组件可以调用 Mixin 方法,Mixin 中也可以使用组件中定义的状态和方法,两者之间的联系仅仅时组件声明的 Mixin 数组
  • 由于 JavaScript 是动态语言,因此语言层面无法追踪和管理这些依赖
  • 场景1:由于 Mixin 可以使用组件的 state,但后续迭代中,你可能会修改组件的 state,这时候你会考虑到该 state 被某一个 Mixin 使用了吗?如果该迭代由其他同事开发,他就更难发现某个角落有个 Mixin 在默默使用这个 state。就算你发现了要修改某个 Mixin,但是你有什么办法保证这个 Mixin 没有被别人组件使用呢。你的修改可能会破坏掉另外一个使用该 Mixin 的组件,但你对此可能一无所知
  • 场景2:如果你在组件中使用某个方法,但组件中没有发现该发现,基本可以认定是 Mixin 混入的,当你发现组件的 Mixin 列表中声明了很多个 Mixin 时,你一定很奔溃。更可怕的是一个 Mixin 可以混入另外的 Mixin,理论上来讲,查找的层次可以是无限的
  • 场景3:如果你是 Mixin 的开发者,你可能永远不知道你添加一个方法会带来什么后果,可能会和使用该组件的某个函数同名,从而出现命名冲突的情况

HOC

从函数式编程中,高阶函数演变过来的,本身是一个函数,接收组件作为参数,并返回一个新的组件。

Mixin 是面向对象语言的模式,HOC 就是函数式编程的模式

简单 HOC 演示

const withLoading = WrappedComponent => {
  const ComponentWithLoading = props => {
    const [isLoadingVisible, setLoadingVisible] = useState(false)
    return (
      <>
        {isLoadingVisible ? <div className="loading">Loading...</div> : null}
        <WrappedComponent {...props} setLoadingVisible={setLoadingVisible} />
      </div>
    )
  }
  return ComponentWithLoading
}

HOC 与 Mixin 相比的具体优势

  • HOC 这种模式是将组件进行更高一层的封装,组件本身并不会受到任何入侵
  • HOC 中组件和功能是解耦的,HOC 可以被应用到任何组件中,组件也可以被任何 HOC 包裹,随意拆分和组合

HOC 具体应用

  • 日志收集
  • 状态传递
  • 身份认证
  • UI 动作响应

以上常用的功能都可以被抽象成各种各样的 HOC。

HOC 存在的问题

  • JSX 嵌套地狱,并且其中大部分节点都是无意义的
  • 不要轻易更改 HOC 嵌套的顺序,HOC 的本质是组合,组合就一定会涉及到执行的先后顺序,如果破坏了顺序,可能会就会出问题
  • props 传递问题,多个 HOC 组合时,需要将顶层的 props 一层一层传递到最内部的组件,在传递的过程中,props 可能会在每一层 HOC 中被加工,这样的加工是孤立的,当前的 HOC 不知道别的 HOC 对 props 做了什么,别的 HOC 也不知道当前的 HOC 对 props 做了什么,这就带来了修改冲突

Render Props

开发者可以将一些通用的状态和逻辑封装在特定组件中,之后通过 Render Props 的渲染函数将这些通用功能复用到其他组件中。

Render Props 就是为父组件的 props 添加一个返回组件的函数,并在父组件 render 的时候执行这个函数

实时追踪鼠标位置示例

const MouseTracker = props => {
  const [position, setPosition] = useState({x: 0, y: 0})

  const handleMouseMove = event => {
    setPosition({
      x: event.clientX,
      y: event.clientY
    })
  }

  return (
    <div onMouseMove={handleMouseMove}>
      {props.render(position)}
    </div>
  )
}

// 具体使用
const App = () => {
  return <MouseTracker render={position => <Picture mouse={position}>} />
}

props 上的任何函数都可以执行渲染行为,甚至是函数子节点

上一节了解了 HOC,该功能也可以使用 HOC 实现如下,你可以直观对比下

const withMousePosition = WrappedComponent => {
  const ComponentWithMousePosition = props => {
    const [position, setPosition] = useState({x: 0, y: 0})

    const handleMouseMove = event => {
      setPosition({
        x: event.clientX,
        y: event.clientY
      })
    }

    return (
      <div onMouseMove={handleMouseMove}>
        <WrappedComponent {...props} mousePosition={position} />
      </div>
    )
  }
  return withMousePosition
}

// 具体使用
const PictureWithMousePosition = withMousePosition(<Picture />)

const App = () => {
  return <PictureWithMousePosition />
}

既然 HOC 也可以达到目的,那么为什么不采用 HOC,而会出现 Render Props 这种模式呢

  • HOC 嵌套地狱会给调试带来困难
  • HOC props 传递问题,如果两个组件先后向 props 注入了同名的属性,会产生命名冲突问题
  • Render Props 正好解决了上述两个问题
    • 不会额外增加任何用于包装的组件,所有被渲染的组件都是真实有意义的,比如上面的例子 HOC 还会增加 withMousePosition 这一层组件
    • 不会出现 props 传递异常问题,因为命名有组件自身决定
  • Render Props 最大的不同点:组件的组合时机
    • HOC 是静态执行
      • 由于每执行一次都会产生新的组件,因此 HOC 都会提前在组件外部执行
      • 由于在组件外执行,因此无法获取,组件状态,无法感知组件的声明周期,因此如果想根据组件状态,判断执行那种 HOC 组合是不可能的
    • Render Props 是动态执行
      • 可以很好的利用组件的声明周期和状态,能够在渲染时动态决定执行哪些组件

Render Props 存在的问题

  • Callback 嵌套地狱问题
  • 与 PureComponent 产生冲突所引发的性能问题,因此在使用 Render Props 需要注意将 Render 函数变成不可变引用

Hooks

Hooks 并不会像 Mixin 那样入侵你的组件,也不会像 HOC 哪些打乱组件的结构,用一种很自然的方式,将逻辑从组件中提取出来。

关于 Hooks 已经在其他文章说的够多了,就不赘述了。



留言