# react component Align 组件分析

[代码仓库](https://github.com/react-component/align)

## 1. 正文

基于上次所分析的 [`dom-align` 源码](https://github.com/yes1am/blog/issues/19),现在看一下该代码如何被封装成`React`组件。

### 1.1 调用方式

```
<Align
  ref={this.alignRef}
  target={this.getTarget}
  monitorWindowResize={this.state.monitor}
  align={this.state.align}
>
  <div
    style={{
      position: 'absolute',
      width: 50,
      height: 50,
      background: 'yellow',
    }}
  />
</Align>
```

其中`this.alignRef`是为了在当前父组件中调用`Align`组件的强制对齐方法,`this.$align.forceAlign();`。

`target`即为`dom-align`中的`target`,可以是一个函数返回DOM节点。

`monitorWindowResize`表示是否监听window的变化。

### 1.2 实现

```
class Align extends Component {

  ...

  static defaultProps = {
    target: () => window,      // 默认target
    monitorBufferTime: 50,     // 防抖时间
    monitorWindowResize: false,
    disabled: false,        // 是否禁止对齐
  };

  componentDidMount() {
    const props = this.props;
    // if parent ref not attached .... use document.getElementById
    this.forceAlign();
    if (!props.disabled && props.monitorWindowResize) {
      this.startMonitorWindowResize();
    }
  }

  componentDidUpdate(prevProps) {
    let reAlign = false;
    const props = this.props;

    // 下面三种情况会发生重新对齐
    // 1.由disabled转为非disabled
    // 2. target改变
    // 3. source元素大小改变 
    if (!props.disabled) {
      const source = ReactDOM.findDOMNode(this);
      const sourceRect = source ? source.getBoundingClientRect() : null;

      if (prevProps.disabled) {
        // 之前是disabled
        reAlign = true;
      } else {
        const lastElement = getElement(prevProps.target);
        const currentElement = getElement(props.target);
        const lastPoint = getPoint(prevProps.target);
        const currentPoint = getPoint(props.target);

        if (isWindow(lastElement) && isWindow(currentElement)) {
          // Skip if is window
          reAlign = false;
        } else if (
          lastElement !== currentElement || // Element change
          (lastElement && !currentElement && currentPoint) || // Change from element to point
          (lastPoint && currentPoint && currentElement) || // Change from point to element
          (currentPoint && !isSamePoint(lastPoint, currentPoint))
        ) {
          reAlign = true;
        }

        // If source element size changed
        const preRect = this.sourceRect || {};
        if (
          !reAlign &&
          source &&
          (!isSimilarValue(preRect.width, sourceRect.width) || !isSimilarValue(preRect.height, sourceRect.height))
        ) {
          reAlign = true;
        }
      }

      this.sourceRect = sourceRect;
    }

    if (reAlign) {
      this.forceAlign();
    }

    if (props.monitorWindowResize && !props.disabled) {
      this.startMonitorWindowResize();
    } else {
      this.stopMonitorWindowResize();
    }
  }

  componentWillUnmount() {
    this.stopMonitorWindowResize();
  }

  startMonitorWindowResize() {
    if (!this.resizeHandler) {   // 防止重复添加监听
      this.bufferMonitor = buffer(this.forceAlign, this.props.monitorBufferTime);
      this.resizeHandler = addEventListener(window, 'resize', this.bufferMonitor);
    }
  }

  stopMonitorWindowResize() {
    if (this.resizeHandler) {
      this.bufferMonitor.clear();
      this.resizeHandler.remove();
      this.resizeHandler = null;
    }
  }

  forceAlign = () => {
    const { disabled, target, align, onAlign } = this.props;
    if (!disabled && target) {
      // 通过 ReactDOM.findDOMNode(this) 获取source
      // this其实最终获取的还是child component的DOM节点,因为render还是返回child
      const source = ReactDOM.findDOMNode(this);

      let result;
      const element = getElement(target);
      const point = getPoint(target);

      // IE lose focus after element realign
      // We should record activeElement and restore later
      const activeElement = document.activeElement;

      if (element) {
        result = alignElement(source, element, align);
      } else if (point) {
        result = alignPoint(source, point, align);
      }

      restoreFocus(activeElement, source);

      if (onAlign) {
        // 如果有onAlign方法，即在`对齐`之后触发
        onAlign(source, result);
      }
    }
  }

  render() {
    const { childrenProps, children } = this.props;
    const child = React.Children.only(children);
    if (childrenProps) {
      const newProps = {};
      const propList = Object.keys(childrenProps);
      propList.forEach((prop) => {
        newProps[prop] = this.props[childrenProps[prop]];
      });

      // 如果有childrenProps，需要通过cloneElement将childrenProps复制到该组件中
      return React.cloneElement(child, newProps);
    }
    return child;
  }
}
```

在初始化时会进行一次对齐，之后在 didUpdate 之后再根据情况对齐

### 1.3 父组件中通过props.children渲染子组件时，如何获取子组件的ref

在刚刚的源码中，是通过`findDomNode`查找到的子元素DOM实例，然而官方文档又说`大多数情况下，你可以绑定一个 ref 到 DOM 节点上，可以完全避免使用 findDOMNode`. 那这种通过`this.props.children`的情况，能不能通过`ref`获取到实例呢？

<https://stackoverflow.com/questions/52209204/is-it-possible-to-get-ref-of-props-children>

父组件：

```
class Parent extends Component {
  componentDidMount () {
    console.log(this.ref)   // 此时的 this.ref 与 findDomNode结果一致
  }
  render () {
    return React.cloneElement(
      this.props.children,
      // 此时的this为父组件，通过传递 ref 函数，使得子组件运行时会将ref绑定到父组件的this上
      { ref: el => { this.ref = el } }
    )
  }
}

调用
<Parent>
  <div>123</div>
</Parent>
```

### 1.4 buffer 与 addEventListener

刚刚源码中有这两个方法,`buffer`用于节流控制，即传统的多次触发则清除上一个定时器的做法，可以参考下:

```javascript
export function buffer(fn, ms) {
  let timer;

  function clear() {
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
  }

  function bufferFn() {
    clear();  // 多次触发删除上次定时器
    timer = setTimeout(fn, ms);
  }

  bufferFn.clear = clear;    // 将clear挂载在返回的对象上
  // this.bufferMonitor = buffer(this.forceAlign, this.props.monitorBufferTime);
  //this.resizeHandler = addEventListener(window, 'resize', this.bufferMonitor);
  // 则可以通过 this.bufferMonitor.clear()清除定时器

  return bufferFn;
}
```

[addEventListener仓库地址](https://github.com/yiminghe/add-dom-event-listener)

该方法屏蔽了`event`事件对象以及给DOM添加事件方法上，各浏览器实现不一致的问题:

> 在 IE6-8 中，事件模型与标准不同。使用非标准的 element.attachEvent() 方法绑定事件监听器。在该模型中，事件对象有一个 srcElement 属性，等价于target 属性。

即这个库即使在IE6-8中也可以使用`e.target`,其内部会处理成`srcElemnt`。

```
function addEventListener (target, eventType, callback, option) {
  function wrapCallback (e) {
    const ne = new EventObject(e);
    callback.call(target, e)
  }

  if (target.addEventListener) {
    let useCapture = false
    if (typeof option === 'object') {
      useCapture = option.capture || false
    } else if (typeof option === 'boolean') {
      useCapture = option
    }

    target.addEventListener(eventType, wrapCallback, option || false)

    return {
      remove () {
        target.removeEventListener(eventType, wrapCallback, useCapture)
      }
    }
  } else if (target.attachEvent) {
    target.attachEvent(`on${eventType}`, wrapCallback)
    return {
      remove () {
        target.detachEvent(`on${eventType}`, wrapCallback)
      }
    }
  }
}
```

另外需要注意的是，该方法通过闭包的方式,返回一个`remove`方法，因此即使`callback`是匿名函数，也是能被移除的。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://yes-1-am.gitbook.io/blog/react-kai-fa-shi-jian/reactcomponentalign-zu-jian-fen-xi.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
