前言
想要学习react的优雅写法,所以翻出antd的源码来读。记录一下button的源码解读
组件库涉及的通用库:
- classNames
github.com/JedWatson/c…
条件判断输出className的值 - omit
github.com/benjycui/om…
移出对象的指定属性,而实现浅拷贝
源码分析
// 这里是概要
const InternalButton = (props, ref) => {
// 从props中解析属性值
const {xx, xxx, ...} = props;
// size和config通过createontext的方式来传递
const size = React.useContext(SizeContext);
const { xx, xxx } = React.useContext(ConfigContext);
// 设置过渡态和是否要在俩中文字插入空格的标志位
const [innerLoading, setLoading] = React.useState<Loading>(!!loading);
const [hasTwoCNChar, setHasTwoCNChar] = React.useState(false);
// 设置button的ref
const buttonRef = (ref as any) || React.createRef<HTMLElement>();
// 设置保存延迟倒计时需求的定时器,通过useRef保存,可以“跨渲染周期”保存数据
// 这样即使重新渲染也能保存定时器,后续方便清理
// mark(这个是楼主以前没有用到过的技巧)
const delayTimeoutRef = React.useRef<number>();
// 是否需要在字间插入空格
const isNeedInserted = () => {}
// 确认按钮内容是否是俩中文字符
const fixTwoCNChar = () => {}
// 确认是否设置loading或延迟标志位
let loadingOrDelay = xxx
// 观测loadingOrDelay设置定时器
// 观测buttonRef嗲调用fixTwoCNChar
React.useEffect(() => {})
// 监听点击事件
const handleClick = () => {}
// 利用classNames获取样式名称
// 获取图标节点和btn里面放的内容
const iconNode =
const kids =
// 定义button,将样式和事件附加
const = buttonNode
// 条件决定是否又wave组件包裹
}
// 这里是源码详解
const InternalButton: React.ForwardRefRenderFunction<unknown, ButtonProps> = (props, ref) => {
const {
loading = false,
prefixCls: customizePrefixCls,
type,
danger,
shape,
size: customizeSize,
className,
children,
icon,
ghost = false,
block = false,
/** If we extract items here, we don't need use omit.js */
// React does not recognize the `htmlType` prop on a DOM element. Here we pick it out of `rest`.
htmlType = 'button' as ButtonProps['htmlType'],
...rest
} = props;
const size = React.useContext(SizeContext);
// 用于控制异步返回函数的loading态
const [innerLoading, setLoading] = React.useState<Loading>(!!loading);
// 是否是两个中文字符,如果是的话,自动填充空格在两个字之间
const [hasTwoCNChar, setHasTwoCNChar] = React.useState(false);
const { getPrefixCls, autoInsertSpaceInButton, direction } = React.useContext(ConfigContext);
const buttonRef = (ref as any) || React.createRef<HTMLElement>();
// 计时器的ref
const delayTimeoutRef = React.useRef<number>();
const isNeedInserted = () =>
React.Children.count(children) === 1 && !icon && !isUnborderedButtonType(type);
// 需要插入并且是两个中文字,标志位还未被置位时更新标志位
const fixTwoCNChar = () => {
// Fix for HOC usage like <FormatMessage />
if (!buttonRef || !buttonRef.current || autoInsertSpaceInButton === false) {
return;
}
const buttonText = buttonRef.current.textContent;
if (isNeedInserted() && isTwoCNChar(buttonText)) {
if (!hasTwoCNChar) {
setHasTwoCNChar(true);
}
} else if (hasTwoCNChar) {
setHasTwoCNChar(false);
}
};
// =============== Update Loading ===============
// 如果loading是对象又设定了延迟标志,就确定设置标志位
let loadingOrDelay: Loading;
if (typeof loading === 'object' && loading.delay) {
loadingOrDelay = loading.delay || true;
} else {
loadingOrDelay = !!loading;
}
// 更新loading态。如果有设置延迟就设置定时器,并保存在ref中便于清理
React.useEffect(() => {
clearTimeout(delayTimeoutRef.current);
if (typeof loadingOrDelay === 'number') {
delayTimeoutRef.current = window.setTimeout(() => {
setLoading(loadingOrDelay);
}, loadingOrDelay);
} else {
setLoading(loadingOrDelay);
}
}, [loadingOrDelay]);
// 当按钮发生变化时候
React.useEffect(fixTwoCNChar, [buttonRef]);
// 按钮点击时只有非loading非禁止才调用函数
const handleClick = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>) => {
const { onClick, disabled } = props;
// https://github.com/ant-design/ant-design/issues/30207
if (innerLoading || disabled) {
e.preventDefault();
return;
}
(onClick as React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>)?.(e);
};
devWarning(
!(typeof icon === 'string' && icon.length > 2),
'Button',
`\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`,
);
devWarning(
!(ghost && isUnborderedButtonType(type)),
'Button',
"`link` or `text` button can't be a `ghost` button.",
);
const prefixCls = getPrefixCls('btn', customizePrefixCls);
const autoInsertSpace = autoInsertSpaceInButton !== false;
// large => lg
// small => sm
// 根据size得到对应的class描述
let sizeCls = '';
switch (customizeSize || size) {
case 'large':
sizeCls = 'lg';
break;
case 'small':
sizeCls = 'sm';
break;
default:
break;
}
const iconType = innerLoading ? 'loading' : icon;
const classes = classNames(
prefixCls,
{
[`${prefixCls}-${type}`]: type,
[`${prefixCls}-${shape}`]: shape,
[`${prefixCls}-${sizeCls}`]: sizeCls,
[`${prefixCls}-icon-only`]: !children && children !== 0 && !!iconType,
[`${prefixCls}-background-ghost`]: ghost && !isUnborderedButtonType(type),
[`${prefixCls}-loading`]: innerLoading,
[`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && autoInsertSpace,
[`${prefixCls}-block`]: block,
[`${prefixCls}-dangerous`]: !!danger,
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,
);
const iconNode =
icon && !innerLoading ? (
icon
) : (
<LoadingIcon existIcon={!!icon} prefixCls={prefixCls} loading={!!innerLoading} />
);
const kids =
children || children === 0
? spaceChildren(children, isNeedInserted() && autoInsertSpace)
: null;
// moit移出对象的指定属性,而实现浅拷贝
const linkButtonRestProps = omit(rest as AnchorButtonProps & { navigate: any }, ['navigate']);
if (linkButtonRestProps.href !== undefined) {
return (
<a {...linkButtonRestProps} className={classes} onClick={handleClick} ref={buttonRef}>
{iconNode}
{kids}
</a>
);
}
const buttonNode = (
<button
{...(rest as NativeButtonProps)}
type={htmlType}
className={classes}
onClick={handleClick}
ref={buttonRef}
>
{iconNode}
{kids}
</button>
);
if (isUnborderedButtonType(type)) {
return buttonNode;
}
return <Wave>{buttonNode}</Wave>;
};
const Button = React.forwardRef<unknown, ButtonProps>(InternalButton) as CompoundedComponent;
Button.displayName = 'Button';
Button.Group = Group;
Button.__ANT_BUTTON = true;
分析详解最后提到的wave组件,其实就是给传入的组件添加动画效果的一个包裹组件
关于颜色样式的less写法 因为这里的按钮有几种基本样式基于色调在变,所以特地试了下less的一些函数写法。 包括each生成一组颜色;lighten,darken生成相近颜色;contrast生成和背景差异大的字颜色
@prefixcls: shell;
@primary-color: #001F97;
@error-color: #680506;
@success-color: #033017;
.get-color(@key-name) {
each(range(4), {
.@{prefixcls}-@{key-name}-@{key} {
color: contrast(@@key-name);
background: lighten(@@key-name, @value * 8%);
&.active, &:active {
background: lighten(@@key-name, @value * 12%);
}
&.hover, &:hover {
background: darken(@@key-name, @value * 4%);
}
background-color: @@key-name;
}
})
}
.get-color(primary-color);
.get-color(error-color);
.get-color(success-color);
生成css如下
.shell-primary-color-1 {
color: #ffffff;
background: #0027c0;
background-color: #001F97;
}
.shell-primary-color-1.active,
.shell-primary-color-1:active {
background: #002cd4;
}
.shell-primary-color-1.hover,
.shell-primary-color-1:hover {
background: #001b83;
}
.shell-primary-color-2 {
color: #ffffff;
background: #0030e9;
background-color: #001F97;
}
.shell-primary-color-2.active,
.shell-primary-color-2:active {
background: #1243ff;
}
.shell-primary-color-2.hover,
.shell-primary-color-2:hover {
background: #00176e;
}
.shell-primary-color-3 {
color: #ffffff;
background: #1243ff;
background-color: #001F97;
}
.shell-primary-color-3.active,
.shell-primary-color-3:active {
background: #5074ff;
}
.shell-primary-color-3.hover,
.shell-primary-color-3:hover {
background: #00125a;
}
.shell-primary-color-4 {
color: #ffffff;
background: #3b63ff;
background-color: #001F97;
}
.shell-primary-color-4.active,
.shell-primary-color-4:active {
background: #8da4ff;
}
.shell-primary-color-4.hover,
.shell-primary-color-4:hover {
background: #000e45;
}
.shell-error-color-1 {
color: #ffffff;
background: #8f0708;
background-color: #680506;
}
.shell-error-color-1.active,
.shell-error-color-1:active {
background: #a20809;
}
.shell-error-color-1.hover,
.shell-error-color-1:hover {
background: #550405;
}
.shell-error-color-2 {
color: #ffffff;
background: #b6090a;
background-color: #680506;
}
.shell-error-color-2.active,
.shell-error-color-2:active {
background: #dd0b0d;
}
.shell-error-color-2.hover,
.shell-error-color-2:hover {
background: #410304;
}
.shell-error-color-3 {
color: #ffffff;
background: #dd0b0d;
background-color: #680506;
}
.shell-error-color-3.active,
.shell-error-color-3:active {
background: #f53032;
}
.shell-error-color-3.hover,
.shell-error-color-3:hover {
background: #2e0203;
}
.shell-error-color-4 {
color: #ffffff;
background: #f41c1e;
background-color: #680506;
}
.shell-error-color-4.active,
.shell-error-color-4:active {
background: #f86a6b;
}
.shell-error-color-4.hover,
.shell-error-color-4:hover {
background: #1a0102;
}
.shell-success-color-1 {
color: #ffffff;
background: #055629;
background-color: #033017;
}
.shell-success-color-1.active,
.shell-success-color-1:active {
background: #076a33;
}
.shell-success-color-1.hover,
.shell-success-color-1:hover {
background: #021d0e;
}
.shell-success-color-2 {
color: #ffffff;
background: #087d3c;
background-color: #033017;
}
.shell-success-color-2.active,
.shell-success-color-2:active {
background: #0aa34e;
}
.shell-success-color-2.hover,
.shell-success-color-2:hover {
background: #010a05;
}
.shell-success-color-3 {
color: #ffffff;
background: #0aa34e;
background-color: #033017;
}
.shell-success-color-3.active,
.shell-success-color-3:active {
background: #0edd6a;
}
.shell-success-color-3.hover,
.shell-success-color-3:hover {
background: #000000;
}
.shell-success-color-4 {
color: #ffffff;
background: #0dca61;
background-color: #033017;
}
.shell-success-color-4.active,
.shell-success-color-4:active {
background: #35f289;
}
.shell-success-color-4.hover,
.shell-success-color-4:hover {
background: #000000;
}