模拟
2024年01月28日
一、认识
二、实现
2.1 index.tsx
import ResizeObserver from './resizeObserver';
export default ResizeObserver;
2.2 resizeObserver.tsx
import { forwardRef } from 'react';
import toArray from './utils/children';
import SingleObserver from './singleObserver';
const INTERNAL_PREFIX_KEY = 'observer-key';
function ResizeObserver(props, ref) {
const { children } = props;
const childNodes =
typeof children === 'function' ? [children] : toArray(children);
return childNodes.map((child, index) => {
const key = child?.key || `${INTERNAL_PREFIX_KEY}-${index}`;
return (
<SingleObserver {...props} key={key} ref={index === 0 ? ref : undefined}>
{child}
</SingleObserver>
);
});
}
const ResizeObserverWrapper = forwardRef(ResizeObserver);
export default ResizeObserverWrapper;
2.3 singleObserver.tsx
import {
useRef,
useEffect,
forwardRef,
useCallback,
cloneElement,
isValidElement,
useImperativeHandle
} from 'react';
import findDOMNode from './utils/dom';
import { supportRef } from './utils/ref';
import { useComposeRef } from './hooks/useComposeRef';
import { observe, unobserve } from './utils/observerUtil';
function SingleObserver(props, ref) {
const { children, disabled } = props;
const sizeRef = useRef({
width: -1,
height: -1,
offsetWidth: -1,
offsetHeight: -1
});
const elementRef = useRef(null);
const wrapperRef = useRef(null);
const isRenderProps = typeof children === 'function';
const mergedChildren = isRenderProps ? children(elementRef) : children;
const canRef =
!isRenderProps &&
isValidElement(mergedChildren) &&
supportRef(mergedChildren);
const originRef = canRef ? mergedChildren.ref : null;
const mergedRef = useComposeRef(originRef, elementRef);
const propsRef = useRef(props);
propsRef.current = props;
const getDom = () =>
findDOMNode(elementRef.current) ||
(elementRef.current && typeof elementRef.current === 'object'
? findDOMNode(elementRef.current?.nativeElement)
: null) ||
findDOMNode(wrapperRef.current);
const onInternalResize = useCallback(target => {
const { onResize } = propsRef.current;
const { width, height } = target.getBoundingClientRect();
const { offsetWidth, offsetHeight } = target;
const fixedWidth = Math.floor(width);
const fixedHeight = Math.floor(height);
if (
sizeRef.current.width !== fixedWidth ||
sizeRef.current.height !== fixedHeight ||
sizeRef.current.offsetWidth !== offsetWidth ||
sizeRef.current.offsetHeight !== offsetHeight
) {
const size = {
width: fixedWidth,
height: fixedHeight,
offsetWidth,
offsetHeight
};
sizeRef.current = size;
const mergedOffsetWidth =
offsetWidth === Math.round(width) ? width : offsetWidth;
const mergedOffsetHeight =
offsetHeight === Math.round(height) ? height : offsetHeight;
const sizeInfo = {
...size,
offsetWidth: mergedOffsetWidth,
offsetHeight: mergedOffsetHeight
};
if (onResize) {
Promise.resolve().then(() => {
onResize(sizeInfo, target);
});
}
}
}, []);
useImperativeHandle(ref, () => getDom());
useEffect(() => {
const currentElement = getDom();
if (currentElement && !disabled) {
observe(currentElement, onInternalResize);
}
return () => unobserve(currentElement, onInternalResize);
}, [elementRef.current, disabled]);
return (
<>
{canRef
? cloneElement(mergedChildren, {
ref: mergedRef
})
: mergedChildren}
</>
);
}
const SingleObserverWrapper = forwardRef(SingleObserver);
export default SingleObserverWrapper;
2.4 /hooks/useComposeRef.tsx
import { useMemo } from 'react';
export function fillRef(ref, node) {
if (typeof ref === 'function') {
ref(node);
} else if (typeof ref === 'object' && ref && 'current' in ref) {
ref.current = node;
}
}
export function composeRef(...refs) {
const refList = refs.filter(ref => ref);
if (refList.length <= 1) {
return refList[0];
}
return node => {
refs.forEach(ref => {
fillRef(ref, node);
});
};
}
export function useComposeRef(...refs) {
return useMemo(
() => composeRef(...refs),
refs,
(prev, next) =>
prev.length !== next.length || prev.every((ref, i) => ref !== next[i])
);
}
2.5 /utils/children.ts
import { Children } from 'react';
import { isFragment } from 'react-is';
export default function toArray(children, option) {
let ret = [];
Children.forEach(children, (child: any | any[]) => {
if ((child === undefined || child === null) && !option.keepEmpty) {
return;
}
if (Array.isArray(child)) {
ret = ret.concat(toArray(child));
} else if (isFragment(child) && child.props) {
ret = ret.concat(toArray(child.props.children, option));
} else {
ret.push(child);
}
});
return ret;
}
2.6 /utils/dom.tsx
import React from 'react';
import ReactDOM from 'react-dom';
export function isDOM(node: any) {
return node instanceof HTMLElement || node instanceof SVGElement;
}
export default function findDOMNode(node) {
if (isDOM(node)) {
return node;
}
if (node instanceof React.Component) {
return ReactDOM.findDOMNode(node);
}
return null;
}
2.7 /utils/observerUtil.ts
import ResizeObserver from 'resize-observer-polyfill';
const elementListeners = new Map();
function onResize(entities) {
entities.forEach(entity => {
const { target } = entity;
elementListeners.get(target)?.forEach(listener => listener(target));
});
}
const resizeObserver = new ResizeObserver(onResize);
export function observe(element, callback) {
if (!elementListeners.has(element)) {
elementListeners.set(element, new Set());
resizeObserver.observe(element);
}
elementListeners.get(element).add(callback);
}
export function unobserve(element, callback) {
if (elementListeners.has(element)) {
elementListeners.get(element).delete(callback);
if (!elementListeners.get(element).size) {
resizeObserver.unobserve(element);
elementListeners.delete(element);
}
}
}
2.8 /utils/ref.tsx
import { isMemo } from 'react-is';
export function supportRef(nodeOrComponent) {
const type = isMemo(nodeOrComponent)
? nodeOrComponent.type.type
: nodeOrComponent.type;
if (typeof type === 'function' && !type.prototype?.render) {
return false;
}
if (
typeof nodeOrComponent === 'function' &&
!nodeOrComponent.prototype?.render
) {
return false;
}
return true;
}
三、测试
import React from 'react';
import ResizeObserver from './resizeObserver';
export default function App() {
const onResize = ({
width,
height,
offsetHeight,
offsetWidth,
}) => {
console.log(
'Resize:',
'\n',
'BoundingBox',
width,
height,
'\n',
'Offset',
offsetWidth,
offsetHeight,
);
};
return (
<ResizeObserver onResize={onResize}>
<textarea placeholder="I'm a textarea!" />
</ResizeObserver>
);
}