react component Align 组件分析
1. 正文
基于上次所分析的 dom-align
源码,现在看一下该代码如何被封装成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
用于节流控制,即传统的多次触发则清除上一个定时器的做法,可以参考下:
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;
}
该方法屏蔽了event
事件对象以及给DOM添加事件方法上,各浏览器实现不一致的问题:
在 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
是匿名函数,也是能被移除的。
Last updated