resize-observer
github 地址:https://github.com/devrelm/resize-observer
本地启动
npm installnpm start
node 18.16.0 (npm 9.5.1) 启动失败报错
node:internal/crypto/hash:71this[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获取要被侦听的elementconst 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是回调函数onInternalResizeelementListeners.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回调函数即onInternalResizeelementListeners.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);// 移除目标elementelementListeners.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 happenedonCollectionResize?.(sizeInfo, target, data);if (onResize) {// defer the callback but not defer to next framePromise.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 SVGElementreturn 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 existonCollectionResize?.(size, element, data);},[onBatchResize, onCollectionResize],);return <CollectionContext.Provider value={onResize}>{children}</CollectionContext.Provider>;
}