普通视图

发现新文章,点击刷新页面。
昨天 — 2025年11月19日首页

打包票!前端和小白一定明白的人工智能基础概念!

作者 孟祥_成都
2025年11月18日 13:38

AI时代,不知道你是否和我有同样的经历:搜索了大量号称“小白也能看懂”的AI科普文章,结果点进去,仍有90%的内容让人一头雾水。

这篇文章,是我在阅读众多资料后,整理出的一份更易懂的总结。它不强求全面,但力求逻辑清晰、层层递进——从基础概念逐步引出更复杂的内容,而不是一上来就抛出“神经网络”“深度学习”或“ChatGPT预测模型”这样的术语。

我相信,只要你具备初中知识水平,就能轻松理解。让我们开始吧!

一、人工智能的起源

1956年,一群科学家在达特茅斯会议上首次提出“人工智能”这一概念。他们讨论的核心问题是:如何制造出能够学习并模拟人类智能的机器。

从此,人工智能作为一个独立的研究领域正式诞生。

但问题在于,机器处理信息的方式与人类截然不同。机器接收的所有数据最终都会转化为数字(包括文字)。

简单解释为什么文字在计算机内部也是数字表示的,就涉及到编码的知识,例如 ASCII 编码,字母 a 在计算机内部表示 97。而 97 最终会被解释而 2 进制,因为计算机本身就是 2 进制的。它只能认识 0 和 1。然后我们将数字和文字做个映射,例如 01100001 表示字母 a ,而 01100001 的 10 进制就是 97.

我们抽象一下计算机的思考方式,简单来说就是:

f(x)=y

  • 我们向计算机输入参数 x

  • 计算机将参数转为数字(这就是为什么很多文章说什么向量这个概念,向量可以简单理解为数字组成的多维数组,也就是说例如 “苹果” 这个词,最终要转化为数字,计算机才能理解),然后通过函数 f 处理并计算

  • 最终输出结果 y

但这显然不是人类思考问题的方式。那么,如何让机器具备类似人类的判断与学习能力,成为真正的“智能机器”呢?

科学家们提出了不同的思路。

二、符号主义:用规则模拟智能

在人工智能的早期阶段,符号主义(Symbolism)是一种主流思路。它认为,可以通过数学逻辑来模拟人类的推理过程。

举个例子,我们设计一个判断是否下雨的机器:

  • 参数 aa:是否为阴天

  • 参数 bb:湿度是否大于70%

只有当 a 和 b 同时为“真”时,机器才输出“要下雨”,否则输出“不下雨”。

这种思路本质上就是编程中的 if...else... 逻辑。

别小看符号主义,它的成功应用之一就是“专家系统”。比如在医疗诊断中:

  • 从 头疼 + 发热 + 咳嗽 的症状 → 能推测出得了流感

  • 从 腹痛 + 尿血 → 能推测出得了 肾结石

通过一系列规则组合,专家系统能够模拟人类专家的决策过程,并在特定领域取得了显著成果。

但它也有明显的局限:

  1. 规则难以统一:比如面对同一张股票走势图,不同专家可能做出完全相反的判断。

  2. 无法自主学习:系统本身不具备学习能力,依赖人工更新规则。

随着研究的深入,另一种思路逐渐兴起:与其预设所有规则,不如让机器自己从数据中学习。这就是“联结主义”(Connectionism)。

三、联结主义:让机器自己学习

这种模式有点像训狗,你说坐下,它坐下你就奖励零食,如果错了,就跟它一飞腿,这样你就能训练出一个会听坐下指令的狗了。

我们把狗换成机器,也可以用同样的方式训练,让它在某个任务下完成任务。例如说现在要训练一个能识别苹果图片的智能。

举例:识别苹果

那么机器肯定要识别苹果的特征,才能区别别的图片,假设我们设置了如下维度

  • 直径: 苹果直径大约10cm

  • 颜色: 苹果是红色

  • 形状: 苹果是球形

例如在某些条件下,直径,颜色,形状都符合苹果特性的条件下,才是苹果,但是我们之前说了,计算机只认识数字,只能通过计算来判断,所以我们需要结合一些数学公式来把 直径,颜色,形状,映射为数字,通过数字的计算映射它们在现实生活的是否对应。

既然文字也可以通过数字映射,例如 97 代表数字 a,那么其它属性也可以,例如(我乱说的,就是表达一种意思),我们把形状,颜色,和直径,都理解为权重。什么意思呢?我们举个例子:

假设我们给每个特征分配一个权重(weight),代表这个特征对“是否是苹果”的重要程度:

  • 直径:苹果直径约 10cm 权重 = +0.6(越接近 10cm 越可能是苹果)

  • 颜色:红色程度(0 不是红,1 是红) 权重 = +0.3(红色对判断有贡献)

  • 形状:球形程度(0 不是球形,1 是球形) 权重 = +0.4(球形对判断有贡献)

然后,我们设计一个简单的“苹果得分”公式:

苹果得分=(直径得分)×0.6+(颜色得分)×0.3+(形状得分)×0.4

然后得出来的值,如果大于 1 就是苹果,如果小于 1 就不是苹果。

计算例子

例1:一个红苹果(直径 10cm,红色,球形)

  • 直径得分 = 1
  • 颜色得分 = 1
  • 形状得分 = 1

苹果得分 = 1×0.6+1×0.3+1×0.4=1.3

例2:一个橙子(直径 8cm,橙色,球形)

  • 直径得分 = 0.8(假设 8cm 离 10cm 差 2cm,得分 0.8)
  • 颜色得分 = 0(不是红色)
  • 形状得分 = 1

苹果得分 = 0.8×0.6+0×0.3+1×0.4=0.48+0.4=0.88

大家应该明白上面的意思了吧。我们再次抽象为数学公式,也就是变为 1 次函数。将得分用 x 表示,将权重用 w 表示,如下:

z = (w1 × x1) + (w2 × x2) + (w3 × x3) + b

其中:

  • w1,w2,w3 是各特征的权重(重要性)

  • b 是偏置项(可理解为判断门槛)

所以

  • 如果 z≥0,判定为苹果

  • 如果 z<0,判定为非苹果

因为有 w1,w2,w3 3个参数,不利于我们后面的讲解,我们再次简化公式,来帮助我们理解后面的概念。

z = (w1 × x1) + (w2 × x2) + b

变为只有两个参数来决定是否是苹果,其实这是这是初中数学中的 线性方程,它的图像是一条直线。如下:

这条直线下方的就是就是非苹果,上方的就是苹果。

接下来有人会问,你说形状,直径这些特征的值,是怎么来的呢?

它们当然不是天然存在的,而是需要我们人为设计和提取的。这个过程在传统机器学习中被称为 “特征工程”。我们又要举一个粗糙的例子了,我们拿颜色得分来举例:

  • 思路: 苹果通常是红色、绿色或黄色。我们需要量化“红色程度”。

  • 设计方法:

    • 如果是从图片中提取: 计算机可以分析图片的所有像素点。

      • 将图片从RGB颜色空间转换到 HSV 颜色空间(H代表色调,能更好地表示颜色本身)。
      • 统计所有像素中,色调(H)在红色范围内(比如0-10度和350-360度)的像素比例。比例越高,x2越接近1。
    • 如果是从文字描述提取: 如果我们的数据是文字“深红色”,我们可以建立一个颜色词典:

      • “深红色” -> x2 = 0.9

      • “浅红色” -> x2 = 0.7

      • “绿色” -> x2 = 0.3(因为青苹果也存在)

      • “蓝色” -> x2 = 0

好了,特征得分我们解决了,然后就是训练,调整 w1 和 w2 参数,从而找出一个分界线,在分界线范围内的就是苹果,范围外的就是其它。当然这个分界线不一定是直线,也可以是很复杂的曲线范围,我们只是为了引出核心概念,就是:

“机器学习”!

虽然没有直接说出“机器学习”四个字,但已经完整地描绘了它的核心思想:通过数据(苹果的特征)和反馈(得分是否大于阈值),让机器自动调整内部参数(权重 w 和偏置 b),从而学会一项任务(识别苹果)。

我们接着聊,刚才我们举得例子非常粗糙,但可以很容易的理解大概的意思。

上面这种机器学习的思路,称为联结主义,可这种思路在最初曾一度被整个世界称为骗子思路!

为什么是骗子?

刚才我们举了一个非常简单的例子,让机器根据 直径、颜色 来判断是不是苹果。

我们最终把它抽象成一个数学公式:

z = (w1 × x1) + (w2 × x2) + b

本质上,它就是一个一次函数(二维下是一条直线,三维下是一张平面)。

在很多任务中,这种模型真的能工作。

比如“苹果 vs 不是苹果”,

如果苹果的数据大多集中在同一块区域,那么一条直线(或平面)确实能把它们区分开来。

如果有一个任务,根本无法用一条直线分开呢?

科学家们很快就发现:

并不是所有问题都像识别苹果一样简单。

其中最典型的例子就是 异或 XOR )问题

先看什么是异或:

输入 A 输入 B 输出
0 0 0
0 1 1
1 0 1
1 1 0

也就是输入是一样的情况,例如都是 0 或者都是 1 ,得到的结果是 0,否则得到的结果是 1。

如下图,我们是无法找到一条直线,分割红色点和蓝色点的

这就意味着,简单的线性模型,有些情况是无法模拟的!

也就是说 ❌ 再怎么调整 w1、w2、b,这个模型也永远无法学会异或。

这导致了人工智能历史上第一次寒冬(AI Winter)。

四、突破:引入非线性!—— 神经网络的诞生

后来科学家们发现:

如果在两个线性模型中间,再加上一层“非线性函数”,就能解开异或。

你可以把思想理解成:

  • 一条直线不能分的

  • 两条直线可以

  • 让模型像搭积木一样把“多条直线”组合起来 → 就能拼出复杂的边界

什么意思呢,我们可以简单用下入理解:

如上图右边,是不是边界变成了不是一条直线,而是两个曲线去分割呢,借助这这种思路就解决了异或问题。

这就是**神经网络(Neural Network)**最核心的思想:

多个简单模型叠在一起,中间加上激活函数(非线性), 就能表达更复杂的决策边界。

我们从开始的线性模型,也就是单层结构是:

输入 → 权重加权 → 激活函数 → 输出

简单来说就是输入 x1, x2 的得分 —> 跟 w1,w2 权重计算 -> 跟阈值对比,例如大于 1 就是苹果 -> 输出是否是苹果

然后再看看新的多层结构:

输入 → [权重加权 + 激活函数] → [权重加权 + 激活函数] → 输出

好了至此,我们就明白了神经网络的概念,神经网络也是联结主义的一部分。此时再次解释以下联结主义的主张:

“智能来自大量简单单元(神经元)的连接和学习,而不是手写规则。”

我们再来一个小小结,就是从符号主义,这种依靠人自身写规则到联结主义,到神经网络,我们逐渐步入了新的人工智能时代。

五、从神经网络到深度学习:当网络变得“深不可测”

好了,现在我们明白了神经网络——它通过多层连接,巧妙地解决了简单模型无法处理的复杂问题(如异或问题)。那么,深度学习又是什么呢?

其实答案出乎意料的简单:

深度学习 = 特别“深”的神经网络。

这里的“深”,指的不是哲学的深邃,而是字面意思上的层数非常多。

我们可以做一个直观的对比:

  • 传统的神经网络:可能只有几层(比如一个输入层、一个隐藏层、一个输出层)。就像一个简单的三明治。

  • 深度学习模型:则拥有十几层、上百层甚至上千层。这就像一个巨无霸汉堡,拥有无数层馅料和面包。

为什么层数多了就厉害?

还记得我们之前手动设计“特征”的麻烦吗?(比如要自己写规则计算“颜色得分”、“形状得分”)深度学习的强大之处在于,它能够自动完成这件事,而且做得比我们好得多。

我们拿一个识别狗的例子举例:

我们可以把一个深度网络理解成一个分工极其精细的流水线工厂,用来识别一张“狗”的图片:

  1. 第一层(最基础的工人):只负责检测图像中最简单的边缘和色块。比如这里是横线,那里是竖线,那片是黑色。
  2. 中间几层(初级组装工):接收下一层的“边缘和色块”,把它们组合成更复杂的局部特征。比如,“两个圆圈”可能是眼睛,“一个三角形”可能是耳朵。
  3. 最后层(最终决策者):基于前面所有层传递过来的、已经高度抽象化的信息(比如“这是一个有胡须、尖耳朵、竖瞳的动物面部”),最终做出判断:“这是狗。”

这个过程,就是一个“逐层抽象,不断精炼”的过程。 每一层都在前一层的基础上,学习并提取更复杂、更核心的特征。网络越深,能学到的特征就越抽象,解决问题的能力也就越强。

到此我相信大部分应该明白了几个很基础的概念,就是机器学习,神经网络,深度学习的概念。我是一名前端开发技术专家,目前除了开始接触 ai 部分的知识,前端部分正在写关于 headless 组件库教程 这是网站首页,欢迎大家给个赞哦,感谢!同时也在写酷炫的动画教程,有兴趣的同学可以看这篇文章

下一章我将简单介绍一下 chatgpt 的基本原理,也是面相纯小白,也绝对包票你能看懂。

昨天以前首页

下一代组件的奥义在此!headless 组件构建思想探索!

作者 孟祥_成都
2025年11月17日 17:14

前言

这里并不是想引起所谓 ant-designelement plus 这种样式,dom结构,javascript在一起的传统组件库更好,还是国外 github start 都要接近 100k 的组件库 shadcn/ui (谷歌的 mui 已经在做 headless 版本了,你可以简单理解 headless 就是无样式组件的意思)更好!

而是提供一个更宽广的视角,都说 headless 组件库拓展性很好,可维护性很好,到底为什么这么说?(当然缺点也很明显,技术一般的人 hold 不住在拓展 headless 组件中复杂功能的实现)。

还有,国内没有特别有含金量的实战文章,比如 从 0 到 1 实现一个比较典型能说明 headless 组件优点的文章,又因为我自己在做 headless 组件库( github地址官网github宣传地址), 也看了一些这方面的文章,受益颇深, 希望能跟大家交流 headless 的思想。

对你的帮助

这篇文章读完,能确保你了解到:

  • 具备自己的 headless 组件的思路,无论是写组件库,还是实现业务组件的抽象,提供思路
  • 想了解 shadcn/ui, reach ui, radix-ui 等等这些可组合的 headless 组件内部是如何构建的

Let's get started!

什么是好的组件

在你工作中,假设你的负责人让你开发一个 手风琴 组件,如下,点击 Accordion 1 或者 2, 3的标题,其折叠在内的文字会展开。你可能会这样做。’

accordin.jpg

首先用法如下,传入 accordionData

const accordionData = [
    { id: 1, headingText: 'Heading 1', panel: 'Panel1 Content' },
    { id: 2, headingText: 'Heading 2', panel: 'Panel2 Content' },
    { id: 3, headingText: 'Heading 3', panel: 'Panel3 Content' },
]

const SomeComponent = () => {
    const [activeIndex, setActiveIndex] = useState(0)

    return (
        <div>
            <Accordion
                data={accordionData}
                activeIndex={activeIndex}
                onChange={setActiveIndex}
            />
        </div>
    )
}

然后,组件内部通过你传入的 data,使用 map 函数渲染,接着给组件的 button(用来装标题的),绑定一个 onClick 事件,当点击的时候,就看看就会 onChange 当前点击的 activeIndex 是否就是自己的 index,从而在展开内容的 <div hidden={activeIndex !== idx}>{item.panel}</div> 部分判断,是否展开自己的文字部分。


function Accordion({ data, activeIndex, onChange }) {
    return (
        <div>
            {data.map((item, idx) => (
                <div key={item.id}>
                    <button onClick={() => onChange(idx)}>
                        {item.headingText}
                    </button>
                    <div hidden={activeIndex !== idx}>{item.panel}</div>
                </div>
            ))}
        </div>
    )
}

这种封装在我们日常业务中司空见惯,但假设需求发生了变化,现在您需要在手风琴按钮和标题中添加对图标的支持,并能添加一些样式。你一般就会这样做:

- function Accordion({ data, activeIndex, onChange }) {
+ function Accordion({ data, activeIndex, onChange, displaySomething }) {
  return (
    <div>
      {data.map((item, idx) => (
        <div key={item.id}>
          <button onClick={() => onChange(idx)}>
            {item.headingText}
+            {item.icon? (
+              <span className='someClassName'>{item.icon}</span>
+            ) : null}
          </button>
-          <div hidden={activeIndex !== idx}>{item.panel}</div>  
+          <div hidden={activeIndex !== idx}>
+            {item.panel}
+            {displaySomething}
+          </div>
        </div>
      ))}
    </div>
  )
}

对于每一个不断变化的需求,你都需要重构你的组件,以满足业务的需求。这似乎很正常,需求变了难到组件不变吗?你仔细想想,似乎也不太对,就跟组件库一样,难道需求变了,组件库内部也需要不断变吗?这些组件库都是第三方的,很难要求他们根据你的需求去变化?

所以怎么可能有万能的组件库,dom 结构跟得上千变万化的业务呢?这也说明了传统 ant-design, element-plus 组件库的局限。

所以我们想想有什么办法能解决?

现在目光转移到一个简单的问题上来,想想 HTML 中的 <select><option> 元素,分开来说,这两个元素能做的很有限,可当他们组合起来的时候,通过共享一些状态,可以组合成下拉框组件,这就复合组件的概念,复杂组件组装而来,而不是一味的隐藏黑盒和过度封装。

复合组件

根据上面复合组件的思想,我们改装之前的 手风琴 组件:

<Accordion>
    <AccordionItem>
        <AccordionButton>Heading 1</AccordionButton>
        <AccordionPanel>
            Panel 1
        </AccordionPanel>
    </AccordionItem>
    <AccordionItem>
        <AccordionButton>Heading 2</AccordionButton>
        <AccordionPanel>
           Panel 2
        </AccordionPanel>
    </AccordionItem>
    <AccordionItem>
        <AccordionButton>Heading 3</AccordionButton>
        <AccordionPanel>
           Panel 3
        </AccordionPanel>
    </AccordionItem>
</Accordion>

这里,我们把整个手风琴组件拆分为 <Accordion><AccordionItem><AccordionButton>, <AccordionPanel> 四部分。

这里有同学会说了,你这例如 <AccordionButton> 都把 html 标签设置死了,拓展性也不强呀。(其实这个感觉还好,因为 css 自定义的情况下,完全可以自己改展示元素的样式,就已经脱离了标签本身样式的限制了),但为了做的更好,我们设置用户可以自定义标签类型。

例如 <AccordionButton> 元素我们这样封装:

const AccordionButton = forwardRef(function (
  { children, as: Comp = "button", ...props }: AccordionButtonProps,
  forwardedRef
) {
  return (
    <Comp {...props} ref={forwardedRef} data-hb-accordion-button="">
      {children}
    </Comp>
  );
});

可以看到,上面有 as 属性,可以自定义标签类型。

其它组件也是类似:

const Accordion = forwardRef(function (
  { children, as: Comp = "div", ...props }: AccordionProps,
  forwardedRef
) {
  return (
    <Comp {...props} ref={forwardedRef} data-hb-accordion="">
      {children}
    </Comp>
  );
});

const AccordionItem = forwardRef(function (
  { children, as: Comp = "div", ...props }: AccordionItemProps,
  forwardedRef
) {
  return (
    <Comp {...props} ref={forwardedRef} data-hb-accordion-item="">
      {children}
    </Comp>
  );
});

const AccordionPanel = forwardRef(function (
  { children, as: Comp = "div", ...props }: AccordionPanelProps,
  forwardedRef
) {
  return (
    <Comp {...props} ref={forwardedRef} data-hb-accordion-panel="">
      {children}
    </Comp>
  );
});

需要说明如下:

  • Accordion 组件是由四部分组成
  • 每个部分都包装在 forwardRef 中,所有外界都可以获取到对应的 dom 元素的实例。
  • as 属性可以自定义渲染元素的是什么
  • data-* 属性用于在测试这个组件的时候,咱们设置一个独一无二的属性选择器,好让测试框架选中

组合起来,我们可以这样使用手风琴组件:

  <Accordion>
    <Accordion.Item>
      <Accordion.Button>Button 1</Accordion.Button>
      <Accordion.Panel>Panel 1</Accordion.Panel>
    </Accordion.Item>
    <Accordion.Item>
      <Accordion.Button>Button 2</Accordion.Button>
      <Accordion.Panel>Panel 2</Accordion.Panel>
    </Accordion.Item>
    <Accordion.Item>
      <Accordion.Button>Button 3</Accordion.Button>
      <Accordion.Panel>Panel 3</Accordion.Panel>
    </Accordion.Item>
  </Accordion>

到此,大家仔细看看,之前我们有个增加 Icon 的功能,其实我们只需要在<Accordion.Button> 中增加 Icon 组件就可以了。

  <Accordion.Button><自定义 Icon 组件 /> Button 1</Accordion.Button>

是不是很简单就拓展了 dom 原来的 Accordion 组件也完全不用改,这属于用户自定义行为。

当然这里有个坑我们后面解决,就是,每个 <Accordion.Item> 可能都需要一个 indexdisabled 参数,index 是告诉我这个面板的索引是多少,disabled是告诉这个面板是否是禁用状态。

而这两个参数,可能我们自定义的 Icon 组件需要,比如在 disabeld 状态下,样式会变化。所以这个悬念后面我们解决。

然后因为手风琴组件,大家要共享选中和关闭状态,例如有哪些面板被选中,这里我们使用 Context API 来实现。

const AccordionContext = createContext({});
const AccordionItemContext = createContext({});

const useAccordionContext = () => {
  const context = useContext(AccordionContext);
  if (!context) {
    throw Error("useAccordionContext must be used within Accordion.");
  }
  return context;
};
.......
const Accordion = forwardRef(function (
  .....  
  return (
    <AccordionContext.Provider value={{}}>
     .....
    </AccordionContext.Provider>
  );
});
  • 我们创建了一个上下文共享状态的 context: AccordionContext 以及对应的的 useContext 钩子:`useAccordionContext``

  • AccordionContext 用来共享全局的状态,例如关闭和打开哪个 <Accordion.Item>(面板) 组件的索引。

小总结

这个 AccordionContext 好处是,如果组件库导出了这个 context,那么你可以在这个里面自己添加任何组件,通过调用useAccordionContext, 就能共享手风琴里的所有共享信息,所以此时这个组件库已经超越了传统组件库不能定制样式和不能定制 dom 结构的问题了。

其实还需要一个 conext,例如 <Accordion.Item>(面板)可以单独传入 disabled 参数,表示是否禁用当前面板,所以在这个<Accordion.Item>下,我们如果自定义的组件,也需要共享到这个 disabled 状态,所以单独导出一个 <Accordion.Item> 共享的 context

在下面的案例里也会有这个 useAccordionItemContext,大家大概明白是什么意思就行。这篇文章主要是讲解 headless 组件构建思路。

到这里其实就解决之前的疑问,如何让面板共享状态给我们自定义的 Icon 组件,思想使用 Context API.

其实还有一种方式,就是使用 React.cloneElement 语法,这种方式也有其用武之地,但在这里明显不如 Context API 灵活,为啥呢,因为我们自定义 Icon,我们可以用 Context API 获取到共享状态,而 React.cloneElement 只能给组件中已知的,例如 <Accordion.Button> 传递状态。比如这样

const AccordionItem = forwardRef(function (
  { children, as: Comp = "div", disabled, ...props }: AccordionItemProps,
  forwardedRef
) {
  return (
    <Comp {...props} ref={forwardedRef} data-hb-accordion-item="">
     ```
      {React.Children.map(children, (child) => {
        if (React.isValidElement(child)) {
          return React.cloneElement(child, { 
            disabled
          });
        }
        return child;
      })}
    </Comp>
  );
});

缺点之前也说过了,这里不赘述,大家可以看做另一种方案。

我们接下来给组件增加一些功能。

继续深入:无状态组件和有状态组件

无状态组件典型的就是 <input> 元素,你输入任何值,它自己单独记录,你不用管。有状态组件就是你要单独自己设置 state 去管理,例如:

<input value={someValue} />

说白了,无状态组件,你不需要单独传参数去控制它最终显示的值,有状态组件就需要。

同时需要记住,一个组件要么是有状态组件,要么是无状态的,只能二选一。如果都有的话,那么就是有状态组件,无状态会被无视。

最后一个无状态组件的关键点是可以传入 defaultValue

<input defaultValue='John Doe' /> 

我们为了丰富之前手风琴组件的效果,支持传入如下参数:

  • index: 可选,类型是 number 或者 number 数组,代表当手风琴面板的索引, 应该跟 onChange 配合使用。

  • onChange: 可选,类型是函数,(index: number) => void,用法是当手风琴里的子元素打开或者关闭时触发此事件。

  • collapsible:可选,类型是 boolean,默认 false. 它决定了是否允许用户关闭所有面板,有些产品要求至少有一个面板是展开的,所以增加了这个参数。此参数仅对非受控组件(即没有 index 和 onChange 属性的组件)有效。在受控组件中,面板的打开与关闭状态完全由父组件通过 index 属性控制。

  • defaultIndex,可选,类型是 number 或者 number 数组,代表打开面板的索引默认值,如果 collapsible 设置为 true,没有设置 defaultIndex,那么所有面板初始化都是关闭的。否则,默认第一个面板打开。

  • multiple,可选,类型是 boolean,默认 false,在非受控组件的情况下,是否允许同时打开多个面板

  • readOnly,可选,类型是 boolean,默认 false,手风琴组件是否是可读状态,也就意味着用户是否可以切换面板状态。

改造组件如下:

const Accordion = forwardRef(function (
  {
    children,
    as: Comp = "div",
    defaultIndex,
    index: controlledIndex,
    onChange,
    multiple = false,
    readOnly = false,
    collapsible = false,
    ...props
  }: AccordionProps,
  forwardedRef
) {
....

const AccordionItem = forwardRef(function (
  {
    children,
    as: Comp = "div",
    disabled = false,
    ...props
  }: AccordionItemProps,
  forwardedRef
) {
....

增加无状态组件功能

涉及无状态组件功能参数包括:defaultIndexmultiplecollapsible, 但是不包括 indexonChange

然后我们也会给 AccordionItem 一个 index 参数,表示每个面板的索引

  <Accordion defaultIndex={[0, 1]} multiple collapsible>
    <Accordion.Item index={0}>   // <= index
      ....
    </Accordion.Item>
    <Accordion.Item index={1}>  // <= index
      ....
    </Accordion.Item>
      .... 
  </Accordion>

然后我们继续丰富组件内容,以下不是完整代码,主要是帮助大家快速理解 headless 组件构建思路。

const Accordion = (...) => {
  const [openPanels, setOpenPanels] = useState(() => {
    // 根据 multiple, collapsible 参数设置初始化展开哪些面板
  });

  const onAccordionItemClick = (index) => {
    setOpenPanels(prevOpenPanels => { // 更新面板展开或者关闭逻辑 })
  }

  const context = {
    openPanels,
    onAccordionItemClick
  };

  return (
    <AccordionContext.Provider value={context}>
     ....
    </AccordionContext.Provider>
  );
};

const AccordionItem = ({ index, ...props }) => {
  const { openPanels } = useAccordionContext();

  const state = openPanels.includes(index) ? 'open' : 'closed'

  const context = {
    index,
    state,
  };

  return (
    <AccordionItemContext.Provider value={context}>
        ....
    </AccordionItemContext.Provider>
  );
};

const AccordionButton = () => {
  const { onAccordionItemClick } = useAccordionContext();
  const { index } = useAccordionItemContext();

  const handleTriggerClick = () => {
    onAccordionItemClick(index);
  };

  return (
    <Comp
      ....
      onClick={handleTriggerClick}
    >
      {children}
    </Comp>
  );
};

const AccordionPanel = (...) => {
  const { state } = useAccordionItemContext();

  return (
    <Comp
      ....
      hidden={state === 'closed' }
    >
      {children}
    </Comp>
  );
});

增加有状态功能

有状态组件是很简单的,因为是用户自己传入参数来控制。我们增加 indexonChange 参数,从而让用户能更新内部状态。

const Accordion = forwardRef(function ({
+  index: controlledIndex,
+  onChange,
....
  const onAccordionItemClick = useCallback(
    (index: number) => {
+     onChange && onChange(index);

      setOpenPanels((prevOpenPanels) => {
       ...
  );

  const context = {
+    openPanels: controlledIndex ? controlledIndex : openPanels,
    .....
  };

如上,,受控状态 controlledIndex 按预期覆盖了 openPanels 中的非受控状态。(openPanels 是前面我们在Accordion组件内定义记录当前打开的是哪些面板的 state)。

关于 onChange,它并不决定我们的组件是受控还是非受控。无论是否传递了受控的 index 属性,都可以传递 onChangeonChange 属性的目的是向父组件通知状态变更。

其实到这里差不多就结束了,其实很多组件库,把无状态和有状态都合并为了一个 hooks 这样可以解决用条件去判断(如我们上面)的繁琐。

这个函数我的 t-ui组件库也有借鉴(求一个 start, 致力于打造最好的组件库教程网站 ,github地址)源码如下:

'use client';
import React, { useState, useEffect, useRef } from 'react';
import { isUndefined } from '../utils';
import { usePrevious } from './use-previous';

export function useMergeValue<T>(
  defaultStateValue: T,
  props?: {
    defaultValue?: T;
    value?: T;
  },
): [T, React.Dispatch<React.SetStateAction<T>>, T] {
  const { defaultValue, value } = props || {};
  const firstRenderRef = useRef(true);
  const prevPropsValue = usePrevious(props?.value);

  const [stateValue, setStateValue] = useState<T>(
    !isUndefined(value) ? value : !isUndefined(defaultValue) ? defaultValue : defaultStateValue,
  );

  // 受控转为非受控的时候,需要做转换处理
  useEffect(() => {
    if (firstRenderRef.current) {
      firstRenderRef.current = false;
      return;
    }
    if (value === undefined && prevPropsValue !== value) {
      setStateValue(value);
    }
  }, [value]);

  const mergedValue = isUndefined(value) ? stateValue : value;

  return [mergedValue, setStateValue, stateValue];
}

这里简单解释以下,其实就是你传了 value 我就认为你是受控组件,然后 value 就透传出去,我这个 hooks 不管,如果有 defaultValue 或者组件库想默认给个默认值, 我会用其初始化 stateValue 然后传出去。并且 setStateValue 方法能改变其值。

setStateValue 其实在传入 value 的情况下,也没什么用,因为改变不了 value 的值。

❌
❌