前言
欲穷千里,必先自宫。吾知夙愿,读木成舟。天上火箭,地上轮子,尽在吾手。
一个菜鸡码畜的学习记录,通过不断学习源码,了解其 设计思路,锻炼在下之思。
以下都是瞎写写,自己记录下。
思考
当我们要做一个组件首先要思考,我们需要什么,这个轮子由什么部分组成,我得让它有轮毂,有气圈等才能让它滚起来吧!那么我们这个按钮需要什么呢?颜色,大小,圆的方的,花里胡哨的还可以放个图标啥的,说到这些这不就是官方文档上的说明嘛,所以必不可少我在学习源码过程中也是在不断阅读官方文档。
始于足下
首先大概粗看一下,代码挺多,先来了解下import的一些内容
import * as React from 'react';
import classNames from 'classnames';
import omit from 'omit.js';
import Group from './button-group';
import { ConfigContext } from '../config-provider';
import Wave from '../_util/wave';
import { Omit, tuple } from '../_util/type';
import devWarning from '../_util/devWarning';
import SizeContext, { SizeType } from '../config-provider/SizeContext';
import LoadingIcon from './LoadingIcon';
import { cloneElement } from '../_util/reactNode';
classnames 是一个有条件的将html的class类名 拼接起来的JS库
omit.js 基于键、值或求值函数从对象(或对象数组)中删除值
button-group 本文不涉及
ConfigContext ,SizeContext 利用CreateContext共享一些数据,不重点讲
Wave 点击波浪动效
cloneElement 克隆元素
添砖加瓦
我们首先看一下组件的传参也就是props,类型定义ButtonProps,发现 Partial将AnchorButtonProps & NativeButtonProps 都转化为可选类型,而AnchorButtonProps,NativeButtonProps都继承自BaseButtonProps
export interface BaseButtonProps {
type?: ButtonType;
icon?: React.ReactNode;
shape?: ButtonShape;
size?: SizeType;
loading?: boolean | { delay?: number };
prefixCls?: string;
className?: string;
ghost?: boolean;
danger?: boolean;
block?: boolean;
children?: React.ReactNode;
}
按钮大小
在上面的 BaseButtonProps 中我们看到按钮的大小可能是 size 这个字段控制,那我们通过 SizeType 看一下size 的 类型
export type SizeType = 'small' | 'middle' | 'large' | undefined;
const SizeContext = React.createContext<SizeType>(undefined);
这里说明我们的按钮内置三种大小 ,对应小中大三种,通过createContext 实现共享
let sizeCls = '';
switch (customizeSize || size) {
case 'large':
sizeCls = 'lg';
break;
case 'small':
sizeCls = 'sm';
break;
default:
break;
}
这段很简单不多说
加载状态
我们看到 BaseButtonProps 中 loading是一个 Boolean 或者是 一个对象延迟多少秒加载
let loadingOrDelay: Loading;
if (typeof loading === 'object' && loading.delay) {
loadingOrDelay = loading.delay || true;
} else {
loadingOrDelay = !!loading;
}
React.useEffect(() => {
clearTimeout(delayTimeoutRef.current);
if (typeof loadingOrDelay === 'number') {
/*
* 这里我们可以了解下 React.createRef 和 React.useRef 的区别和作用
* createRef 和 useRef 都可以用来保存任何值 和 Dom实例
* useRef 在函数组件的生命周期,保持状态不变,除非手动修改
* createRef 在函数组件中使用,行为模式任然是函数,被重新渲染时会初始化所有变量和表达式,
* 而在 class 组件中则是因为 class 组件在更新的时候只会调用 render ,componentDidUpdate
* 等几个methods,只会在组件初始化的时候创建ref,之后在整个过程中保持不变
*/
delayTimeoutRef.current = window.setTimeout(() => {
setLoading(loadingOrDelay);
}, loadingOrDelay);
} else {
setLoading(loadingOrDelay);
}
}, [loadingOrDelay]);
// 存储loading 加载状态, 默认 false
const [innerLoading, setLoading] = React.useState<Loading>(!!loading);
const iconType = innerLoading ? 'loading' : icon;
// icon 图标Dom 节点存在,只在innerLoading = true 状态下展示LoadingIcon,不存在icon,
则LoadingIcon 的 loading={!!innerLoading} 显示loading状态
const iconNode = icon && !innerLoading ? ( icon ) : (
<LoadingIcon existIcon={!!icon} prefixCls={prefixCls} loading={!!innerLoading} />
);
组件样式
const classes = classNames(prefixCls, className, {
[`${prefixCls}-${type}`]: type,
[`${prefixCls}-${shape}`]: shape,
[`${prefixCls}-${sizeCls}`]: sizeCls,
[`${prefixCls}-icon-only`]: !children && icon,
[`${prefixCls}-loading`]: loading,
[`${prefixCls}-clicked`]: clicked,
[`${prefixCls}-background-ghost`]: ghost,
});
没啥记录的
function insertSpace(child: React.ReactChild, needInserted: boolean) {
// Check the child if is undefined or null.
if (child == null) {
return;
}
const SPACE = needInserted ? ' ' : '';
// strictNullChecks oops.
/*
* 这个判断的意思是当这个child不是字符串也不是数字并且child.type为字符串并且child
* 的children是汉字的情况下加上空格,不是嵌套子组件的时候 child.type 为string,是
* React组件的时候,react会给其包装一些属性
*/
if (
typeof child !== 'string' &&
typeof child !== 'number' &&
isString(child.type) &&
isTwoCNChar(child.props.children)
) {
return cloneElement(child, {
children: child.props.children.split('').join(SPACE),
});
}
// Button 组件中的子元素是 字符串且为中文汉字的时候添加一个空格
if (typeof child === 'string') {
if (isTwoCNChar(child)) {
child = child.split('').join(SPACE);
}
return <span>{child}</span>;
} return child;
}
其它函数
// 两个中文字符
const rxTwoCNChar = /^[\u4e00-\u9fa5]{2}$/;
/*
* bind创建一个新函数(绑定函数),
* bind第一个参数作为this,剩余参数加上绑定函数运行时本身的参数按照顺序作为原函 * 数的参数来调用原函数
* /
const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar);
// 没有边框的按钮类型function isUnborderedButtonType(type: ButtonType | undefined) { return type === 'text' || type === 'link';}
完整源码
/* eslint-disable react/button-has-type */import * as React from 'react';import classNames from 'classnames';import omit from 'omit.js';import Group from './button-group';import { ConfigContext } from '../config-provider';import Wave from '../_util/wave';import { Omit, tuple } from '../_util/type';import devWarning from '../_util/devWarning';import SizeContext, { SizeType } from '../config-provider/SizeContext';import LoadingIcon from './LoadingIcon';import { cloneElement } from '../_util/reactNode';const rxTwoCNChar = /^[\u4e00-\u9fa5]{2}$/;const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar);function isString(str: any) { return typeof str === 'string';}function isUnborderedButtonType(type: ButtonType | undefined) { return type === 'text' || type === 'link';}// Insert one space between two chinese characters automatically.function insertSpace(child: React.ReactChild, needInserted: boolean) { // Check the child if is undefined or null. if (child == null) { return; } const SPACE = needInserted ? ' ' : ''; // strictNullChecks oops. if ( typeof child !== 'string' && typeof child !== 'number' && isString(child.type) && isTwoCNChar(child.props.children) ) { return cloneElement(child, { children: child.props.children.split('').join(SPACE), }); } if (typeof child === 'string') { if (isTwoCNChar(child)) { child = child.split('').join(SPACE); } return <span>{child}</span>; } return child;}function spaceChildren(children: React.ReactNode, needInserted: boolean) { let isPrevChildPure: boolean = false; const childList: React.ReactNode[] = []; React.Children.forEach(children, child => { const type = typeof child; const isCurrentChildPure = type === 'string' || type === 'number'; if (isPrevChildPure && isCurrentChildPure) { const lastIndex = childList.length - 1; const lastChild = childList[lastIndex]; childList[lastIndex] = `${lastChild}${child}`; } else { childList.push(child); } isPrevChildPure = isCurrentChildPure; }); // Pass to React.Children.map to auto fill key return React.Children.map(childList, child => insertSpace(child as React.ReactChild, needInserted), );}const ButtonTypes = tuple('default', 'primary', 'ghost', 'dashed', 'link', 'text');export type ButtonType = typeof ButtonTypes[number];const ButtonShapes = tuple('circle', 'circle-outline', 'round');export type ButtonShape = typeof ButtonShapes[number];const ButtonHTMLTypes = tuple('submit', 'button', 'reset');export type ButtonHTMLType = typeof ButtonHTMLTypes[number];export type LegacyButtonType = ButtonType | 'danger';export function convertLegacyProps(type?: LegacyButtonType): ButtonProps { if (type === 'danger') { return { danger: true }; } return { type };}export interface BaseButtonProps { type?: ButtonType; icon?: React.ReactNode; shape?: ButtonShape; size?: SizeType; loading?: boolean | { delay?: number }; prefixCls?: string; className?: string; ghost?: boolean; danger?: boolean; block?: boolean; children?: React.ReactNode;}// Typescript will make optional not optional if use Pick with union.// Should change to `AnchorButtonProps | NativeButtonProps` and `any` to `HTMLAnchorElement | HTMLButtonElement` if it fixed.// ref: https://github.com/ant-design/ant-design/issues/15930export type AnchorButtonProps = { href: string; target?: string; onClick?: React.MouseEventHandler<HTMLElement>;} & BaseButtonProps & Omit<React.AnchorHTMLAttributes<any>, 'type' | 'onClick'>;export type NativeButtonProps = { htmlType?: ButtonHTMLType; onClick?: React.MouseEventHandler<HTMLElement>;} & BaseButtonProps & Omit<React.ButtonHTMLAttributes<any>, 'type' | 'onClick'>;export type ButtonProps = Partial<AnchorButtonProps & NativeButtonProps>;interface CompoundedComponent extends React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLElement>> { Group: typeof Group; __ANT_BUTTON: boolean;}type Loading = number | boolean;const InternalButton: React.ForwardRefRenderFunction<unknown, ButtonProps> = (props, ref) => { const { loading, prefixCls: customizePrefixCls, type, danger, shape, size: customizeSize, className, children, icon, ghost, block, ...rest } = props; const size = React.useContext(SizeContext); 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>(); const delayTimeoutRef = React.useRef<number>(); const isNeedInserted = () => { return 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 =============== let loadingOrDelay: Loading; if (typeof loading === 'object' && loading.delay) { loadingOrDelay = loading.delay || true; } else { loadingOrDelay = !!loading; } React.useEffect(() => { clearTimeout(delayTimeoutRef.current); if (typeof loadingOrDelay === 'number') { delayTimeoutRef.current = window.setTimeout(() => { setLoading(loadingOrDelay); }, loadingOrDelay); } else { setLoading(loadingOrDelay); } }, [loadingOrDelay]); React.useEffect(() => { fixTwoCNChar(); }, [buttonRef]); const handleClick = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>) => { const { onClick } = props; if (innerLoading) { return; } if (onClick) { (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 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, className, { [`${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', }); const iconNode = icon && !innerLoading ? ( icon ) : ( <LoadingIcon existIcon={!!icon} prefixCls={prefixCls} loading={!!innerLoading} /> ); const kids = children || children === 0 ? spaceChildren(children, isNeedInserted() && autoInsertSpace) : null; const linkButtonRestProps = omit(rest as AnchorButtonProps, ['htmlType', 'loading']); if (linkButtonRestProps.href !== undefined) { return ( <a {...linkButtonRestProps} className={classes} onClick={handleClick} ref={buttonRef}> {iconNode} {kids} </a> ); } // React does not recognize the `htmlType` prop on a DOM element. Here we pick it out of `rest`. const { htmlType, ...otherProps } = rest as NativeButtonProps; const buttonNode = ( <button {...(omit(otherProps, ['loading']) 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.defaultProps = { loading: false, ghost: false, block: false, htmlType: 'button' as ButtonProps['htmlType'],};Button.Group = Group;Button.__ANT_BUTTON = true;export default Button;