读 Antd 组件源码有感-0

1,126 阅读2分钟

背景

最近在做一个 Design 下面的 PC 端项目,之前一直从事的移动端开发,使用 Vue 和 Egret 偏多,但这次是用 React 和 Antd 开发完成的,摸鱼的时候就顺便读一下 Antd 的源码。我的习惯是:先学会怎么用,再看它怎么实现。(时隔三年,再次开启写作。。。)

起手

通过 Vite 简单启动一个 React 项目,然后删去不需要的文件,最终我们只看下面两个文件

main.tsx

import ReactDOM from 'react-dom';
import './index.css';

const App: React.FC = () => <h1>hello</h1>;

ReactDOM.render(<App />, document.getElementById('root'));

vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [
        react(),
    ],
});

项目已正常运行,今天我们研究的对象是 Antd,需要先安装,再进行如下配置:

vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import styleImport from 'vite-plugin-style-import';

// https://vitejs.dev/config/
export default defineConfig({
    css: {
        preprocessorOptions: {
            less: {
                javascriptEnabled: true,
            },
        },
    },
    plugins: [
        react(),
        styleImport({
            libs: [
                {
                    libraryName: 'antd',
                    esModule: true,
                    resolveStyle: name => {
                        return `antd/es/${name}/style/index`;
                    },
                },
            ],
        }),
    ],
});

通过 vite-plugin-style-import 这个插件,帮助我们自动导入组件样式。后面出于篇幅问题,不再提供重复的代码预览了。

使用

main.tsx

const App: React.FC = () => {
    const onClick = React.useCallback(() => {}, []);

    return (
        <Button type="primary" onClick={onClick}>
            按钮
        </Button>
    );
};

ReactDOM.render(
    <ConfigProvider>
        <App />
    </ConfigProvider>,
    document.getElementById('root'),
);

不出意外,页面上可以看到一个大大的按钮了。正式进入今天的第一个主角:ConfigProvider

ConfigProvider

在深入之前,我们得先了解一下 Context (懂的跳过)。在 React 的某个版本中,为了解决组件层级过深传递 Props 的麻烦问题,引入了上下文 Context 这个方案,不论层级多深,都可以通过 Consumer 拿到顶层的 Value。

const MyContext = React.createContext(defaultValue);

const App = <MyContext.Provider value={/* some value */}>
    <Child />
</MyContext.Provider>

const Child = <MyContext.Consumer>
  {value => /* render something based on the context value */}
</MyContext.Consumer>

但我们可能更喜欢通过下面的方式拿到 Value

const Child = () => {
    const value = React.useContext(MyContext)
    return <div>{value}<div>
}

那么为什么 Antd 需要提供一个 ConfigProvider 呢?

我需要所有按钮都用最小状态

<Button size="sm" />

我需要所有弹出框挂载到 app 节点,而不是 body

<Modal getContainer={() => document.getElementById('app')} />

我需要

事实上,在业务开发中,组件只是一个很小的单元罢了,上层封装了非常多的业务组件,我们不可能每个组件的属性都重复设置一遍吧。通过上下文的方式,我们只要在最顶层设置一次,所有组件都能够响应。

<ConfigProvider componentSize="small">
    <App />
</ConfigProvider>

其它 API 点击查看 config-provider

ConfigProvider 非 Provider

这句话怎么理解呢?其实是这样的,为了清晰每个上下文的作用,Antd 内部总共拆分为 5 个上下文,它们分别是: RcFromContext、LocaleContext、IconContext、SizeContext、configContext。对外暴露的 ConfigProvider 是一个高阶组件,接受用户的属性输入,按类别分别给到不同的上下文,这样组件在获取上下文的时候就相对职责清晰,比如:

const Child = () => {
    const size = React.useContext(SizeContext)
    return <button size={size} />
}

所以 ConfigProvider 的实现大致如下:

const ConfigProvider = (props) => {
    return <ConfigContext.Provider value={...}>
        <SizeContext.Provider value={...}>
            <IconContext.Provider value={...}>
                <LocaleContext.Provider value={...}>
                    <RcFromContext.Provider value={...}>
                        {props.children}
                    </RcFromContext.Provider>
                </LocaleContext.Provider>
            </IconContext.Provider>
        </SizeContext.Provider>
    </ConfigContext.Provider>
}

Button

不知道谁说的:读懂一个组件库,就看读懂它的按钮组件,那我们就从 button/button.tsx 开始。

// 正则表达式:匹配两个汉字
const rxTwoCNChar = /^[\u4e00-\u9fa5]{2}$/;
// 判断一个字符串是不是两个汉字
const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar);

tuple 函数,非常巧妙的生成一个纯字符串元祖类型的数组

// 生成一个 元祖类型 的数组对象
const tuple = <T extends string[]>(...args: T) => args;
// 按钮类型的数组
const ButtonTypes = tuple('default', 'primary', 'ghost', 'dashed', 'link', 'text');
// 按钮类型的类型
type ButtonType = typeof ButtonTypes[number]

ref

一般情况下,ref 属性用的不多,但在组件库中使用颇多。但 ref 属性只能用于 DOM 元素,或者 class 组件,对于 class 组件,我们拿到的是实例。详细说明可以看一下 这一章

函数组件,不存在实例,所以在上层我们就拿不到组件实例。如果我们想拿到函数组件的 DOM 呢?

首先想到了 props 传递(ref属性会被props属性剥离,所以必须使用别名)

const Parent = () => {
    const ref = React.useRef(null)
    
    return <Child inputRef={ref} />
}

const Child = (props) => {
    return <input ref={props.inputRef} />
}

总感觉多一个 inputRef prop 不够优雅,于是:

const Parent = () => {
    const ref = React.useRef(null)
    
    return <Child ref={ref} />
}

const Child = React.forwardRef<HTMLInputElement, {}>((props, ref) => {
    return <input ref={ref} />
})

使用 React.forwardRef 包裹,就可以传递 ref 了,详细可以看一下这一章

记得给匿名的函数组件定义一个 displayName 否则在 devtools 中就不太好看了

classNames

使用 classNames 这个库来处理 classname 拼接问题,这里 prefixCls 也是受上文提到的 ConfigProvider 的 prefixCls 属性控制

const classes = classNames(
    prefixCls,
    {
      [`${prefixCls}-${type}`]: type,
      [`${prefixCls}-${shape}`]: shape !== 'default' && 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,
  );

vite.config.ts

export default defineConfig({
    css: {
        preprocessorOptions: {
            less: {
                modifyVars: {
                    // 这里和 prefixCls 一致即可
                    'ant-prefix': 'aa',
                },
            },
        },
    },
}

到这里 Button 组件的细节就说完了,但是还有两个遗漏的点,一个是 Loading,一个是 Wave 。这是 Button 组件中的两个动画效果,实际还是比较复杂的,涉及到 React 中动画的实现,可以放到单独的章节再讲。

最后

应该是出于字符数不达标,小编一直不给过。我现在是懂了,为什么有些文章里面废话很多~~那就结尾再说一下动画吧,后面会抽出章节,讲一下框架中的封装。

动画是人眼对于单位时间内物体发生频繁变化的视觉停留效果,一般来说,每秒变化20下在人眼中已经是连续的了,专业点来说就是每秒20帧。(有些人眼睛有毒,可能需要更高的帧数,否则就会发现卡顿)。

动画的几个概念

单位时间

其实就是时间线,它代表某一时刻开始,连续不断的递增过程。就像青春,一去不复回。

时钟

时间线本身不会变化,需要一个力,而这个力,我们专业称之为时钟。就像一个高级的闹钟,我们可以设置时间,设置快慢,定时等等一系列操作。而时钟的电池,就是 requestAnimationFrame。由于 requestAnimationFrame 本身运行的不稳定性(每一帧回调时间有长有短),所以时钟本身还需要有一定的自我矫正能力。

属性变化

一般情况下,物体本质不会变,变的是它的属性,位置、大小、外观等。既然是变化,肯定有一个初值和终值,在单位时间内初值变化到种植,这中间就产生了插值,通过将插值反应到物体身上,动画自然就成了。如果用一个函数来表示就是:

y=vty = v * t

但是大部分情况下,不会这么简单。

插值计算

可能我们更习惯称之为缓动方程/计算/算法。 假设物体初始位置为 x1,终点位置为 x2,运行时间为 t,时间线当前时间为 dt,那么简单的插值计算方式为:

x=x1+(x2x1)(dt/t)x = x1 + (x2 - x1) * (dt / t)

这其实就是最简单的 linear 函数了。

封装

动画的本质就是上面这些内容了,无非是不同的语言,不同的实现,归根到底就是:数值在单位时间内的计算。要封装的就是就是两点:时间怎么动、数值怎么算。

而在视图层面,不同的物体,设置属性的方式不同罢了。