跳到主要内容

模拟

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>
);
}