跳到主要内容

模拟

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