Node.js EventEmitter

前情提要

會接觸並使用它是為了完成這個功能...

class CreateServiceFormFieldList extends PureComponent<Props, State> {
  static sharedEmitter = sharedEmitter;

  static ARRANGE_START = 'E/ARRANGE_START';

  static ARRANGE_END = 'E/ARRANGE_END';

  static ARRANGE_HOVERED = 'E/ARRANGE_HOVERED';

  originY: ?number

  constructor(props) {
    super(props);

    this.state = {
      isDragging: false,
      isDragOn: false,
    };

    this.dragElement = React.createRef();

    this.onMouseMoveListener = ({ clientY }) => this.onMouseMove(clientY);
    this.onMouseUpListener = () => this.onMouseUp();
    this.onMouseEnterListener = () => this.onMouseEnter();
    this.onMouseLeaveListener = () => this.onMouseLeave();

    this.onArrangeStartListener = () => {
      const elem = this.dragElement.current.parentNode;

      if (elem) {
        elem.addEventListener('mouseenter', this.onMouseEnterListener);
        elem.addEventListener('mouseleave', this.onMouseLeaveListener);
      }
    };

    this.onArrangeEndListener = () => {
      const elem = this.dragElement.current.parentNode;

      if (elem) {
        elem.removeEventListener('mouseenter', this.onMouseEnterListener);
        elem.removeEventListener('mouseleave', this.onMouseLeaveListener);
      }

      const { isDragOn } = this.state;

      if (isDragOn) {
        this.setState({
          isDragOn: false,
        });
      }
    };

    this.onArrangeHovered = (index) => { this.hoveredRank = index; };
    this.hoveredRank = -1;
  }

  componentDidMount() {
    CreateServiceFormFieldList.sharedEmitter.on(
      CreateServiceFormFieldList.ARRANGE_START,
      this.onArrangeStartListener,
    );

    CreateServiceFormFieldList.sharedEmitter.on(
      CreateServiceFormFieldList.ARRANGE_END,
      this.onArrangeEndListener,
    );
  }

  componentWillUnmount() {
    CreateServiceFormFieldList.sharedEmitter.removeListener(
      CreateServiceFormFieldList.ARRANGE_START,
      this.onArrangeStartListener,
    );

    CreateServiceFormFieldList.sharedEmitter.removeListener(
      CreateServiceFormFieldList.ARRANGE_END,
      this.onArrangeEndListener,
    );
  }

  onMouseEnter() {
    const { index } = this.props;

    CreateServiceFormFieldList.sharedEmitter.emit(
      CreateServiceFormFieldList.ARRANGE_HOVERED,
      index,
    );

    this.setState({
      isDragOn: true,
    });
  }

  onMouseLeave() {
    this.setState({
      isDragOn: false,
    });
  }

  onMouseMove(nowY) {
    const elem = this.dragElement.current;

    if (elem) {
      elem.style.top = `${nowY - this.originY}px`;
    }
  }

  onMouseUp() {
    const elem = this.dragElement.current;

    const {
      index,
      replaceFieldArrayValue,
    } = this.props;

    if (elem) {
      elem.style.top = '0px';
    }

    this.originY = 0;

    window.removeEventListener('mousemove', this.onMouseMoveListener, false);
    window.removeEventListener('mouseup', this.onMouseUpListener, false);

    CreateServiceFormFieldList.sharedEmitter.removeListener(
      CreateServiceFormFieldList.ARRANGE_HOVERED,
      this.onArrangeHovered,
    );

    CreateServiceFormFieldList.sharedEmitter.emit(
      CreateServiceFormFieldList.ARRANGE_END,
    );

    const fieldValues = selector(store.getState(), 'formInfo');

    const draggingValue = fieldValues[index];

    if (~this.hoveredRank) {
      if (fieldValues) {
        const removedList = [
          ...fieldValues.slice(0, index),
          ...fieldValues.slice(index + 1),
        ];

        replaceFieldArrayValue([
          ...removedList.slice(0, this.hoveredRank),
          draggingValue,
          ...removedList.slice(this.hoveredRank),
        ]);
      }
    }

    this.setState({
      isDragging: false,
    });
    this.hoveredRank = -1;
  }

  getHeightValue() {
    const elem = this.dragElement.current;

    if (elem) {
      return elem.clientHeight;
    }

    return null;
  }

  startArrange(originY) {
    this.setState({
      isDragging: true,
    });

    this.originY = originY;

    window.addEventListener('mousemove', this.onMouseMoveListener, false);
    window.addEventListener('mouseup', this.onMouseUpListener, false);

    const elem = this.dragElement.current;

    CreateServiceFormFieldList.sharedEmitter.on(
      CreateServiceFormFieldList.ARRANGE_HOVERED,
      this.onArrangeHovered,
    );

    CreateServiceFormFieldList.sharedEmitter.emit(
      CreateServiceFormFieldList.ARRANGE_START,
      elem,
    );
  }

  render() {
    const {
      field,
      fields,
      index,
    } = this.props;

    const {
      isDragging,
      isDragOn,
    } = this.state;

    return (
      <div
        style={[
          styles.placement,
          isDragging && styles.placementHolded,
          isDragging && {
            height: `${this.getHeightValue()}px`,
          },
        ]}>
        <div
          ref={this.dragElement}
          style={[styles.mainWrapper, isDragging && styles.wrapperDragging]}>
          {createFormItem(field, fields.get(index).type)}
          <div style={styles.subFeatureWrapper}>
            <ItemTypeSelectBlock
              formItem={field} />
            <AdditionFeatureBlock
              formItem={field}
              removeFieldItem={() => fields.remove(index)}
              copyFieldItem={value => fields.insert(index + 1, value)} />
          </div>
          <div style={styles.btnWrapper}>
            <button
              onMouseDown={({ clientY }) => this.startArrange(clientY)}
              style={styles.arrangeBtn}
              type="button" />
          </div>
        </div>
        {isDragOn ? (
          <div style={styles.isDragOnLine} />
        ) : null}
      </div>
    );
  }
}

一臉懵逼

EventEmitter概念邏輯

Emitter instance

EventEmitter

Hub

Emit

'A'

on('A')

on('A')

on('B')

自訂事件觸發

回頭看看dragging feature

class CreateServiceFormFieldList extends PureComponent<Props, State> {
  static sharedEmitter = sharedEmitter;

  static ARRANGE_START = 'E/ARRANGE_START';

  static ARRANGE_END = 'E/ARRANGE_END';

  static ARRANGE_HOVERED = 'E/ARRANGE_HOVERED';

  originY: ?number

  constructor(props) {
    super(props);

    this.state = {
      isDragging: false,
      isDragOn: false,
    };

    this.dragElement = React.createRef();

    this.onMouseMoveListener = ({ clientY }) => this.onMouseMove(clientY);
    this.onMouseUpListener = () => this.onMouseUp();
    this.onMouseEnterListener = () => this.onMouseEnter();
    this.onMouseLeaveListener = () => this.onMouseLeave();

    this.onArrangeStartListener = () => {
      const elem = this.dragElement.current.parentNode;

      if (elem) {
        elem.addEventListener('mouseenter', this.onMouseEnterListener);
        elem.addEventListener('mouseleave', this.onMouseLeaveListener);
      }
    };

    this.onArrangeEndListener = () => {
      const elem = this.dragElement.current.parentNode;

      if (elem) {
        elem.removeEventListener('mouseenter', this.onMouseEnterListener);
        elem.removeEventListener('mouseleave', this.onMouseLeaveListener);
      }

      const { isDragOn } = this.state;

      if (isDragOn) {
        this.setState({
          isDragOn: false,
        });
      }
    };

    this.onArrangeHovered = (index) => { this.hoveredRank = index; };
    this.hoveredRank = -1;
  }

  componentDidMount() {
    CreateServiceFormFieldList.sharedEmitter.on(
      CreateServiceFormFieldList.ARRANGE_START,
      this.onArrangeStartListener,
    );

    CreateServiceFormFieldList.sharedEmitter.on(
      CreateServiceFormFieldList.ARRANGE_END,
      this.onArrangeEndListener,
    );
  }

  componentWillUnmount() {
    CreateServiceFormFieldList.sharedEmitter.removeListener(
      CreateServiceFormFieldList.ARRANGE_START,
      this.onArrangeStartListener,
    );

    CreateServiceFormFieldList.sharedEmitter.removeListener(
      CreateServiceFormFieldList.ARRANGE_END,
      this.onArrangeEndListener,
    );
  }

  onMouseEnter() {
    const { index } = this.props;

    CreateServiceFormFieldList.sharedEmitter.emit(
      CreateServiceFormFieldList.ARRANGE_HOVERED,
      index,
    );

    this.setState({
      isDragOn: true,
    });
  }

  onMouseLeave() {
    this.setState({
      isDragOn: false,
    });
  }

  onMouseMove(nowY) {
    const elem = this.dragElement.current;

    if (elem) {
      elem.style.top = `${nowY - this.originY}px`;
    }
  }

  onMouseUp() {
    const elem = this.dragElement.current;

    const {
      index,
      replaceFieldArrayValue,
    } = this.props;

    if (elem) {
      elem.style.top = '0px';
    }

    this.originY = 0;

    window.removeEventListener('mousemove', this.onMouseMoveListener, false);
    window.removeEventListener('mouseup', this.onMouseUpListener, false);

    CreateServiceFormFieldList.sharedEmitter.removeListener(
      CreateServiceFormFieldList.ARRANGE_HOVERED,
      this.onArrangeHovered,
    );

    CreateServiceFormFieldList.sharedEmitter.emit(
      CreateServiceFormFieldList.ARRANGE_END,
    );

    const fieldValues = selector(store.getState(), 'formInfo');

    const draggingValue = fieldValues[index];

    if (~this.hoveredRank) {
      if (fieldValues) {
        const removedList = [
          ...fieldValues.slice(0, index),
          ...fieldValues.slice(index + 1),
        ];

        replaceFieldArrayValue([
          ...removedList.slice(0, this.hoveredRank),
          draggingValue,
          ...removedList.slice(this.hoveredRank),
        ]);
      }
    }

    this.setState({
      isDragging: false,
    });
    this.hoveredRank = -1;
  }

  getHeightValue() {
    const elem = this.dragElement.current;

    if (elem) {
      return elem.clientHeight;
    }

    return null;
  }

  startArrange(originY) {
    this.setState({
      isDragging: true,
    });

    this.originY = originY;

    window.addEventListener('mousemove', this.onMouseMoveListener, false);
    window.addEventListener('mouseup', this.onMouseUpListener, false);

    const elem = this.dragElement.current;

    CreateServiceFormFieldList.sharedEmitter.on(
      CreateServiceFormFieldList.ARRANGE_HOVERED,
      this.onArrangeHovered,
    );

    CreateServiceFormFieldList.sharedEmitter.emit(
      CreateServiceFormFieldList.ARRANGE_START,
      elem,
    );
  }

  render() {
    const {
      field,
      fields,
      index,
    } = this.props;

    const {
      isDragging,
      isDragOn,
    } = this.state;

    return (
      <div
        style={[
          styles.placement,
          isDragging && styles.placementHolded,
          isDragging && {
            height: `${this.getHeightValue()}px`,
          },
        ]}>
        <div
          ref={this.dragElement}
          style={[styles.mainWrapper, isDragging && styles.wrapperDragging]}>
          {createFormItem(field, fields.get(index).type)}
          <div style={styles.subFeatureWrapper}>
            <ItemTypeSelectBlock
              formItem={field} />
            <AdditionFeatureBlock
              formItem={field}
              removeFieldItem={() => fields.remove(index)}
              copyFieldItem={value => fields.insert(index + 1, value)} />
          </div>
          <div style={styles.btnWrapper}>
            <button
              onMouseDown={({ clientY }) => this.startArrange(clientY)}
              style={styles.arrangeBtn}
              type="button" />
          </div>
        </div>
        {isDragOn ? (
          <div style={styles.isDragOnLine} />
        ) : null}
      </div>
    );
  }
}

前傳

Publish/Subscribe Pattern

情境題

題目:做出複數個元件,並在使用者觸發其中一個時,其餘元件能接收到觸發元件資料

function PanelA({
  panelState,
  setPanelState,
}) {
  useEffect(() => {
    console.log('PanelA notice state changed to panel', panelState);
  }, [panelState]);

  return (
    <button onClick={() => setPanelState('A')}>
      panelA
    </button>
  );
}

function PanelB({
  panelState,
  setPanelState,
}) {
  useEffect(() => {
    console.log('PanelB notice state changed to panel', panelState);
  }, [panelState]);

  return (
    <button onClick={() => setPanelState('B')}>
      panelB
    </button>
  );
}

function PanelC({
  panelState,
  setPanelState,
}) {
  useEffect(() => {
    console.log('PanelC notice state changed to panel', panelState);
  }, [panelState]);

  return (
    <button onClick={() => setPanelState('C')}>
      panelC
    </button>
  );
}

function PanelInterface() {
  const [panelState, setPanelState] = useState(null);
  return (
    <div style={styles.interface}>
      <PanelA
        panelState={panelState}
        setPanelState={setPanelState} />
      <PanelB
        panelState={panelState}
        setPanelState={setPanelState} />
      <PanelC
        panelState={panelState}
        setPanelState={setPanelState} />
    </div>
  );
}

基本款

問題

  • 層級不同造成非必較的props傳遞
  • 依賴關係錯誤建立非必要的耦合性

Flux-redux

Turn into an interface

升級版

const sharedEmitter = new EventEmitter();
sharedEmitter.setMaxListeners(30);

const PANEL_STATE_CHANGED = 'E/PANEL_STATE_CHANGED';
function PanelA() {
  useEffect(() => {
    function stateChangedRecall(panelId) {
      console.log('PanelA noticed state changed to', panelId);
    }

    sharedEmitter.on(PANEL_STATE_CHANGED, stateChangedRecall);

    return () => {
      sharedEmitter.removeListener(PANEL_STATE_CHANGED, stateChangedRecall);
    };
  });

  return (
    <button onClick={() => {
      sharedEmitter.emit(PANEL_STATE_CHANGED, 'PanelA');
    }}>
      PanelA
    </button>
  );
}

// PanelB, PanelC, ... and so on
function PanelInterface() {
  return (
    <div style={styles.interface}>
      <PanelA />
      <PanelB />
      <PanelC />
    </div>
  );
}

升級版

Parent

Instance

ElementA

ElementB

Call state change

Current State

Call state change

Current State

children layer

parent layer

Parent

Instance

ElementA

ElementB

EventEmitter

Trigger event

& send message

children layer

parent layer

Event Manager Interface

event / message filter

Call Subscriber

& get message

再來回頭看看Pub/Sub的定義介紹

針對特定的事件驅動建立抽象介面,藉此達成模組的解耦

中間人

穴大家

deck

By ian Lai

deck

  • 348