Antd-FloatButton组件
何时使用
- 用于网站上的全局功能;
- 无论浏览到何处都可以看见的按钮。
源码学习
1.类型定义
export type FloatButtonType = 'default' | 'primary';
export type FloatButtonShape = 'circle' | 'square';
export interface FloatButtonProps extends React.DOMAttributes<FloatButtonElement> {
prefixCls?: string;
className?: string;
rootClassName?: string;
style?: React.CSSProperties;
icon?: React.ReactNode;
description?: React.ReactNode;
type?: FloatButtonType;
shape?: FloatButtonShape;
tooltip?: TooltipProps['title'];
href?: string;
target?: React.HTMLAttributeAnchorTarget;
badge?: FloatButtonBadgeProps;
['aria-label']?: React.HtmlHTMLAttributes<HTMLElement>['aria-label'];
}
2. 核心代码
let buttonNode = (
<div className={`${prefixCls}-body`}>
<Content {...contentProps} />
</div>
);
if ('badge' in props) {
buttonNode = <Badge {...badgeProps}>{buttonNode}</Badge>;
}
if ('tooltip' in props) {
buttonNode = (
<Tooltip title={tooltip} placement={direction === 'rtl' ? 'right' : 'left'}>
{buttonNode}
</Tooltip>
);
}
return wrapCSSVar(
props.href ? (
<a ref={ref} {...restProps} className={classString}>
{buttonNode}
</a>
) : (
<button ref={ref} {...restProps} className={classString} type="button">
{buttonNode}
</button>
),
);
- a标签的
target属性
<a target="_blank|_self|_parent|_top|framename">
a标签的target 属性的用途是告诉浏览器希望将所链接的资源显示在哪里。默认情况下,浏览器使用的是显示当前文档的窗口、标签页或框架(iframe),所以新文档将会取代现在显示的文档,不过还有其他选择,请看下表:
属性值
| 值 | 描述 |
|---|---|
| _blank | 在新窗口或选项卡中打开链接文档。 |
| _self | 在与点击相同的框架中打开链接的文档(默认)。 |
| _parent | 在父框架中打开链接文档。 |
| _top | 在窗口的整个主体中打开链接的文档。 |
| framename | 在指定的 iframe 中打开链接文档。 |
-
An element with
position: fixed;is positioned relative to the viewport, which means it always stays in the same place even if the page is scrolled. The top, right, bottom, and left properties are used to position the element.An element with
position: absolute;is positioned relative to the nearest positioned ancestor (instead of positioned relative to the viewport, like fixed). However; if an absolute positioned element has no positioned ancestors, it uses the document body, and moves along with page scrolling.Note: Absolute positioned elements are removed from the normal flow, and can overlap elements.
An element with
position: sticky;is positioned based on the user's scroll position.A sticky element toggles betweenrelativeandfixed, depending on the scroll position. It is positioned relative until a given offset position is met in the viewport - then it "sticks" in place (like position:fixed). the sticky element sticks to the top of the page (top: 0), when you reach its scroll position.div.sticky {
position: -webkit-sticky; /* Safari */
position: sticky;
top: 0;
background-color: green;
border: 2px solid #4CAF50;
}
const handleOpenChange = () => {
setOpen((prevState) => {
onOpenChange?.(!prevState);
return !prevState;
});
};
const onClick = useCallback(
(e: MouseEvent) => {
if (floatButtonGroupRef.current?.contains(e.target as Node)) {
if (floatButtonRef.current?.contains(e.target as Node)) {
handleOpenChange();
}
return;
}
setOpen(false);
onOpenChange?.(false);
},
[trigger],
);
useEffect(() => {
if (trigger === 'click') {
document.addEventListener('click', onClick);
return () => {
document.removeEventListener('click', onClick); //监听都要取消
};
}
}, [trigger]);
3.useMergedState
通过该 Hook 你可以自由定义表单控件的受控和非受控状态。 当我们再次传入 defaultValue 和 value 时,由于内部统一作为了组件内部 state 来处理所以自然也不会出现对应的 Warning 警告了
type Updater<T> = (
updater: T | ((origin: T) => T),
ignoreDestroy?: boolean,
) => void;
export default function useMergedState<T, R = T>(
defaultStateValue: T | (() => T), //这个参数表示传入的默认 value 值,当传入参数不存在 option 中的 value 或者 defaultValue 时就会 defaultStateValue 来作为初始值
option?: {
defaultValue?: T | (() => T); //表示接收非受控的初始化默认值,它的优先级高于 defaultStateValue
value?: T; //表示作为受控时的 value props,它的优先级高于 defaultValue 和 defaultStateValue。
onChange?: (value: T, prevValue: T) => void; //当内部值改变后会触发该函数
postState?: (value: T) => T; //表示对于传入值的 format 函数
},
): [R, Updater<T>] {
const { defaultValue, value, onChange, postState } = option || {};
// ======================= Init =======================
const [innerValue, setInnerValue] = useState<T>(() => {
if (hasValue(value)) {
return value;
} else if (hasValue(defaultValue)) {
return typeof defaultValue === 'function'
? (defaultValue as any)()
: defaultValue;
} else {
return typeof defaultStateValue === 'function'
? (defaultStateValue as any)()
: defaultStateValue;
}
});
const mergedValue = value !== undefined ? value : innerValue;
const postMergedValue = postState ? postState(mergedValue) : mergedValue;
// ====================== Change ======================
const onChangeFn = useEvent(onChange);
const [prevValue, setPrevValue] = useState<[T]>([mergedValue]);
//useLayoutUpdateEffect仅在依赖值更新时调用 callback 首次渲染并不执行。
useLayoutUpdateEffect(() => {
const prev = prevValue[0];
if (innerValue !== prev) {
onChangeFn(innerValue, prev);
}
}, [prevValue]);
// Sync value back to `undefined` when it from control to un-control
useLayoutUpdateEffect(() => {
if (!hasValue(value)) {
setInnerValue(value);
}
}, [value]);
// ====================== Update ======================
const triggerChange: Updater<T> = useEvent((updater, ignoreDestroy) => {
setInnerValue(updater, ignoreDestroy); //改变内部的值,如果是非受控(用户没传value),postmergedValue = innerValue,如果是受控,通过onChangeFn修改了prop.value,最终postmergedValue = prop.value
setPrevValue([mergedValue], ignoreDestroy); //传数组[mergedValue]原因是为了强制更新prevValue
});
return [postMergedValue as unknown as R, triggerChange];
}
再回过头看上面的核心代码就知道受控时,为什么内置的click效果没有生效,因为用户传了options.value,那就是受控状态,postMergedValue = value,即使调用了setInnerValue修改了innervalue,也没有传出去,value还是没有改变。
4.FloatButton.BackTop
const BackTop = React.forwardRef<FloatButtonRef, BackTopProps>((props, ref) => {
const {
prefixCls: customizePrefixCls,
className,
type = 'default',
shape = 'circle',
visibilityHeight = 400, //滚动高度达到此参数值才出现 BackTop
icon = <VerticalAlignTopOutlined />,
target, //设置需要监听其滚动事件的元素
onClick,
duration = 450,
...restProps
} = props;
const [visible, setVisible] = useState<boolean>(visibilityHeight === 0);
const internalRef = React.useRef<FloatButtonRef['nativeElement']>(null);
React.useImperativeHandle(ref, () => ({
nativeElement: internalRef.current,
}));
const getDefaultTarget = (): HTMLElement | Document | Window =>
internalRef.current && internalRef.current.ownerDocument
? internalRef.current.ownerDocument //返回节点的所有者文档
: window;
//throttleByAnimationFrame 该函数是通过调用window.requestAnimationFrame来进行节流
const handleScroll = throttleByAnimationFrame(
(e: React.UIEvent<HTMLElement, UIEvent> | { target: any }) => {
const scrollTop = getScroll(e.target, true);
setVisible(scrollTop >= visibilityHeight);
},
);
useEffect(() => {
const getTarget = target || getDefaultTarget;
const container = getTarget();
handleScroll({ target: container });
container?.addEventListener('scroll', handleScroll);
return () => {
handleScroll.cancel();
container?.removeEventListener('scroll', handleScroll); //监听器要取消,且函数要指向同一个
};
}, [target]);
const scrollToTop: React.MouseEventHandler<FloatButtonElement> = (e) => {
scrollTo(0, { getContainer: target || getDefaultTarget, duration });
onClick?.(e);
};
...
return (
<CSSMotion visible={visible} motionName={`${rootPrefixCls}-fade`}>
{({ className: motionClassName }) => (
<FloatButton
ref={internalRef}
{...contentProps}
onClick={scrollToTop}
className={classNames(className, motionClassName)}
/>
)}
</CSSMotion>
);
});
1)scroll事件节流为什么用requestAnimationFrame
- requestAnimationFrame 充分利用显示器的刷新机制,由系统来决定回调函数的执行时机,从而节省系统资源,提高系统性能,改善视觉效果。
- requestAnimationFrame 的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。
- requestAnimationFrame 告诉浏览器希望执行一个动画,让浏览器在下一个动画帧安排一次网页重绘。
- requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。 显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘
export default function scrollTo(y: number, options: ScrollToOptions = {}) {
const { getContainer = () => window, callback, duration = 450 } = options;
const container = getContainer();
const scrollTop = getScroll(container, true);
const startTime = Date.now();
const frameFunc = () => {
const timestamp = Date.now();
const time = timestamp - startTime;
const nextScrollTop = easeInOutCubic(time > duration ? duration : time, scrollTop, y, duration);
if (isWindow(container)) {
//滚动到水平坐标,垂直坐标
(container as Window).scrollTo(window.pageXOffset, nextScrollTop);
} else if (container instanceof Document || container.constructor.name === 'HTMLDocument') {
(container as Document).documentElement.scrollTop = nextScrollTop;
} else {
(container as HTMLElement).scrollTop = nextScrollTop;
}
if (time < duration) {
raf(frameFunc);
} else if (typeof callback === 'function') {
callback();
}
};
raf(frameFunc);
}