主题
组件初识与类组件(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 执行过程打牢,后续再展开类组件状态等内容。
课堂画面






代码示例
示例一:类组件的定义与调用(笔记原码,可直接运行)
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 = ...去改数据(本节只讲定义与读取,修改下一节再讲)。
常见错误
调用时写成小写标签
<header />现象:页面上出现一个空的 HTML header 元素,组件内容不显示,控制台也无报错。 原因:JSX 只有首字母大写才会被当作组件解析,小写一律当原生标签。 修法:组件名与调用标签都用大写的<Header />。类组件没继承
React.Component,或忘了写render现象:报render is not a function/Element type is invalid,页面白屏。 原因:缺继承就没有 render 机制;缺 render 方法时实例化后无方法可调。 修法:class X extends React.Component { render(){ return (...) } },两样缺一不可。render里的this变成undefined现象:Cannot read properties of undefined (reading 'state')。 原因:把 render 内的逻辑抽成普通函数、或作为回调传出去,调用时丢失了实例上下文。 修法:抽出的方法用箭头函数定义(class fields),或在 constructor 里bind(this);记住 render 的 this 由实例调用,别手动剥离。组件文件没默认导出,或导入路径写错 现象:导入得到
undefined,渲染时报Element type is invalid。 原因:只写了class X {}没export default X;或路径少一层./components/。 修法:文件末尾export default 组件名;导入时用./相对路径,并核对目录层级。文件名与组件名不一致 现象:输入组件名没有自动导入提示,只能手写 import,且经常导错文件。 原因:编辑器的自动导入依赖"文件名 ≈ 组件名"的匹配。 修法:
Header.jsx里就叫Header;组件统一放src/components,组件文件用.jsx后缀。constructor里没先调super()就写this.state现象:报Must call super constructor ... before accessing 'this'。 原因:子类构造器必须先调用父类构造器,this才被初始化。 修法:constructor(){ super(); this.state = {...} },this.state一律写在super()之后。解构后仍混用
this.state.msg和msg现象:解构了却仍写this.msg,渲染出undefined或空白。 原因:解构出来的是局部变量,不是实例属性,this上没有这个字段。 修法:解构后统一用局部变量名,或干脆不解构全程this.state.xxx,二选一,别混。开启
StrictMode后误判"组件渲染了两次" 现象:console.log打印两次,怀疑逻辑写错或死循环。 原因:开发态严格模式会故意双调用以暴露副作用。 修法:学习调试阶段先去掉<React.StrictMode>包裹;理解这是开发态行为,不是 bug。在错误的目录执行
npm start现象:npm ERR! Missing script: "start"。 原因:当前终端不在项目根目录(package.json所在处)。 修法:先cd到项目根目录再启动;看到这个报错第一反应是查路径,而不是查脚本。
练习
在
src下新建components目录,创建Header.jsx(类组件)、Main.jsx(函数组件)、Footer.jsx(类组件),并在App.jsx中依次调用。用 React DevTools 的 Components 面板确认三者的父子层级关系。分别用
rcc和rfc快捷指令各生成一个组件,观察生成代码的差异;再故意把其中一个文件的组件名改成与文件名不同,看自动导入提示是否消失。把
App改造成类组件,用 constructor 方式定义count = 100、msg = 'atguigu'两条状态,然后在 render 中分别用「直接this.state.xxx」和「解构后使用」两种写法渲染,确保输出一致。找错:下面代码有 3 处会导致运行失败或渲染异常,请指出并改正。
jsxclass header extends Component { render() { return <div>头部</div> } } // App.jsx import Header from './components/header' root.render(<header />)在
App的 render 里加一行console.log('app render'),分别在有 / 无<React.StrictMode>包裹时刷新页面,记录打印次数并说明原因。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 答疑-模拟render函数 | 08:26 |
| 2 | react项目目录及文件介绍 | 21:01 |
| 3 | 创建一个类组件并在App根组件中调用 | 06:01 |
| 4 | rcc-rfc | 03: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。 - 报错先查三样:路径对不对、导没导出、大小写对不对——这三样能覆盖入门阶段绝大多数"玄学"问题。