不积跬步,无以至千里。

我的学习笔记

首页

前端框架

react

内容更新于: 2022-05-31 10:13:37

HOC、render props、Hooks

前言:用了许久的react,在本文中对react进阶部分:高阶组件、render props、hooks的优缺点进行剖析。并且对一些使用场景进行分析及记录。并结合平时的工作内容给出具体的使用示例,以便之后温故知新。

(1).谈一谈 HOC、Render props、Hooks

在以前我们可能会看到很多文章在分析 HOC 和 render props, 但是在 2020 年 ,我们有了新欢 “hook” . 本篇文章会分析 hook , render props 和 HOC 三种模式的优缺点. 让你彻底理解这三种模式. 并且, 告诉你为何应该尽可能使用 hook.

(2).创建 HOC 的方式

学习 HOC 我们只需要记住以下 2 点定义: 创建一个函数, 该函数接收一个组件作为输入除了组件, 还可以传递其他的参数。 基于该组件返回了一个不同的组件.
代码如下:


function withSubscription(WrappedComponent, selectData) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        data: selectData(DataSource, props)
      };
    }
    // 一些通用的逻辑处理
    render() {
      // ... 并使用新数据渲染被包装的组件!
      return <WrappedComponent 
        data={this.state.data} 
        {...this.props} 
      />;
    }
  };
}


1.HOC 的优点

不会影响内层组件的状态, 降低了耦合度

2.HOC 的缺点

2-1.固定的 props 可能会被覆盖.


<MyComponent
  x="a"
  y="b"
/>


这 2 个值,如果跟高阶组件的值相同, 那么 x,y 都会被来自高阶组件的值覆盖.


// 如果 withMouse 和 withPage 使用了同样的 props, 比如 x , y.
// 则会有冲突.
export default withMouse(withPage(MyComponent));


2-2.它无法清晰地标识数据的来源

withMouse(MyComponent) 它不会告诉你组件中包含了哪些 props , 增加了调试和修复代码的时间。

(3).render props

1).功能

将一个组件内的 state 作为 props 传递给调用者, 调用者可以动态的决定如何渲染.

2).创建 render props 的方式

接收一个外部传递进来的 props 属性 将内部的 state 作为参数传递给调用组件的 props 属性方法. 代码如下:


class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.state = { x: 0, y: 0 };
  }

  render() {
    return (
      <div style={{ height: '100%' }}>
        // 使用 render props 属性来确定要渲染的内容
        {this.props.render(this.state)}
      </div>
    );
  }
}
// 调用方式:
<Mouse render={mouse => (
  <p>鼠标的位置是 {mouse.x},{mouse.y}</p>
)}/>


3).缺点

3-1.无法在 return 语句外访问数据
3-2.它不允许在 return 语句之外使用它的数据. 比如上面的例子, 不能在 useEffect 钩子或组件中的任何其他地方使用 x 和 y 值, 只能在 return 语句中访问.
3-3.嵌套 它很容易导致嵌套地狱. 如下代码


const MyComponent = () => {
  return (
    <Mouse>
      {({ x, y }) => (
        <Page>
          {({ x: pageX, y: pageY }) => (
            <Connection>
              {({ api }) => {
                // yikes
              }}
            </Connection>
          )}
        </Page>
      )}
    </Mouse>
  )
};


基于上面的两种方式,都有他们的缺点,那我们来到最香的解决方案: hook

(4).hook

我们来看一个例子:


const { x, y } = useMouse();
const { x: pageX, y: pageY } = usePage();

useEffect(() => {

}, [pageX, pageY]);


从上面的代码可以看出, hook 有以下的特性. 它解决了上面 hoc 和 render props 的缺点.

4-1.hook 可以重命名

如果 2 个 hook 暴露的参数一样,我们可以简单地进行重命名.

4-2.hook 会清晰地标注来源

从上面的例子可以简单地看到, x 和 y 来源于 useMouse. 下面的 x, y 来源于 usePage.

4-3.hook 可以让你在 return 之外使用数据

4-4.hook 不会嵌套

简单易懂, 对比 hoc 和 render props 两种方式, 它非常直观, 也更容易理解.

缺点:

1.只能在函数组件中使用。

2.响应式的useEffect

写函数组件时,你不得不改变一些写法习惯。你必须清楚代码中useEffect和useCallback等api的第二个参数“依赖项数组”的改变时机,并且掌握上下文的useEffect的触发时机。 当逻辑较复杂的时候,useEffect触发的次数,可能会被你预想的多。对比componentDidmount和componentDidUpdate,useEffect带来的心智负担更大。

3.状态不同步

这绝对是最大的缺点。函数的运行是独立的,每个函数都有一份独立的作用域。函数的变量是保存在运行时的作用域里面,当我们有异步操作的时候,经常会碰到异步回调的变量引用是之前的,也就是旧的(这里也可以理解成闭包)。


import React, { useState } from "react";
​
const Counter = () => {
  const [counter, setCounter] = useState(0);
​
  const onAlertButtonClick = () => {
    setTimeout(() => {
      alert("Value: " + counter);
    }, 3000);
  };
​
  return (
    <div>
      <p>You clicked {counter} times.</p>
      <button 
        onClick={() => setCounter(counter + 1)}
      >Click me</button>
      <button onClick={onAlertButtonClick}>
        Show me the value in 3 seconds
      </button>
    </div>
  );
};
​
export default Counter;


当你点击Show me the value in 3 seconds的后,紧接着点击Click me使得counter的值从0变成1。三秒后,定时器触发,但alert出来的是0(旧值),但我们希望的结果是当前的状态1。
这个问题在class component不会出现,因为class component的属性和方法都存放在一个instance上,调用方式是:this.state.xxx和this.method()。因为每次都是从一个不变的instance上进行取值,所以不存在引用是旧的问题。
其实解决这个hooks的问题也可以参照类的instance。用useRef返回的immutable RefObject(current属性是可变的)来保存state,然后取值方式从counter变成了: counterRef.current。如下:


import React, { useState, useRef, useEffect } from "react";
​
const Counter = () => {
  const [counter, setCounter] = useState(0);
  const counterRef = useRef(counter);
​
  const onAlertButtonClick = () => {
    setTimeout(() => {
      alert("Value: " + counterRef.current);
    }, 3000);
  };
​
  useEffect(() => {
    counterRef.current = counter;
  });
​
  return (
    <div>
      <p>You clicked {counter} times.</p>
      <button 
        onClick={() => setCounter(counter + 1)}
      >Click me</button>
      <button onClick={onAlertButtonClick}>
        Show me the value in 3 seconds
      </button>
    </div>
  );
};
​
export default Counter;


总结

在 hook、render props 和 HOC(higher-order components) 之间选择时,应该尽可能使用 hook.

本文结束