Ant Design 源码分析(一): 按钮
在电影里,会有这样的桥段,某人捡了一个遥控器,按下了按钮,一个妹子莫名倒地
按钮可以说是网页上最常见的元素之一了,简单的按钮,可能就是一个<button/>
标签,再加上一些样式与事件。而复杂的,则会对按钮的常用功能进行各种封装,以应对各种业务场景。Ant Design 中,仅仅是一个按钮组件,就能管中窥豹,可见一斑。
按钮的 loading 处理
Ant Design 的按钮有loading
属性,他的定义如下
export interface BaseButtonProps {
// 此处省略其它props
loading?: boolean | { delay?: number };
}
当loading
的类型为boolean
时,组件的 loading 状态受控。当loading
为{ delay?: number }
时,按钮会进入加载动画,并且在delay
秒之后加载动画结束。那么因此可以推断,按钮中本身也有一个 loading 的 state,这个 state 既会受到props.loading
的影响,也会在内部由 delay 来 setTimeout。
因此我们在ButtonState
这个接口中,就能发现这个 loading
interface ButtonState {
loading?: boolean | { delay?: number };
hasTwoCNChar: boolean;
}
进一步往下看,通过 React 的getDerivedStateFromProps
生命周期,在组件挂载后(didMount
)和更新后(didUpdate
)将 props 映射到 state 中。
static getDerivedStateFromProps(nextProps: ButtonProps, prevState: ButtonState) {
// 当props的loading为`boolean`时会同步`props.loading`到`state.loading`
if (nextProps.loading instanceof Boolean) {
return {
...prevState,
loading: nextProps.loading,
};
}
return null;
}
那么,在componentDidUpdate
中,自然会有处理{delay: number}
的loading
的逻辑。
componentDidUpdate(prevProps: ButtonProps) {
this.fixTwoCNChar();
// 清理上一个timer
if (prevProps.loading && typeof prevProps.loading !== 'boolean') {
clearTimeout(this.delayTimeout);
}
const { loading } = this.props;
// 当前props中有delay的loading时,在delay秒之后设置loading状态为true
if (loading && typeof loading !== 'boolean' && loading.delay) {
// 显然,直接将loading直接设置为对象,确实可以做到loading
this.delayTimeout = window.setTimeout(() => this.setState({ loading }), loading.delay);
} else if (prevProps.loading === this.props.loading) {
// `prevProps.loading`和`this.props.loading`相同时跳过setState,避免进入无限更新
return;
} else {
this.setState({ loading });
}
}
最后还有个在 willUnmount 时的扫尾工作,在组件卸载的时候清除 loading 的 timeout 避免空指针引用等问题
componentWillUnmount() {
if (this.delayTimeout) {
clearTimeout(this.delayTimeout);
}
}
按钮的渲染
与按钮渲染相关的主要有 2 个函数,其中一个是react
生命周期自带的 render,render 的实现非常简单,用一个<ConfigCustomer>
包裹了this.renderButton
,这个ConfigCustomer
是由 React context 创建出的一部分,用于 context 的消费
render() {
return <ConfigConsumer>{this.renderButton}</ConfigConsumer>;
}
接下来看看最关键的this.renderButton
的实现
renderButton = ({ getPrefixCls }: ConfigConsumerProps) => {
const {
prefixCls: customizePrefixCls,
type,
shape,
size,
className,
children,
icon,
ghost,
loading: _loadingProp, // 这个_loadingProp提取出来只是为了不让他进入...rest中,无其它用处
block,
...rest
} = this.props;
const { loading, hasTwoCNChar } = this.state;
const prefixCls = getPrefixCls('btn', customizePrefixCls); // 从context中获取class的前缀
// 按钮尺寸
// large => lg
// small => sm
let sizeCls = '';
switch (size) {
case 'large':
sizeCls = 'lg';
break;
case 'small':
sizeCls = 'sm';
default:
break;
}
const classes = classNames(prefixCls, className, {
[`${prefixCls}-${type}`]: type,
[`${prefixCls}-${shape}`]: shape,
[`${prefixCls}-${sizeCls}`]: sizeCls,
[`${prefixCls}-icon-only`]: !children && children !== 0 && icon, //是否是仅有图标的按钮,这种按钮是圆的
[`${prefixCls}-loading`]: loading, // 加载中
[`${prefixCls}-background-ghost`]: ghost, //幽灵按钮
[`${prefixCls}-two-chinese-chars`]: hasTwoCNChar, // 按钮文字是否是两个中文
[`${prefixCls}-block`]: block // 是否为块状按钮
});
const iconType = loading ? 'loading' : icon; // 按钮加载时icon强制为loading图标
const iconNode = iconType ? <Icon type={iconType} /> : null; // icon组件
const kids =
children || children === 0 // 此处特判0是因为0是falsy但不是nullish,也需要渲染出来
? React.Children.map(children, child =>
insertSpace(child, this.isNeedInserted())
)
: null;
const linkButtonRestProps = omit(rest as AnchorButtonProps, ['htmlType']); // 当是link的时候从props中忽略'htmlType'属性
if (linkButtonRestProps.href !== undefined) {
// a标签的时候返回一个a标签
return (
<a
{...linkButtonRestProps}
className={classes}
onClick={this.handleClick}
ref={this.saveButtonRef}
>
{iconNode}
{kids}
</a>
);
}
// React认不得htmlType,所以需要将它从`rest`提取出来
const { htmlType, ...otherProps } = rest as NativeButtonProps;
// 下面的`<Wave/>`组件,是按钮被点击时从四周扩散的波纹效果
return (
<Wave>
<button
{...otherProps as NativeButtonProps}
type={htmlType || 'button'}
className={classes}
onClick={this.handleClick}
ref={this.saveButtonRef}
>
{iconNode}
{kids}
</button>
</Wave>
);
};
内容为两个中文字时的空格添加
进一步观察 Button 的源码,发现当按钮内容为两个中文字时会在其中插入一个空格
首先看看插入空格的实现
// 自动地在两个中文字之间插入空格
function insertSpace(child: React.ReactChild, needInserted: boolean) {
// 为null直接返回
if (child == null) {
return;
}
// 是否需要插入
const SPACE = needInserted ? ' ' : '';
// child为浏览器原生标签时,判断child.props.children是否为两个中文字符
if (
typeof child !== 'string' &&
typeof child !== 'number' &&
isString(child.type) &&
isTwoCNChar(child.props.children)
) {
return React.cloneElement(
child,
{},
child.props.children.split('').join(SPACE)
);
}
// child为string时,往两个中文字之中插入
if (typeof child === 'string') {
if (isTwoCNChar(child)) {
child = child.split('').join(SPACE);
}
return <span>{child}</span>;
}
// 其它情况,直接返回传入的组件
return child;
}
按钮组
按钮组的实现非常简单,下面直接贴代码,注释会在代码中
const ButtonGroup: React.SFC<ButtonGroupProps> = props => (
// 从context中接收config
<ConfigConsumer>
{({ getPrefixCls }: ConfigConsumerProps) => {
const {
prefixCls: customizePrefixCls,
size,
className,
...others
} = props;
const prefixCls = getPrefixCls('btn-group', customizePrefixCls);
// large => lg
// small => sm
let sizeCls = ''; // 按钮大小
switch (size) {
case 'large':
sizeCls = 'lg';
break;
case 'small':
sizeCls = 'sm';
default:
break;
}
const classes = classNames(
prefixCls,
{
[`${prefixCls}-${sizeCls}`]: sizeCls // 当size位指定时,`${prefixCls}-${sizeCls}`不会出现在最终的className中
},
className
);
return <div {...others} className={classes} />;
}}
</ConfigConsumer>
);
export default ButtonGroup;