React进阶-状态逻辑复用

目录

1. 组件复用的说明

2. mixins 混入(已废弃)

3. 高阶组件

概述

基本使用

封装 withMouse 高阶组件

高阶组件的注意点

4. render-props 模式

基本使用

children 代替 render 属性

5. React Hooks 状态逻辑复用

6. 为什么要有 Hooks

7. 性能优化

8. 优化的方向

9. React.memo

浅对比的说明

10. useCallback

11. useMemo

12. class 组件优化

1. 组件复用的说明

问题 1:如果两个组件中的部分功能相似或相同,但 UI 结构不同,该如何优化相似的功能?复用

问题 2:复用什么?1. state 状态 2. 操作 state 状态的方法。也就是:组件状态逻辑复用

在 Hooks 之前,组件的状态逻辑复用经历了:mixins(混入)HOCs(高阶组件)render-props 等模式

注意:这几种方式不是新的 API,而是利用 React 自身特点的编码技巧,演化而成的固定模式(写法)

通过一个鼠标位置的案例,来演示组件的状态逻辑复用

2. mixins 混入(已废弃)

参考:React mixins

参考:mixins 弊大于利

存在的问题:

  • Mixins 引入了隐式依赖关系,组件中的方法和数据的来源不明确,不利于维护

  • Mixins 导致名称冲突

3. 高阶组件

参考:React 文档 HOC

概述

高阶组件(HOC,High-Order Component)作用:通过增强组件的能力,来实现组件状态逻辑复用

采用 包装(装饰)模式 ,比如,

  • 手机:获取保护功能

  • 手机壳 :提供保护功能

高阶组件就相当于手机壳,通过包装组件,增强组件功能

基本使用

高阶组件是一个函数,接收要包装的组件,返回增强后的组件

高阶组件的命名约定以 with 开头,比如:withMousewithRouter

原理:高阶组件内部创建一个类组件,在这个类组件中提供复用的状态逻辑代码,通过 prop 将复用的状态传递给被包装组件

// 高阶组件内部创建的类组件:
const withMouse = (BaseComponent) => {
  class Wrapper extends React.Component {
    state = {
      x: 0,
      y: 0,
    };
    render() {
      // const { x, y } = this.state
      return <BaseComponent x={x} y={y} {...wrapper组件状态数据} />;
    }
  }

  return Wrapper;
};

// 参数:需要增强的组件
// 返回值:增强后的组件
const HOCComponent = withMouse(被包装的组件);

示例:

假设 withMouse 高阶组件,可以拿到鼠标位置状态,而 Cat 组件需要使用鼠标位置状态,就可以通过高阶组件来复用鼠标位置相关的状态逻辑代码了:

// withMouse 高阶组件:提供鼠标位置相关的状态逻辑
const CatWithMouse = withMouse(Cat);

// 复用1:

// 使用高阶组件包装后,组件内部就可以通过 props 来获取鼠标位置
const Cat = (props) => {
  // props => { x: 1, y: 1 }
  return (
    <img
      src={catImg}
      style={{
        position: 'absolute',
        top: props.y,
        left: props.x,
      }}
      alt=""
    />
  );
};
// 渲染时,要渲染增强后返回的组件
<CatWithMouse />;

// 复用2:

// 使用高阶组件包装后,组件内部就可以通过 props 来获取鼠标位置
const PositionWithMouse = withMouse(Position);

const Position = ({ x, y }) => {
  return (
    <div>
      鼠标当前位置:(x: {x}, y: {y})
    </div>
  );
};
<PositionWithMouse />;

封装 withMouse 高阶组件

// 创建高阶组件
const withMouse = (BaseComponent) => {
  // 该类组件用来提供状态逻辑
  return class Wrapper extends React.Component {
    state = {
      x: 0,
      y: 0,
    };

    handleMousemove = (e) => {
      const { pageX, pageY } = e;

      this.setState({
        x: pageX,
        y: pageY,
      });
    };

    componentDidMount() {
      window.addEventListener('mousemove', this.handleMousemove);
    }

    componentWillUnmount() {
      window.removeEventListener('mousemove', this.handleMousemove);
    }

    render() {
      const { x, y } = this.state;
      return <BaseComponent x={x} y={y} />;
    }
  };
};

高阶组件的注意点

1.推荐设置 displayName 属性

  • 作用: 用来在 React 浏览器开发者工具(插件)中展示名称
const withMouse = () => {
  class Wrapper ... {}

  // 设置 displayName
  Wrapper.displayName = `WithMouse${getDisplayName(WrappedComponent)}`
  function getDisplayName(WrappedComponent) {
    return WrappedComponent.displayName || WrappedComponent.name || 'Component'
  }

  return Wrapper
}

2.推荐传递 props

  • 如果没有传递,那么,props 会丢失
  • 最终的 props 传递路径: 增强后的组件(CatWithMouse) -> 高阶组件函数中的 Mouse -> 被包装组件(Cat)
const withMouse = (WrappedComponent) => {
  class Mouse ... {
    render() {

      // {...this.props} 就表示将接收到 props 传递给被包装组件
      return <WrappedComponent {...this.props} />
    }
  }
  return Mouse
}

const CatWithMouse = withMouse(Cat)

// 注意: 如果不传递 props,那么,name属性就丢了
<CatWithMouse name="rose" />

4. render-props 模式

参考:React 文档 render-props 模式

基本使用

将要复用的状态逻辑代码封装到一个组件中,通过一个值为函数的 prop 对外暴露数据,实现状态逻辑复用

比如,要通过 render-props 实现鼠标位置的状态逻辑复用:

  1. 创建一个类组件,提供鼠标位置相关的状态逻辑代码
  2. 调用传给该组件的 render 属性,将状态通过参数暴露出去
  3. 通过 render 属性的返回值指定要渲染的结构内容
class Mouse extends React.Component {
  // … 省略state和操作state的方法

  render() {
    return this.props.render(this.state);
  }
}
<Mouse
  render={(mouse) => (
    <p>
      鼠标当前位置 {mouse.x},{mouse.y}
    </p>
  )}
/>

children 代替 render 属性

  • 注意:并不是该模式叫 render props 就必须使用名为 render 的 prop,实际上可以使用任意名称的 prop

  • 把 prop 是一个函数并且告诉组件要渲染什么内容的技术叫做:render props 模式

<Mouse>
  {({ x, y }) => (
    <p>
      鼠标的位置是 {x},{y}
    </p>
  )}
</Mouse>;

// 组件内部:
this.props.children(this.state);

比如,Context 中的 Consumer 组件就是 render-props 的使用模式 

// Context 中的用法:
<Consumer>{(data) => <span>data参数表示接收到的数据 -- {data}</span>}</Consumer>

5. React Hooks 状态逻辑复用

// 创建自定义 hook,实现鼠标位置状态逻辑复用
const useMouse = () => {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const onMouseMove = (e) => {
      const { pageX, pageY } = e;
      setPosition({
        x: pageX,
        y: pageY,
      });
    };

    window.addEventListener('mousemove', onMouseMove);
    return () => window.removeEventListener('mousemove', onMouseMove);
  }, []);

  return position;
};

// 复用:
const Cat = () => {
  const { x, y } = useMouse();
  return (
    <img
      src={catImg}
      style={{
        position: 'absolute',
        top: y,
        left: x,
      }}
      alt=""
    />
  );
};

const Position = () => {
  const { x, y } = useMouse();
  return (
    <div>
      鼠标当前位置:(x: {x}, y: {y})
    </div>
  );
};

6. 为什么要有 Hooks

从以下两个角度来看下 Hooks 出现之前(React v16.8 以前)React 存在的问题

两个角度: 1 组件的状态逻辑复用 2 class 组件自身的问题

  1. 组件的状态逻辑复用:

    • 在 Hooks 之前,组件的状态逻辑复用经历了:mixins(混入)HOCs(高阶组件)render-props 等模式
    • (早已废弃)mixins 的问题:1 数据来源不清晰 2 命名冲突
    • HOCs、render-props 的问题:重构组件结构,导致组件形成 JSX 嵌套地狱问题,这两种模式都不是官方实现。所以,React 急需一种官方的方式,来解决状态逻辑复用导致的其他问题。
  2. class 组件自身的问题:

    • 选择:函数组件和 class 组件之间的区别以及使用哪种组件更合适
    • 需要理解 class 中的 this 是如何工作的
    • 相互关联且需要对照修改的代码被拆分到不同生命周期函数中
    • 相比于函数组件来说,不利于代码压缩和优化,也不利于 TS 的类型推导

正是由于 React 原来存在的这些问题,才有了 Hooks 来解决这些问题。

7. 性能优化

React 自身提供了一些与性能优化相关的 API,按照 class 或 hooks,分为两类:

  1. class
    • PureComponent
    • shouldComponentUpdate
  2. hooks
    • React.memo
    • useMemo
    • useCallback

注:对于性能优化来说,一定要避免过早优化,也就是在没有出现性能问题前,可以不用进行优化。因为只要是优化,就会有成本,所以,优化时一定要确保成本小于优化带来的收获。

React 对性能的理解:只要用户不感觉卡顿,那就是没有性能问题。

8. 优化的方向

React 组件的特性:只要父组件更新,子组件都会无条件的更新

注意,此处的 更新 指的是:子组件中的代码重新执行一遍,进行一次 diff,最终只把变化后的内容更新到浏览器中

但是,某些情况下,子组件是没有必要更新的,比如:

  1. 子组件是一个展示组件,仅仅用来展示一段静态的 JSX 结构
  2. 子组件没有从父组件中接收任何 props
  3. 父组件传递给子组件的 props 引用变了,但是,本质内容没变(比如,父组件给子组件传递了一个回调函数)

此时,就可以通过上述 API 来进行优化。

9. React.memo

React.memo 是一个高阶组件,用来包裹 函数组件 来阻止函数组件不必要的更新

用法:

const Child2 = React.memo(() => {
  console.log('Child2 re-render');
  return <div>Child2</div>;
});

// 通过 React.memo 避免不必要的组件更新
const Child2 = React.memo(
  () => {
    console.log('Child2 re-render');
    return <div>Child2</div>;
  },

  // 如果需要自己来对比 props ,就需要传入第二个参数
  // 如果返回 false,表示更新前后的两次 props 发生了改变,此时,组件会重新渲染
  // 如果返回 true,表示更新前后的两次 props 没有改变,此时,组件不会重新渲染
  // (prevProps, nextProps) => {
  //   console.log(prevProps, nextProps)
  //   return false
  // }
);

说明:React.memo 会对比组件更新前后两次接收到的 props 是否相同,

  • 如果相同,就阻止组件重新渲染
  • 如果不同,才会重新渲染组件

注意:函数组件每次更新时,会重新执行组件中的所有代码,也就是每次都会重新创建该组件中声明的函数、对象等等

浅对比的说明

注意:React.memo 对比 props 的方式是:浅对比也就是之比较值或引用是否相同

比如:

// 对于简单类型来说,比较的是:值是否相同
1 === 1           // true   -> 组件不会重新渲染
true === false    // false  -> 组件会重新渲染

// 对于引用类型来说,比较的是:引用是否相同
[1, 2] === [1, 2] // false  -> 组件会重新渲染
{} === {}         // false  -> 组件会重新渲染

这样,就导致了一个问题:对于引用类型的 props来说,即使更新前后的 prop 值相同,但是引用不同,还是会导致组件重新渲染。

因此,针对于这种 props 是引用类型的情况,需要使用 useCallback 或 useMemo 来处理。

10. useCallback

作用:缓存(记忆)一个函数,该函数只在依赖项变化时才会重新创建新函数(改变)

一般都会配合 React.memo 高阶组件来使用

用法:

const fn = () => {
  console.log('fn 执行了')
}

// 第一个参数:要缓存的函数
// 第二个参数:依赖项,类似于 useEffect 的第二个参数
const memoFn = useCallback(fn, [])

<Child2 fn={memoFn} />

11. useMemo

作用:缓存(记忆)一个对象(非函数),该对象只在依赖项变化时才会改变

一般都会配合 React.memo 高阶组件来使用

用法:

const memoObj = useMemo(() => {
  return {
    name: '豆豆'
  }
}, [])
<Child2 obj={memoObj} />
  • 使用 useMemo 模拟 useCallback 的功能:
// 使用 useMemo 来模拟 useCallback
const memoFn = useMemo(() => {
  return () => {
    console.log('fn 执行了', count)
  }
}, [count])

<Child2 fn={memoFn} />
  • useMemo 的回调函数代码只会在依赖项改变时重新执行,因此,除了缓存对象之外,还可以避免昂贵计算,提升性能
    • 如果一个数据,需要经过昂贵的大量计算得到,此时,也可以使用 useMemo 来缓存数据,避免重复计算提升性能
const memoObj = useMemo(() => {
	// 模拟昂贵计算:
  // 创建长度为 1000 的数组
  const nums = new Array(1000).fill(0).map((item, index) => index)

  return {
    name: '豆豆'
  }
}, [])
<Child2 obj={memoObj} />

12. class 组件优化

对于 class 组件来说,PureComponent 的作用相当于 React.memo;而 shouldComponentUpdate 可以自定义 props 之间的对比规则

  • PureComponent  - 浅对比
  • shouldComponentUpdate 
    • 注意:shouldComponentUpdate 不能与 PureComponent 一起使用 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值