Skip to content

解决横切关注点问题

什么是关注点?

关注点指的是,系统的某个功能模块。

什么是横切关注点?

横切关注点指的是,某个功能模块横跨系统中的大多业务模块,即在大多业务模块中都存在这个功能模块。

「日志模块」就是一个典型案例,因为它通常存在于各个业务模块,即横切所有需要日志模块的业务模块,因此日志模块就成为了横切这些业务模块的关注点,也就是横切关注点。

React 官方推荐解决横切关注点问题的方案有两个:

  • HOC
    • Higher Order Component,即高阶组件,以组件作为参数,返回一个新组件。
  • Render Props

案例

我们通过一个案例来看这两个方案是如何解决横切关注点问题的。

我们要实现两个功能,分别是:

  • 记录鼠标在容器中的位置
  • 实现方块在容器中跟随鼠标移动

效果图如下:

image-20221026142638835

可以看到,这两个功能,都涉及到了鼠标位置,那么获取鼠标位置功能就是横跨这两个模块的关注点。

下面是不使用 HOC 和 Render Props 的情况下,代码的实现逻辑:

jsx
// MouseTracker.js
import React, { Component } from "react";
import "../style.css";

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

  divRef = React.createRef();

  handleMouseMove = (event) => {
    const { left, top } = this.divRef.current.getBoundingClientRect();
    this.setState({
      x: event.clientX - left,
      y: event.clientY - top,
    });
  };

  render() {
    return (
      <div
        ref={this.divRef}
        className="container"
        onMouseMove={this.handleMouseMove}
      >
        <p>
          鼠标相对容器的位置是 ({this.state.x}, {this.state.y})
        </p>
      </div>
    );
  }
}
jsx
// MovableDiv.js
import React, { Component } from "react";
import "../style.css";

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

  divRef = React.createRef();

  handleMouseMove = (event) => {
    const { left, top } = this.divRef.current.getBoundingClientRect();
    this.setState({
      x: event.clientX - left,
      y: event.clientY - top,
    });
  };

  getDivPosition = () => {
    let divX = this.state.x - 50;
    let divY = this.state.y - 50;
    if (divX < 0) {
      divX = 0;
    } else if (divX > 300) {
      divX = 300;
    }
    if (divY < 0) {
      divY = 0;
    } else if (divY > 400) {
      divY = 400;
    }
    return { divX, divY };
  };

  render() {
    const { divX, divY } = this.getDivPosition();
    return (
      <div
        ref={this.divRef}
        className="container"
        onMouseMove={this.handleMouseMove}
      >
        <div
          style={{
            width: "100px",
            height: "100px",
            background: "red",
            position: "absolute",
            left: divX,
            top: divY,
          }}
        ></div>
      </div>
    );
  }
}
css
/* style.css */
.App {
    display: flex;
}

.container {
    position: relative;
    width: 400px;
    height: 500px;
    margin: 10px;
    border: 1px solid;
}

可以看到两者在实现上,有许多代码相同的情况,为了能够复用代码逻辑,我们可以分别通过 HOC 和 Render Props 来实现。

HOC

jsx
// withMouseListener.js
import React, { PureComponent } from "react";
import '../style.css'

export default function withMouseListener(Comp) {
  return class MouseListener extends PureComponent {
    state = {
      x: 0,
      y: 0,
    };

    divRef = React.createRef();

    handleMouseMove = (event) => {
      const { left, top } = this.divRef.current.getBoundingClientRect();
      this.setState({
        x: event.clientX - left,
        y: event.clientY - top,
      });
    };

    render() {
      return (
        <div
          ref={this.divRef}
          className="container"
          onMouseMove={this.handleMouseMove}
        >
          <Comp {...this.props} x={this.state.x} y={this.state.y} />
        </div>
      );
    }
  };
}
jsx
// TestHoc.js
import React, { Component } from "react";
import withMouseListener from "./HOC/withMouseListener";

function MouseTracker(props) {
    return <p>鼠标相对容器的位置是 ({props.x}, {props.y})</p>
}

const getDivPosition = (mouse) => {
    let divX = mouse.x - 50;
    let divY = mouse.y - 50;
    if (divX < 0) {
      divX = 0;
    } else if (divX > 300) {
      divX = 300;
    }
    if (divY < 0) {
      divY = 0;
    } else if (divY > 400) {
      divY = 400;
    }
    return { divX, divY };
};
function MovableDiv(props) {
    const { divX, divY } = getDivPosition(props)
    return <div style={{
        width: "100px",
        height: "100px",
        background: "red",
        position: "absolute",
        left: divX,
        top: divY,
    }}></div>
}

const MouseTrackerWithMouseListener = withMouseListener(MouseTracker)
const MovableDivWithMouseListener = withMouseListener(MovableDiv)

export default class TestHoc extends Component {
    render() {
      return (
        <>
          <MouseTrackerWithMouseListener />
          <MovableDivWithMouseListener />
        </>
      );
    }
}

Render Props

jsx
// MouseListener.js
import React, { Component } from "react";
import "../style.css";

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

  divRef = React.createRef();

  handleMouseMove = (event) => {
    const { left, top } = this.divRef.current.getBoundingClientRect();
    this.setState({
      x: event.clientX - left,
      y: event.clientY - top,
    });
  };

  render() {
    return (
      <div
        ref={this.divRef}
        className="container"
        onMouseMove={this.handleMouseMove}
      >
        {this.props.render ? this.props.render(this.state) : "默认值"}
      </div>
    );
  }
}
jsx
// TestRenderProps.js
import React, { Component } from "react";
import MouseListener from "./RenderProps/MouseListener";

const MouseTracker = (mouse) => (
  <p>
    鼠标相对容器的位置是 ({mouse.x}, {mouse.y})
  </p>
);

const getDivPosition = (mouse) => {
  let divX = mouse.x - 50;
  let divY = mouse.y - 50;
  if (divX < 0) {
    divX = 0;
  } else if (divX > 300) {
    divX = 300;
  }
  if (divY < 0) {
    divY = 0;
  } else if (divY > 400) {
    divY = 400;
  }
  return { divX, divY };
};
const MovableDiv = (mouse) => {
  const { divX, divY } = getDivPosition(mouse);
  return (
    <div
      style={{
        width: "100px",
        height: "100px",
        background: "red",
        position: "absolute",
        left: divX,
        top: divY,
      }}
    ></div>
  );
};

export default class TestRenderProps extends Component {
  render() {
    return (
      <>
        <MouseListener render={MouseTracker} />
        <MouseListener render={MovableDiv} />
      </>
    );
  }
}