Antd 4.X 源码解读 - Button

1,079 阅读5分钟

前言

欲穷千里,必先自宫。吾知夙愿,读木成舟。天上火箭,地上轮子,尽在吾手。

一个菜鸡码畜的学习记录,通过不断学习源码,了解其 设计思路,锻炼在下之思。

以下都是瞎写写,自己记录下。

思考

当我们要做一个组件首先要思考,我们需要什么,这个轮子由什么部分组成,我得让它有轮毂,有气圈等才能让它滚起来吧!那么我们这个按钮需要什么呢?颜色,大小,圆的方的,花里胡哨的还可以放个图标啥的,说到这些这不就是官方文档上的说明嘛,所以必不可少我在学习源码过程中也是在不断阅读官方文档。

始于足下

首先大概粗看一下,代码挺多,先来了解下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;