【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。
相关推荐
Book_熬夜!10 分钟前
CSS—补充:CSS计数器、单位、@media媒体查询
前端·css·html·媒体
几度泥的菜花1 小时前
如何禁用移动端页面的多点触控和手势缩放
前端·javascript
狼性书生1 小时前
electron + vue3 + vite 渲染进程到主进程的双向通信
前端·javascript·electron
肥肠可耐的西西公主1 小时前
前端(AJAX)学习笔记(CLASS 4):进阶
前端·笔记·学习
拉不动的猪1 小时前
Node.js(Express)
前端·javascript·面试
Re.不晚2 小时前
Web前端开发——HTML基础下
前端·javascript·html
几何心凉2 小时前
如何处理前端表单验证,确保用户输入合法?
前端·css·前端框架
浪遏2 小时前
面试官😏: 讲一下事件循环 ,顺便做道题🤪
前端·面试
Joeysoda2 小时前
JavaEE进阶(2) Spring Web MVC: Session 和 Cookie
java·前端·网络·spring·java-ee
小周同学:3 小时前
npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。
前端·npm·node.js