0
点赞
收藏
分享

微信扫一扫

resize-observer源码解读

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>;
}
举报

相关推荐

0 条评论