【React 进阶系列第一课】组件就是函数

组件就是函数

在 React 中,组件即函数(不讨论类组件)。 ------你当然知道,但你可能没意识到这意味着什么。

组件就是 JavaScript 函数

从语法上看,函数组件就是普通的 JavaScript 函数,它和平时写的其他函数没有什么区别。 React 只是规定了组件函数应该接收一个 props 参数,返回 React Element。

或许是先接触了 Vue 先入为主,或者是认为函数无法实现组件的生命周期等效果,许多初学者误以为,React 把我们写的函数进行层层封装转化,注入了许多魔法,让组件能够渲染 UI,具有生命周期等等。 或者误认为,函数组件 + hooks 是类组件的语法糖,React 会把它们转成类组件。

以上理解都是错误的。事实上,你写的函数就是你的组件,React 不会转化你的组件函数,也不会把它们转成类组件。React 只是执行它们,就像执行普通的 JavaScript 函数。

记住,React 是一个 JavaScript 库,除了与之搭配使用的 JSX 语法需要经过编译转化,其他代码都是直接运行的 JavaScript 代码,都需要遵守 JavaScript 的运行机制。

不要用类的思维模式理解函数组件

既然函数组件不会被转成类组件,那组件函数就是工厂函数,用于生产组件实例咯?比如,Button 是工厂函数,<Button /> 是组件实例?

这样理解也是不对的,组件函数的职责是定义视图,而不是生产组件实例。<Button /> 是一个 React Element,也不是组件实例。 在函数组件中,没有"实例"的概念。如果你强行这么理解,会陷入用类组件的思维模式理解函数组件的误区。 当 React 渲染时,就执行组件函数,拿到视图,并呈现它,这中间不需要什么实例的概念。

函数组件和类组件虽然能实现相同的功能,但是它们的实现方式是完全不同的。类组件有实例和生命周期的概念,但是函数组件并没有这些,它由一个定义视图的函数,和通过 hooks 实现的状态、副作用组成。

如果你之前是用类组件的思维模式理解函数组件,请你忘记它,只需要记住,组件就是一个函数。

组件的渲染和更新就是函数执行

组件即函数,函数自然就是用来执行的。 React 每次渲染(首次渲染和更新),都会执行组件函数。

比如,对这个 Timer 组件:

jsx 复制代码
function Timer() {
  const [sec, setSec] = useState(0);

  useEffect(() => {
    let timerId = setInterval(() => {
      setSec(sec + 1);
    }, 1000);
    return () => clearInterval(timerId);
  }, []);

  return <p>Seconds passed: {sec}</p>;
}

React 通过多次调用 Timer() 函数,来渲染和更新视图。

js 复制代码
Timer(); // 首次渲染执行,sec = 0
// 一秒后...
Timer(); // 组件更新再次执行,sec = 1
// ...

这里也可能存在一个误区:❌ 组件更新时,代码是"按需执行"的,只有涉及到更新的代码才会执行,这显然是错的。 即使是更新,也是执行整个函数,这是 JavaScript 的运行机制,React 可做不到只执行函数内的一部分代码。

利用函数特性

组件即函数,函数组件具有 JavaScript 函数的所有特性。 我们可以在代码中运用这些特性。

比如,JavaScript 函数可以赋值给变量,所以可以这么写:

jsx 复制代码
const iconMap = {
  win: IconWin,
  linux: IconLinux,
};

function OSIcon({ os }) {
  const Icon = iconMap[os];
  return <Icon />;
}

再比如,函数式编程中,函数是一等公民,可以作为参数传递,也可以作为返回值。 高阶组件(Higher-Order Components, HOC)就是这么玩的,高阶组件接收一个组件,返回一个新的组件(接收一个函数,返回一个新函数)。

jsx 复制代码
function WithStyle(Component, styles) {
  return function ComponentWithStyle(props) {
    return <Component {...props} style={styles} />;
  };
}

// LongButton 是一个新的组件
const LongButton = WithStyle(Button, { display: "block" });

关于高阶组件,这里不深入,不理解的可以参考 Higher-Order Components -- React

你也可以在函数组件中利用闭包、递归等特性。比如,用递归写一个树形结构的组件。

jsx 复制代码
function UnorderedList({ list }) {
  if (!list) return null;

  return (
    <ul>
      {list.map((item) => (
        <li key={item.id}>
          {item.value}
          <UnorderedList list={item.children} />
        </li>
      ))}
    </ul>
  );
}

函数的限制

反过来,函数组件也受到 JavaScript 函数的限制

看下面这段代码,你能发现问题吗?

jsx 复制代码
function Timer() {
  let timerId;
  const [sec, setSec] = useState(0);

  function startTimer() {
    timerId = setInterval(() => {
      setSec(sec + 1);
    }, 1000);
  }

  function stopTimer() {
    clearInterval(timerId);
  }

  return (
    <div>
      <p>Seconds passed: {sec}</p>
      <button onClick={startTimer}>开始计时</button>
      <button onClick={stopTimer}>停止计时</button>
    </div>
  );
}

上例中,计时器无法正常停止。 因为当计时开始之后,setSec(sec + 1) 触发组件的更新。根据上面提到的,组件每次渲染,都会执行一次函数。

js 复制代码
Timer(); // 首次渲染执行
// 一秒后
Timer(); // setSec 触发 React 对组件进行更新再次执行

因为timerId是组件内声明的变量,每次 Timer() 执行,都会创建一个新的 timerId 变量(值为undefined)。所以,stopTimer 无法正确清除计时器。

这几乎是所有 React 初学者有过的经历,组件内的变量明明被赋值了,但却又变回了初始值。 然后,他们被告知组件内的变量每次渲染都会重新创建和计算,困惑于 React 为什么设计得这么奇怪。

但事实是,这不是有意设计的,而是受限于 JavaScript 的运行机制,React 无法在多次函数执行中复用变量。

甚至不单是变量 timerId,每次执行,也重新声明了 startTimer/stopTimer 函数。 执行完成后,这些函数内的局部变量又被 JavaScript 引擎回收,在下次执行时重新创建...

正是受限于函数运行机制,在引入 hooks 之前(16.8),函数组件只能写无状态组件,有状态组件必须用 class 组件。 (Hooks 让函数组件可以有状态,是一种非常巧妙的设计,后面讲 hooks 的时候再细聊)

组件函数是数据到视图的映射

组件函数接收一个 props 参数,返回一段描述 UI 的 JSX。 把 props 理解成数据,JSX 理解成视图,那组件就是把数据映射成视图的函数。

组件的状态也是数据,不过来源不同,props 是从父组件传递过来的,而状态是组件自己维护的。

由此,我们可以得到一个对组件的抽象理解:

ini 复制代码
UI = FC(props, state)

等等,state 怎么是输入?是的,如果你从函数的角度出发,那 state 就是输入,它是由组件的上一帧传递过来的。

理解这个有什么用呢?从我的个人经历来看,这能让我更轻松得设计和实现组件。 因为我认识到我其实是在写一个类似 dataToUI(props, state) 函数,我只需要把数据转成 UI 即可。

另外,从这点出发,我们不该直接在组件函数内做一些附带操作(副作用),比如,发起网络请求,修改 DOM 等,这些应该通过 useEffect 实现。

renderXxx 函数 与 组件函数

比较这 3 种写法,它们有什么区别吗?

jsx 复制代码
// 写法1
function Post(props) {
  return (
    <div>
      <h1>{props.title}</h1>
      <p>{props.content}</p>
    </div>
  );
}

// 写法2
function Post(props) {
  function renderTitle() {
    return <h1>{props.title}</h1>;
  }
  function renderContent() {
    return <p>{props.content}</p>;
  }
  return (
    <div>
      {renderTitle()}
      {renderContent()}
    </div>
  );
}

// 写法3
function Post(props) {
  function Title() {
    return <h1>{props.title}</h1>;
  }
  function Content() {
    return <p>{props.content}</p>;
  }
  return (
    <div>
      <Title />
      <Content />
    </div>
  );
}

1 和 2 是等效的,从函数机制来说,它们必须是一样的。不过,写法 2 把渲染逻辑拆分到多个函数中,在复杂组件中可读性更好,也方便进行函数复用。

也就是说,renderXxx 函数是一种代码的组织方式而已,是不同于函数组件的。 我们自己写这个函数,自己调用它,React 不感知它,不能用 <renderXxx /> 的语法使用它,它内部也不能使用 hooks。

那换个问题,写法 3 中,<Title /> 可以写成 Title() 吗?------可以,这就变回写法 2 了,函数命名并不影响什么。当然,如果 Title 组件内使用了 hooks,就只能用 <Title /> 了。

那说回来,哪种是推荐写法呢?写法 2 肯定是没有问题的。而写法 3 这种嵌套组件声明的写法是应该尽量避免的,因为内层组件会发生状态无法保持的情况,容易导致 bug。

不过,可以的话,组件的写法当然是最推荐的。

jsx 复制代码
function Post(props) {
  return (
    <div>
      <Title title={props.title} />
      <Content content={props.content} />
    </div>
  );
}
// 把组件提到最外层
function Title(props) {
  return <h1>{props.title}</h1>;
}

function Content(props) {
  return <p>{props.content}</p>;
}

把组件提到最外层了,但相应的,没法直接读取外层组件的变量了,需要手动传递 props。

最后,renderXxx 函数最常见的常见,还是把它作为 prop,让外层组件来决定如何渲染。

总结

  • 组件语法上是 JavaScript 函数。
  • React 不会改变你的组件函数。
  • 组件每次渲染,都会执行组件函数。
  • 可以在代码中运用函数的特性编写组件。
  • 按 JavaScript 运行机制,每次更新,组件内的变量/函数都会重新创建,结束后被回收。
  • renderXxx 是一种代码组织方式,不是组件,内部不能使用 hooks。
相关推荐
沿着路走到底16 分钟前
JS事件循环
java·前端·javascript
子春一234 分钟前
Flutter 2025 可访问性(Accessibility)工程体系:从合规达标到包容设计,打造人人可用的数字产品
前端·javascript·flutter
白兰地空瓶41 分钟前
别再只会调 API 了!LangChain.js 才是前端 AI 工程化的真正起点
前端·langchain
jlspcsdn2 小时前
20251222项目练习
前端·javascript·html
行走的陀螺仪2 小时前
Sass 详细指南
前端·css·rust·sass
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ2 小时前
React 怎么区分导入的是组件还是函数,或者是对象
前端·react.js·前端框架
LYFlied2 小时前
【每日算法】LeetCode 136. 只出现一次的数字
前端·算法·leetcode·面试·职场和发展
子春一22 小时前
Flutter 2025 国际化与本地化工程体系:从多语言支持到文化适配,打造真正全球化的应用
前端·flutter
前端无涯2 小时前
React/Vue 代理配置全攻略:Vite 与 Webpack 实战指南
vue.js·react.js
QT 小鲜肉3 小时前
【Linux命令大全】001.文件管理之file命令(实操篇)
linux·运维·前端·网络·chrome·笔记