Skip to content

组件初识与类组件(Day3 1/3) ​

本页对应 React + TypeScript + 医通项目,共 6 段视频,约 48 分钟。

学习目标 ​

  • 能手工模拟实现一个简易 render 函数:接收 JSX 创建的 React 元素对象,读取其 type 与 props.children,将虚拟 DOM 转换为真实 DOM 并挂载到指定根节点,同时说明类组件与函数组件在该过程中的处理差异。
  • 能准确说明 React 脚手架项目根目录下 public、node_modules 与 src 的作用,并指出 index.html、index.js、App.js、index.css、App.css、favicon.ico、manifest.json、robots.txt 等文件的用途。
  • 能解释 index.js 作为 JS 入口文件如何导入根组件 App、如何通过 ReactDOM 创建根节点并完成渲染,并能说明关闭 StrictMode 后控制台输出次数由两次变为一次的原因。
  • 能在 src/component 目录下新建类组件文件,使用 class Xxx extends Component 定义类组件并默认导出,在 App 根组件中导入并以组件标签形式调用,且能借助 React DevTools 的 Components 面板验证组件层级关系。
  • 能使用 rcc / rfc 代码片段分别快速生成类组件与函数组件骨架,并独立完成新建组件、默认导出、在父组件中导入调用的完整流程。
  • 能通过替换 src 目录的方式运行此前备份的旧代码,并能在类组件的 constructor 中通过 this.state 定义多个状态数据,在 render 中通过 this.state.xxx 或解构赋值读取并渲染这些数据。

知识点 ​

组件与 React 组件的组成 ​

为什么需要:页面越做越大,不能把所有 HTML、JS、CSS 都堆在一个文件里;组件就是把整体拆成可复用、可维护的部件。怎么想:先把它理解成“页面的一块”,再记住 React 组件不只是 HTML。怎么做:本节笔记给的定义是:组件是组成整体的一个部件;React 中的组件是 <font color='red'>jsx(html结构 + js 逻辑) + css</font> + 静态资源【图片、视频、字体】。易错点:组件不是单纯函数或单纯标签,它最终要返回 JSX 结构;组件名通常首字母大写,使用时写成组件标签。

类组件与函数组件的最小写法 ​

为什么需要:React 有两种组件写法,先能各写一个最小组件,后面才能谈调用和状态。怎么想:类组件用 class 继承 Component,必须有 render() 并返回 JSX;函数组件就是函数返回 JSX。怎么做:

jsx
// components/HeaderClass.jsx
import React, { Component } from 'react';

class HeaderClass extends Component {
  render() {
    return <header>HeaderClass 组件</header>;
  }
}
export default HeaderClass;
jsx
// components/MeFun.jsx
function MeFun() {
  return <div>MeFun 组件</div>;
}
export default MeFun;

转写稿中强调:组件名最好与文件名一致,这样 VS Code 输入组件名时更容易自动导入;不一致时可能没有提示,只能手动写导入路径。易错点:类组件别漏 render 的 return;函数组件别漏默认导出;组件文件可用 .jsx 后缀,便于识别,但不是强制。

用简化 render 推导组件如何变成真实 DOM ​

为什么需要:不理解“组件标签调用后到底是什么”,就容易死记。怎么想:转写稿用 console.log 观察:组件标签调用后得到 React 元素对象,里面有 type;类组件的 type 是类本身,函数组件的 type 是函数。怎么做:课堂用简化模拟说明:类组件先 new type() 实例化,再调用 render() 得到 vdom;函数组件直接 type() 得到 vdom;再根据 vdom.type 创建真实 DOM,把 vdom.props.children 赋给 innerHTML,最后 appendChild 到 root。

js
// 课堂简化模拟,不是 React 源码
function render(vdom, root) {
  let childVdom;
  if (vdom.type.prototype && vdom.type.prototype.render) {
    const instance = new vdom.type();
    childVdom = instance.render();
  } else {
    childVdom = vdom.type();
  }
  const dom = document.createElement(childVdom.type);
  dom.innerHTML = childVdom.props.children;
  root.appendChild(dom);
}

易错点:这是帮助理解的简化过程,不要背成 React 源码;真实 React 内部更复杂。调试时给打印加标识,如 console.log('vdom', vdom),否则输出多了分不清。

项目目录与入口文件 ​

为什么需要:React 工程文件多,先知道谁入口、谁放代码。怎么想:public 放静态资源和 HTML 模板,src 才是开发目录。怎么做:public 下有 index.html、favicon.ico、logo192.png、logo512.png、manifest.json、robots.txt;src 下有 App.jsx(根组件)、App.css、index.js(JS 入口)、index.css、logo.svg、reportWebVitals.js、测试文件等。入口大致是:

jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />); // 课堂建议先去掉 StrictMode 包裹,便于调试

易错点:npm start 必须在项目根目录执行,否则会报 missing script start;StrictMode 下 console.log 可能输出两次,不是组件写错,课堂为调试方便会先关闭。robots.txt 是爬虫相关文件,reportWebVitals.js 是报告文件,初学先认识即可。

在 App 根组件中调用组件 ​

为什么需要:组件定义后要在根组件里组合成页面。怎么想:App 是根组件,其他组件作为它的子组件导入并使用。怎么做:

jsx
import HeaderClass from './components/HeaderClass';
import MeFun from './components/MeFun';

function App() {
  return (
    <div>
      <HeaderClass />
      <MeFun />
    </div>
  );
}
export default App;

转写稿还讲了快捷生成:输入 rcc 生成类组件骨架,输入 rfc 生成函数组件骨架,依赖 React 语法提示插件。易错点:导入路径要写对;组件使用要写成标签;没有自动提示时可重启 VS Code 或手动导入。

类组件状态的定义与读取 ​

为什么需要:组件上要保存会参与渲染的数据,例如数量、消息。怎么想:本节先讲类组件的 state,它是类组件上的特殊属性。怎么做:在 constructor 中先 super(),再写 this.state:

jsx
import React, { Component } from 'react';

class App extends Component {
  constructor() {
    super();
    this.state = {
      count: 100,
      msg: 'atguigu'
    };
  }

  render() {
    const { count, msg } = this.state;
    return <div>{count} - {msg}</div>;
  }
}
export default App;

读取时可用 this.state.count、this.state.msg,也可在 render 中解构后直接写 count、msg。易错点:super() 之前不能用 this;state 是对象;render 里的 this 指向当前实例,所以能读 this.state。转写稿中“quant/qu/框/c”应纠正为 count。本节先讲类组件状态,函数组件的状态管理(Hooks)后续再学,不要误以为函数组件永远不能有状态。

类组件要解决什么问题 ​

为什么需要:在 React 里,我们不想把一整页结构写死,而是希望把头部、内容、底部拆成可复用的“组件”。类组件就是早期 React 中最常用的组件写法,本节先把“怎么定义、怎么调用、render 怎么执行”搞清楚。
怎么想:把类组件理解成“一个类 + 一个 render 方法”。类名就是组件名,render 负责返回要显示的 JSX。
怎么做:用 class 定义类,继承 React.Component,在 render 中 return React 元素。
易错点:类名首字母必须大写;必须继承 React.Component;必须有 render;render 中要返回 React 元素,不能随便返回普通字符串或忘记 return。

类组件的定义规则 ​

为什么需要:React 要通过类名识别组件,通过继承获得组件能力,通过 render 拿到界面结构。
怎么想:定义类组件时,先把“组件名”和“类名”统一起来,再让 render 只负责返回 JSX。
怎么做:笔记中的规则是:本质是一个类,使用 class 定义;类名首字母必须大写,类名就是组件名;必须继承 React.Component;必须有 render 方法;render 方法中必须【99.99%】返回 React 元素【JSX】。
易错点:类名小写时,JSX 会把它当普通标签处理;忘记继承会导致 render、this 等机制不成立;render 里写了 JSX 但忘记 return,页面不会出现内容。

调用类组件与 render 执行过程 ​

为什么需要:定义完组件只是“声明”,还要在 JSX 中调用,React 才知道在哪里渲染。
怎么想:调用类组件就是写组件标签:单标签 <组件名/>,对标签 <组件名></组件名>。React 看到首字母大写的 JSX 标签,会按组件处理。
怎么做:调用过程可以按转写稿里的模拟思路理解:root.render() 接收到的 JSX 会变成 React 元素对象,其中 type 指向我们定义的类;React 发现它是类组件后,会实例化该类,并用实例对象调用 render;render 返回的 JSX 再被转换成真实 DOM,替换到组件标签所在位置。转写稿中通过 console.log 打印 React 元素对象、type、render 返回值,就是为了看清这条链路:JSX → React 元素对象 → 实例化类 → 调用 render → 得到返回的 React 元素 → 渲染成真实 DOM。
易错点:只有首字母大写的标签才会被当成组件;render 方法中的 this 永远指向该类组件的实例对象,调试时可以打印 this 确认,但不要误以为它指向别处。

完整示例代码保留如下:

jsx
const root = ReactDOM.createRoot(document.querySelector("#root"));

// 定义Header类组件
class Header extends React.Component{
    render(){
        // 类组件render方法,永远是当前类组件的实例对象调用的
        console.log('header render this: ' ,this);
        return (
            <div className="header">我是头部</div>
        )
    }
}
class Main extends React.Component{
    render(){
        return (
            <div className="main">我是内容</div>
        )
    }
}
class Footer extends React.Component{
    render(){
        return (
            <div className="footer">我是底部</div>
        )
    }
}
root.render((
    <>
        <Header></Header>
        <Header/>
        <Main></Main>
        <Footer></Footer>
    </>
))

在项目中创建并调用类组件 ​

为什么需要:真实项目里组件要拆文件管理,不能都堆在入口文件里。
怎么想:先建 component 目录存放组件;组件文件名和组件名尽量一致,方便导入和编辑器提示;组件文件用 .jsx 更能表明它返回 JSX。
怎么做:转写稿中创建 HeaderClass 类组件,在 App 根组件里导入并写成 <HeaderClass /> 调用;也可以用 rcc 快速生成类组件、rfc 快速生成函数组件。调试时打开 React 开发者工具的 Components 面板,刷新后能看到 App 与子组件的结构。
易错点:导入路径写错;组件名和文件名不一致导致自动导入提示不出来;改完代码忘记刷新或保存;把组件当普通标签小写调用。

调试与易错点清单 ​

为什么需要:类组件最容易糊涂的是“谁调用了 render、this 是谁、返回值去了哪里”。
怎么想:用 console.log 分层打印:先打印 JSX 调用后得到的 React 元素对象,再打印 render 里的 this,最后看页面是否替换成功。转写稿反复强调打印标识,就是为了让对象结构可读。
怎么做:在 Header 的 render 中保留 console.log('header render this: ', this),观察输出是否为 Header 实例;再对照页面结构确认 <Header></Header> 和 <Header/> 都能调用。
易错点:不要把 render 里的 this 当成全局对象;不要在 JSX 中把组件名写成小写;不要漏掉 return;不要忘记类组件必须继承 React.Component。本节先把定义、调用和 render 执行过程打牢,后续再展开类组件状态等内容。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:类组件的定义与调用(笔记原码,可直接运行) ​

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>12-1 类组件</title>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  <style>
    .header { height: 60px; background: #f5a; color: #fff; line-height: 60px; text-align: center; }
    .main   { height: 200px; background: #eee; line-height: 200px; text-align: center; }
    .footer { height: 60px; background: #333; color: #fff; line-height: 60px; text-align: center; }
  </style>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel">
    const root = ReactDOM.createRoot(document.querySelector("#root"));

    // 定义Header类组件
    class Header extends React.Component {
      render() {
        // 类组件render方法,永远是当前类组件的实例对象调用的
        console.log('header render this: ', this);
        return (
          <div className="header">我是头部</div>
        );
      }
    }

    class Main extends React.Component {
      render() {
        return (
          <div className="main">我是内容</div>
        );
      }
    }

    class Footer extends React.Component {
      render() {
        return (
          <div className="footer">我是底部</div>
        );
      }
    }

    root.render((
      <>
        <Header></Header>
        <Header />
        <Main></Main>
        <Footer></Footer>
      </>
    ));
  </script>
</body>
</html>

要点解释 / 易错点

  • 类名首字母必须大写,类名就是组件名;必须 extends React.Component,必须写 render(),render 里必须 return 一个 React 元素(JSX)。
  • 调用只能用 JSX:<Header/> 或 <Header></Header>;单标签写法结尾的 / 不能丢。
  • 首字母大写的 JSX 标签才会被当成组件处理;小写会被当成原生 DOM 标签。
  • render 中的 this 永远指向该类组件的实例对象,控制台打印可验证。
  • 根节点用 React 18 的 ReactDOM.createRoot(...),不是老的 ReactDOM.render;多个同级标签外面需要 <>...</> 包裹。

示例二:手写 mini render,模拟“类组件 / 函数组件”被渲染的过程(转写稿 08:26) ​

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>模拟 render 函数</title>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel">
    // ===== 类组件 =====
    class Header extends React.Component {
      render() {
        return <div className="header">我是头部</div>;
      }
    }

    // ===== 函数组件 =====
    function Main() {
      return <div className="main">我是内容</div>;
    }

    // ===== 手写 mini render,模拟 ReactDOM 的渲染过程 =====
    function miniRender(vdom, root) {
      // 1. 组件标签调用之后到底是个啥?——React 元素对象(虚拟 DOM 对象)
      console.log('组件标签调用之后得到:', vdom);
      // 2. 这个元素身上的 type,恰好就是我们定义的组件本身
      console.log('这个元素的 type 是:', vdom.type);

      // 3. 判断 type 是类组件还是函数组件
      const isClass = vdom.type.prototype && vdom.type.prototype.isReactComponent;

      // 4. 类组件:new 实例化后,用实例调用 render 拿到返回值;
      //    函数组件:直接加括号调用
      const realVdom = isClass ? new vdom.type().render() : vdom.type();
      console.log('render 的返回值(真正要渲染的 vdom):', realVdom);

      // 5. vdom → 真实 DOM
      const dom = document.createElement(realVdom.type); // 标签名从 vdom.type 取
      dom.innerHTML = realVdom.props.children;           // 内容从 props.children 取
      root.appendChild(dom);                             // 塞进 root 里
    }

    const root = document.querySelector('#root');
    miniRender(<Header />, root); // 类组件
    miniRender(<Main />, root);   // 函数组件
  </script>
</body>
</html>

要点解释 / 易错点

  • JSX 就是创建 React 元素(等价于 React.createElement),它返回的是一个对象(虚拟 DOM 对象),不是真实 DOM。
  • 类组件标签调用后得到的对象只有 type 有用(props 为空),type 就是那个类;所以要先 new vdom.type() 实例化,再 实例.render()。
  • 函数组件不用 new,直接 vdom.type() 加括号调用即可,后续步骤与类组件完全一样。
  • 真实 DOM 的标签名来自 vdom.type,内容来自 vdom.props.children,最后 appendChild 到 root——这三步就是“渲染”的本质。
  • 易错点:忘记 new 直接调用类,或对函数组件用 new,都会报错。

示例三:类组件 state 的定义与读取(转写稿 08:16) ​

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>类组件 state</title>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel">
    // 只有类组件才能定义状态数据
    class App extends React.Component {
      constructor() {
        super();            // 必须先调用父类构造器
        this.state = {      // 特殊属性名:state
          count: 100,
          msg: 'atguigu'
        };
      }

      render() {
        // 解构,避免每次都写 this.state.xxx
        let { count, msg } = this.state;

        return (
          <div>
            <p>直接读:{this.state.count} --- {this.state.msg}</p>
            <p>解构读:{count} --- {msg}</p>
          </div>
        );
      }
    }

    const root = ReactDOM.createRoot(document.querySelector('#root'));
    root.render(<App />);
  </script>
</body>
</html>

要点解释 / 易错点

  • 状态数据就是给类加一个特殊名字的属性 state,值是一个对象,可以放多条数据。
  • 用 constructor 定义属性时,第一行必须 super() 调用父类构造函数,super() 之后才能用 this;漏写 super() 会直接报错。
  • 读取状态:render 里的 this 指向当前实例对象,所以 this.state.count 就能读到。
  • 解构 let { count, msg } = this.state; 之后模板里直接写 count、msg,比每次写长串更清爽。
  • 易错点:把 state 写成 State、在 super() 之前访问 this、在 render 里用 this.state = ... 去改数据(本节只讲定义与读取,修改下一节再讲)。

常见错误 ​

  1. 调用时写成小写标签 <header /> 现象:页面上出现一个空的 HTML header 元素,组件内容不显示,控制台也无报错。 原因:JSX 只有首字母大写才会被当作组件解析,小写一律当原生标签。 修法:组件名与调用标签都用大写的 <Header />。

  2. 类组件没继承 React.Component,或忘了写 render 现象:报 render is not a function / Element type is invalid,页面白屏。 原因:缺继承就没有 render 机制;缺 render 方法时实例化后无方法可调。 修法:class X extends React.Component { render(){ return (...) } },两样缺一不可。

  3. render 里的 this 变成 undefined 现象:Cannot read properties of undefined (reading 'state')。 原因:把 render 内的逻辑抽成普通函数、或作为回调传出去,调用时丢失了实例上下文。 修法:抽出的方法用箭头函数定义(class fields),或在 constructor 里 bind(this);记住 render 的 this 由实例调用,别手动剥离。

  4. 组件文件没默认导出,或导入路径写错 现象:导入得到 undefined,渲染时报 Element type is invalid。 原因:只写了 class X {} 没 export default X;或路径少一层 ./components/。 修法:文件末尾 export default 组件名;导入时用 ./ 相对路径,并核对目录层级。

  5. 文件名与组件名不一致 现象:输入组件名没有自动导入提示,只能手写 import,且经常导错文件。 原因:编辑器的自动导入依赖"文件名 ≈ 组件名"的匹配。 修法:Header.jsx 里就叫 Header;组件统一放 src/components,组件文件用 .jsx 后缀。

  6. constructor 里没先调 super() 就写 this.state 现象:报 Must call super constructor ... before accessing 'this'。 原因:子类构造器必须先调用父类构造器,this 才被初始化。 修法:constructor(){ super(); this.state = {...} },this.state 一律写在 super() 之后。

  7. 解构后仍混用 this.state.msg 和 msg 现象:解构了却仍写 this.msg,渲染出 undefined 或空白。 原因:解构出来的是局部变量,不是实例属性,this 上没有这个字段。 修法:解构后统一用局部变量名,或干脆不解构全程 this.state.xxx,二选一,别混。

  8. 开启 StrictMode 后误判"组件渲染了两次" 现象:console.log 打印两次,怀疑逻辑写错或死循环。 原因:开发态严格模式会故意双调用以暴露副作用。 修法:学习调试阶段先去掉 <React.StrictMode> 包裹;理解这是开发态行为,不是 bug。

  9. 在错误的目录执行 npm start 现象:npm ERR! Missing script: "start"。 原因:当前终端不在项目根目录(package.json 所在处)。 修法:先 cd 到项目根目录再启动;看到这个报错第一反应是查路径,而不是查脚本。

练习 ​

  1. 在 src 下新建 components 目录,创建 Header.jsx(类组件)、Main.jsx(函数组件)、Footer.jsx(类组件),并在 App.jsx 中依次调用。用 React DevTools 的 Components 面板确认三者的父子层级关系。

  2. 分别用 rcc 和 rfc 快捷指令各生成一个组件,观察生成代码的差异;再故意把其中一个文件的组件名改成与文件名不同,看自动导入提示是否消失。

  3. 把 App 改造成类组件,用 constructor 方式定义 count = 100、msg = 'atguigu' 两条状态,然后在 render 中分别用「直接 this.state.xxx」和「解构后使用」两种写法渲染,确保输出一致。

  4. 找错:下面代码有 3 处会导致运行失败或渲染异常,请指出并改正。

    jsx
    class header extends Component &#123;
      render() &#123; return &lt;div&gt;头部&lt;/div&gt; &#125;
    &#125;
    // App.jsx
    import Header from './components/header'
    root.render(&lt;header /&gt;)
  5. 在 App 的 render 里加一行 console.log('app render'),分别在有 / 无 <React.StrictMode> 包裹时刷新页面,记录打印次数并说明原因。

原视频对照 ​

#视频时长
1答疑-模拟render函数08:26
2react项目目录及文件介绍21:01
3创建一个类组件并在App根组件中调用06:01
4rcc-rfc03:45
5如何运行之前src目录的代码01:02
6类组件状态数据的定义及读取08:16

小结 ​

  • 组件名、文件名、调用标签三者统一大写且同名,是省掉一半低级错误的性价比最高的习惯。
  • 类组件的三件套(继承、render、默认导出)缺任何一件,报错信息都不会直说"你漏了",要学会从 Element type is invalid、render is not a function 反推。
  • this 在类组件里不是"大概指向实例",而是"必须由实例调用"——一旦把方法当值传出去,上下文就断了。
  • 状态三问(定义 / 读取 / 修改)要按顺序学,先能稳定读出 this.state 再谈改,读取阶段解构只是语法糖,别把它当成新 API。
  • 严格模式下的双次打印是开发态特性,不是渲染 bug;调试期关掉它,比对着重复日志猜半天更省时间。
  • 项目里 90% 的文件(favicon、manifest、robots、reportWebVitals、测试文件)可以长期不看,注意力集中在 src 下的 index.js、App 与 components。
  • 报错先查三样:路径对不对、导没导出、大小写对不对——这三样能覆盖入门阶段绝大多数"玄学"问题。