模拟
2024年01月28日
一、认识
二、实现
2.1 /hooks/useSize.tsx
import useRafState from './useRafState';
import ResizeObserver from 'resize-observer-polyfill';
import useLayoutEffectWithTarget from './useLayoutEffectWithTarget';
function getTargetElement(target, defaultElement) {
if (!target) {
return defaultElement;
}
let targetElement;
if (typeof target === 'function') {
targetElement = target();
} else if ('current' in target) {
targetElement = target.current;
} else {
targetElement = target;
}
return targetElement;
}
function useSize(target) {
const [state, setState] = useRafState(() => {
const el = getTargetElement(target);
return el ? { width: el.clientWidth, height: el.clientHeight } : undefined;
});
useLayoutEffectWithTarget(
() => {
const el = getTargetElement(target);
if (!el) {
return;
}
const resizeObserver = new ResizeObserver(entries => {
entries.forEach(entry => {
const { clientWidth, clientHeight } = entry.target;
setState({ width: clientWidth, height: clientHeight });
});
});
resizeObserver.observe(el);
return () => {
resizeObserver.disconnect();
};
},
[],
target
);
return state;
}
export default useSize;
2.2 /hooks/useRafState.tsx
import { useCallback, useRef, useState } from 'react';
import useUnmount from './useUnmount';
function useRafState(initialState) {
const ref = useRef(0);
const [state, setState] = useState(initialState);
const setRafState = useCallback(value => {
cancelAnimationFrame(ref.current);
ref.current = requestAnimationFrame(() => {
setState(value);
});
}, []);
useUnmount(() => {
cancelAnimationFrame(ref.current);
});
return [state, setRafState];
}
export default useRafState;
2.3 /hooks/useUnmount.tsx
import { useEffect } from 'react';
import useLatest from './useLatest';
function useUnmount(fn) {
const fnRef = useLatest(fn);
useEffect(
() => () => {
fnRef.current();
},
[]
);
}
export default useUnmount;
2.4 /hooks/useLatest.tsx
import { useRef } from 'react';
function useLatest(value) {
const ref = useRef(value);
ref.current = value;
return ref;
}
export default useLatest;
2.5 /hooks/useLayoutEffectWithTarget.tsx
import useUnmount from './useUnmount';
import { useLayoutEffect, useRef } from 'react';
function depsAreSame(oldDeps, deps) {
if (oldDeps === deps) return true;
for (let i = 0; i < oldDeps.length; i++) {
if (!Object.is(oldDeps[i], deps[i])) return false;
}
return true;
}
function getTargetElement(target, defaultElement) {
if (!target) {
return defaultElement;
}
let targetElement;
if (typeof target === 'function') {
targetElement = target();
} else if ('current' in target) {
targetElement = target.current;
} else {
targetElement = target;
}
return targetElement;
}
function useLayoutEffectWithTarget(effect, deps, target) {
const unLoadRef = useRef();
const lastDepsRef = useRef([]);
const hasInitRef = useRef(false);
const lastElementRef = useRef([]);
useLayoutEffect(() => {
const targets = Array.isArray(target) ? target : [target];
const els = targets.map(item => getTargetElement(item));
if (!hasInitRef.current) {
hasInitRef.current = true;
lastElementRef.current = els;
lastDepsRef.current = deps;
unLoadRef.current = effect();
return;
}
if (
els.length !== lastElementRef.current.length ||
!depsAreSame(els, lastElementRef.current) ||
!depsAreSame(deps, lastDepsRef.current)
) {
unLoadRef.current?.();
lastElementRef.current = els;
lastDepsRef.current = deps;
unLoadRef.current = effect();
}
});
useUnmount(() => {
unLoadRef.current?.();
hasInitRef.current = false;
});
}
export default useLayoutEffectWithTarget;
三、测试
import useSize from './hooks/useSize';
import React, { useRef } from 'react';
export default () => {
const ref = useRef(null);
const size = useSize(ref);
return (
<div ref={ref}>
<p>Try to resize the preview window </p>
<p>
width: {size?.width}px, height: {size?.height}px
</p>
</div>
);
};