resize-observer
github 地址:https://github.com/devrelm/resize-observer
本地启动
npm install
npm start
node 18.16.0 (npm 9.5.1) 启动失败报错
node:internal/crypto/hash:71
this[kHandle] = new _Hash(algorithm, xofLen);
^
Error: error:0308010C:digital envelope routines::unsupported
解决:更改 node 版本
node 16.16.0 (npm 8.11.0) 启动成功
使用示例
const onResize: ResizeObserverProps["onResize"] = ({
width,
height,
offsetHeight,
offsetWidth,
}) => {
setTimes((prevTimes) => prevTimes + 1);
console.log(
"Resize:",
"\n",
"BoundingBox",
width,
height,
"\n",
"Offset",
offsetWidth,
offsetHeight
);
};
<ResizeObserver onResize={onResize} disabled={disabled}>
<Wrapper>
<textarea ref={textareaRef} placeholder="I'm a textarea!" />
</Wrapper>
</ResizeObserver>;
export type OnResize = (size: SizeInfo, element: HTMLElement) => void;
export interface ResizeObserverProps {
/** Pass to ResizeObserver.Collection with additional data */
data?: any;
children:
| React.ReactNode
| ((ref: React.RefObject<any>) => React.ReactElement);
disabled?: boolean;
/** Trigger if element resized. Will always trigger when first time render. */
onResize?: OnResize;
}
ResizeObserve 組件
真正组件在ResizeObserver组件
const RefResizeObserver = React.forwardRef(ResizeObserver) as React.ForwardRefExoticComponent<
React.PropsWithoutRef<ResizeObserverProps> & React.RefAttributes<any>
> & {
Collection: typeof Collection;
};
RefResizeObserver.Collection = Collection;
export default RefResizeObserver;
ResizeObserver
里面还有一层组件SingleObserver
//src\index.tsx
function ResizeObserver(props: ResizeObserverProps, ref: React.Ref<HTMLElement>) {
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>
);
}) as any as React.ReactElement;
}
SingleObserver 組件
真正实现观察的方法在这个组件
const RefSingleObserver = React.forwardRef(SingleObserver);
//src\SingleObserver\index.tsx
function SingleObserver(
props: SingleObserverProps,
ref: React.Ref<HTMLElement>
) {
return (
<DomWrapper ref={wrapperRef}>
{canRef
? React.cloneElement(mergedChildren as any, {
ref: mergedRef,
})
: mergedChildren}
</DomWrapper>
);
}
实现元素变化逻辑
监听 elementRef.current 的變化
在 SingleObserver 组件
import { observe, unobserve } from "../utils/observerUtil";
// Dynamic observe
React.useEffect(() => {
// getDom获取要被侦听的element
const currentElement: HTMLElement = getDom();
if (currentElement && !disabled) {
// 执行侦听
observe(currentElement, onInternalResize);
}
// 清除侦听
return () => unobserve(currentElement, onInternalResize);
}, [elementRef.current, disabled]);
创建侦听器实例
// src\utils\observerUtil.ts
const elementListeners = new Map<Element, Set<ResizeListener>>();
import ResizeObserver from 'resize-observer-polyfill';
// interface ResizeObserverEntry {
// readonly target: Element;
// readonly contentRect: DOMRectReadOnly;
// }
// onResize 创建侦听器传入的callback
function onResize(entities: ResizeObserverEntry[]) {
entities.forEach((entity) => {
const { target } = entity;
// elementListeners.get(target)是set集合 ,listener是回调函数onInternalResize
elementListeners.get(target)?.forEach((listener) => listener(target));
});
}
// Note: ResizeObserver polyfill not support option to measure border-box resize
const resizeObserver = new ResizeObserver(onResize);
// resize-observer-polyfill中ResizeObserverSPI类
const observer = new ResizeObserverSPI(callback, controller, this);
ResizeObserverSPI
类的broadcastActive
方法
callback
返回的信息,entries
是一个数组,返回所有正在活跃的目标element列表
// Create ResizeObserverEntry instance for every active observation.
const entries = this.activeObservations_.map((observation) => {
// 返回被觀察element最新的大小
return new ResizeObserverEntry(
observation.target,
// 執行observation.broadcastRect函數獲取最新的大小
observation.broadcastRect()
);
});
// 改变回调函数的this指向ctx
this.callback_.call(ctx, entries, ctx);
observe 函数
const elementListeners = new Map<Element, Set<ResizeListener>>();
function observe(element: Element, callback: ResizeListener) {
if (!elementListeners.has(element)) {
// 给elementListeners添加一个键值对
elementListeners.set(element, new Set());
//
resizeObserver.observe(element);
}
// elementListeners.get(element) 是set结构,给set插入一个新元素callback回调函数即onInternalResize
elementListeners.get(element).add(callback);
}
unobserve 函数
const elementListeners = new Map<Element, Set<ResizeListener>>();
// 取消侦听
function unobserve(element: Element, callback: ResizeListener) {
if (elementListeners.has(element)) {
//set集合移除callback回调函数
elementListeners.get(element).delete(callback);
if (!elementListeners.get(element).size) {
// 取消侦听
resizeObserver.unobserve(element);
// 移除目标element
elementListeners.delete(element);
}
}
}
onInternalResize 函数
CollectionContext = React.createContext<onCollectionResize>(null);
const onCollectionResize = React.useContext(CollectionContext);
const propsRef = React.useRef < SingleObserverProps > props;
propsRef.current = props;
// Handler
const onInternalResize = React.useCallback((target: HTMLElement) => {
const { onResize, data } = propsRef.current;
// getBoundingClientRect侦听器内部实现的一个方法,获取元素尺寸大小
const { width, height } = target.getBoundingClientRect();
const { offsetWidth, offsetHeight } = target;
/**
* Resize observer trigger when content size changed.
* In most case we just care about element size,
* let's use `boundary` instead of `contentRect` here to avoid shaking.
*/
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;
// IE is strange, right?
const mergedOffsetWidth =
offsetWidth === Math.round(width) ? width : offsetWidth;
const mergedOffsetHeight =
offsetHeight === Math.round(height) ? height : offsetHeight;
const sizeInfo = {
...size,
offsetWidth: mergedOffsetWidth,
offsetHeight: mergedOffsetHeight,
};
// Let collection know what happened
onCollectionResize?.(sizeInfo, target, data);
if (onResize) {
// defer the callback but not defer to next frame
Promise.resolve().then(() => {
// 给父组件传递信息
onResize(sizeInfo, target);
});
}
}
}, []);
getDom 函數
const getDom = () =>
findDOMNode<HTMLElement>(elementRef.current) ||
// Support `nativeElement` format
(elementRef.current && typeof elementRef.current === 'object'
? findDOMNode<HTMLElement>((elementRef.current as any)?.nativeElement)
: null) ||
findDOMNode<HTMLElement>(wrapperRef.current);
findDOMNode函數
github:https://github.com/react-component/util/blob/master/src/Dom/findDOMNode.ts
/**
* Return if a node is a DOM node. Else will return by `findDOMNode`
*/
function findDOMNode<T = Element | Text>(
node: React.ReactInstance | HTMLElement | SVGElement,
): T {
if (isDOM(node)) {
return (node as unknown) as T;
}
if (node instanceof React.Component) {
return (ReactDOM.findDOMNode(node) as unknown) as T;
}
return null;
}
function isDOM(node: any): node is HTMLElement | SVGElement {
// https://developer.mozilla.org/en-US/docs/Web/API/Element
// Since XULElement is also subclass of Element, we only need HTMLElement and SVGElement
return node instanceof HTMLElement || node instanceof SVGElement;
}
Collection组件
function Collection({ children, onBatchResize }: CollectionProps) {
const resizeIdRef = React.useRef(0);
const resizeInfosRef = React.useRef<ResizeInfo[]>([]);
const onCollectionResize = React.useContext(CollectionContext);
const onResize = React.useCallback<onCollectionResize>(
(size, element, data) => {
resizeIdRef.current += 1;
const currentId = resizeIdRef.current;
resizeInfosRef.current.push({
size,
element,
data,
});
Promise.resolve().then(() => {
if (currentId === resizeIdRef.current) {
onBatchResize?.(resizeInfosRef.current);
resizeInfosRef.current = [];
}
});
// Continue bubbling if parent exist
onCollectionResize?.(size, element, data);
},
[onBatchResize, onCollectionResize],
);
return <CollectionContext.Provider value={onResize}>{children}</CollectionContext.Provider>;
}