Skip to content

类组件与函数组件(Day3 2/3) ​

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

学习目标 ​

  • 能够解释类组件状态数据为什么必须定义在 state 上,并通过对比演示证明直接修改普通实例属性不会触发组件重新渲染。
  • 能够使用构造函数赋值和类字段直接赋值两种方式定义 state,并说明第二种方式的简洁性。
  • 能够判断类组件事件回调中普通方法与箭头函数的 this 指向,并使用 bind、箭头函数包裹或类字段箭头函数解决 this 丢失问题。
  • 能够使用 setState 修改状态数据,并验证修改后会触发 render 重新执行、页面同步更新。
  • 能够在父子类组件之间通过标签属性传递数据,并在子组件中通过 this.props 接收、解构和使用外部数据。

知识点 ​

先复习类:类组件的“地基” ​

为什么需要:类组件的本质就是一个类。如果对“属性定义在哪、方法定义在哪、this 由谁决定”没有感觉,后面 render 里的 this、state、事件回调的 this 都会糊在一起。

怎么想:类里定义属性有两种写法——在 constructor 里写 this.xxx = ...,或者直接在类体里赋值 xxx = ...;定义方法也有两种——普通方法(挂在原型对象上),箭头函数赋值(挂在实例上,this 在“出生”时就绑定好)。

怎么做:转写稿里用一个 Person 类做了对照:

js
class Person {
  name = '艾硅谷';
  age = 10;
  say() { console.log(this.name, this.age); }
  eat = () => { console.log(this.name); };
}
const p1 = new Person();
p1.say();
p1.eat();

调试过程:再 new 一个 p2,比较 p1.say === p2.say,结果是 true,说明 say 定义在原型对象上,所有实例共用;比较 p1.eat === p2.eat,结果是 false,说明 eat 是每个实例自己的。再把 p1.say 赋给 obj.f1、p1.eat 赋给 obj.f2 去调用:f1 里的 this 变成了 obj(普通方法的 this 由调用者决定);f2 里的 this 仍然是 p1(箭头函数没有自己的 this,它在 constructor 执行时就绑定了实例)。

易错点:不要背“方法里的 this 指向实例”。普通方法的 this 看调用者;箭头函数的 this 看定义位置。

定义类组件的五条规则 ​

为什么需要:JSX 里标签那么多,React 得有一套规则判断“这是组件还是普通 HTML 标签”,类组件本身也得拿到 React 给的能力(state、setState、props 等)。

怎么想:把笔记五条拆成三组——命名(class 定义、首字母大写)、继承(extends React.Component)、渲染(必须有 render,render 返回 jsx)。

怎么做:按笔记逐条落地:

  1. 本质就是一个类,使用 class 关键字定义;
  2. 类名首字母必须大写,类名就是组件名;
  3. 类组件必须继承 React.Component 类;
  4. 类组件中必须有 render 方法;
  5. render 方法中必须(99.99%)返回 React 元素【jsx】。
jsx
class Header extends React.Component{
    render(){
        // 类组件render方法,永远是当前类组件的实例对象调用的
        console.log('header render this: ' ,this);
        return (
            <div className="header">我是头部</div>
        )
    }
}

易错点:

  • 类名小写:class header 在 JSX 里写成 <header/> 会被当成 HTML 的 header 标签,不是组件;
  • 忘记 extends:拿不到 React.Component 上的能力;
  • render 不返回或返回 undefined:页面什么都不渲染;
  • 返回多个并列元素:需要包一层 Fragment 或父元素,笔记代码里 root.render 就用了 <>...</>。

调用类组件的两种写法 ​

为什么需要:定义完得用起来,JSX 提供单标签和双标签两种形式,和 HTML 习惯一致。

怎么做:笔记里两种都出现了:

jsx
<Header></Header>
<Header/>

易错点:单标签必须带 /,写成 <Header> 不闭合会报错;双标签中间本节还没讲 children,先别乱塞内容。

React 调用类组件时做了什么 ​

为什么需要:理解调用过程,才能理解 render 里的 this,以及组件为什么能“自动”执行。

怎么想:JSX 编译后,React 遇到首字母大写的标签,会去查这个标识符是不是组件。

怎么做(笔记三步):

  1. 发现首字母大写的 jsx 调用标签,会将其当做组件处理;
  2. 查找是否定义了该组件,如果定义了并且发现是类组件,那么 render 函数会帮我们实例化该类,并用实例化出来的对象调用 render 方法;
  3. 将 render 方法执行后的返回值,替换掉组件标签调用的位置。

调试过程:在 Header 的 render 里执行 console.log('header render this: ', this),控制台打印出来的是 Header 的实例对象,不是 window,也不是类本身。

易错点:别把“实例化”理解成“执行类”。类本身不执行,执行的是 new 出来的实例的 render。

render 里的 this 永远指向实例 ​

为什么需要:render 里要读 this.state、this.props、调用 this.xxx 方法,如果 this 不是实例,全拿不到。

怎么想:render 不是我们手动调用的,是 React 内部用实例去调 实例.render();调用者是实例,普通方法的 this 由调用者决定,所以 this 就是实例。

怎么做:笔记代码里已经打了这行日志:

jsx
console.log('header render this: ' ,this);

易错点:不要把 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>
    </>
))

root.render 里 Header 出现两次,React 就会实例化两次、调用两次 render,两个实例互不影响——和前面 Person 的 p1、p2 是一个道理。控制台能看到两次 header render this,都是各自的 Header 实例。

易错点:最外层用 Fragment <>...</> 是因为要返回多个兄弟节点;className 不是 class;调用标签首字母大写这条规则贯穿始终。

函数组件本质:一个返回 React 元素的函数 ​

为什么需要:类组件要写 constructor、this、state,入门成本高;函数组件把“组件”还原成更直接的函数,便于理解和组合。怎么想:组件就是“输入数据,返回 UI 描述”的函数,本节先掌握最小模型——函数名就是组件名,返回值就是 React 元素。怎么做:函数名首字母大写,函数体里必须有 return,并且返回 React 元素。易错点:函数名小写会被 JSX 当成原生 HTML 标签;只写 return 但返回字符串、undefined 或多个并列元素,都不是本节要求的 React 元素返回形式。

组件名为什么必须首字母大写 ​

为什么需要:JSX 需要区分“原生标签”和“自定义组件”。怎么想:<div> 是小写原生标签;<Header> 首字母大写,JSX 会把它当变量/组件处理。怎么做:定义 function Header(){},调用时写 <Header/> 或 <Header></Header>,名字保持一致。易错点:定义成 Header 却写成 <header>,React 不会找你的函数组件;组件名和调用标签大小写不一致也会出错。结合转写稿里类组件反复强调的“实例对象、原型、this”等概念来对比:函数组件没有组件实例,不要套用类组件思路去找 this。

返回值必须是 React 元素 ​

为什么需要:render 最终要把组件返回值放到页面上,返回值必须能被 React 渲染。怎么想:先写一个最简结构 return (<div>...</div>);多个同级节点时用 Fragment <>...</> 包裹。怎么做:本节代码中 Header、Main、Footer 都返回一个 <div>;最外层调用时用 <>...</> 包住多个组件标签。易错点:return 后直接写多个并列 <div> 会语法错误;返回 undefined 或忘记 return,组件位置就没有可渲染内容。

调用方式:单标签与对标签 ​

为什么需要:JSX 提供两种等价写法,方便有/无子内容时表达。怎么想:没有子内容用自闭合更简洁;需要写子内容时用对标签。怎么做:单标签调用 <组件名/>,对标签调用 <组件名></组件名>。本节代码故意同时写了 <Header></Header> 和 <Header/>,用来验证两种写法都能调用同一个函数组件。易错点:自闭合标签不能再塞内容;对标签中间的内容会作为 children 处理,本节不展开,但不要误以为“对标签必须写内容”。

调用过程:render 如何把标签替换成返回值 ​

为什么需要:理解 JSX 不是 HTML,组件标签最终要由 React 处理。怎么想:render 遇到首字母大写标签,会查找组件定义;发现是函数组件后,由 render 帮我们调用该函数;函数返回的 React 元素替换掉组件调用标签的位置。怎么做:看下面完整代码,root.render 中写的是组件标签,不是手动 Header()。易错点:不要把手动函数调用和组件调用混为一谈;组件标签应交给 React 渲染流程处理。

jsx
const root = ReactDOM.createRoot(document.querySelector("#root"));
function Header(){
    console.log('Header run');
    console.log('Header this: ', this);// 函数组件中this 指向undefined
    return (
        <div>我是Header组件</div>
    )
}
function Main(){
    return (
        <div>我是Main组件</div>
    )
}
function Footer(){
    return (
        <div>我是Footer组件</div>
    )
}
root.render((
    <>
        <Header></Header>
        <Header/>
        <Main/>
        <Footer/>
    </>
))

用代码观察调用次数与 this ​

为什么需要:只看定义容易误以为组件只执行一次,实际每次渲染时函数组件都会执行。怎么想:在 Header 里放 console.log('Header run'),可以看到它被调用;代码里写了两个 Header 标签,因此 Header 函数会执行两次,Main、Footer 各一次。怎么做:用 console.log 做最小调试,观察“组件函数是否执行、this 是什么”。易错点:函数组件里打印 this 得到 undefined,这不是 bug,而是提醒你不要在函数组件里依赖 this 访问状态或方法;类组件那套 this.state、this.props 思路不能直接搬过来。转写稿中讨论类组件时出现的“实例对象、原型、bind、undefined、render、props”等术语,在这里要纠正为规范理解:函数组件没有实例,重点看函数参数和返回值。

易错点检查清单 ​

为什么需要:函数组件语法短,错误往往集中在命名和返回值。怎么想:写完先检查“名字大写了吗、return 了吗、返回的是 React 元素吗、标签调用对吗”。怎么做:按清单逐项核对;用 console.log 验证执行次数;多个同级节点用 Fragment 包裹。易错点:组件名小写、忘记 return、返回多个并列元素、在函数组件里找 this、把组件标签写成普通 HTML 标签。掌握这些,就完成了函数组件的最小闭环:定义、调用、由 render 调用并替换标签位置。

先说明本节内容范围 ​

本节笔记标题是“react脚手架构建工程化开发环境”,但转写稿实际围绕类组件的 state、事件回调 this、setState 和 props 展开,没有出现脚手架创建命令。因此下面按转写稿内容整理,不补写未讲到的脚手架步骤。

状态数据为什么必须放在 state 上 ​

为什么需要:组件不仅有 HTML 结构、CSS 样式,还要有“活”的数据来动态渲染;数据变了,页面要跟着更新。
怎么想:如果把数据随便定义成组件实例的普通属性,虽然能读、能改,但改完不会触发组件重新渲染。转写稿用购物车数量举例:数量从 100 改成 0,页面必须重新显示 0,所以数据必须放在特殊位置 state 上。
怎么做:类组件把状态数据定义在 this.state 中;修改时不能直接赋值,而要用 setState。
易错点:把状态写成 this.count = 100 也能改值,但不会触发 render,页面不会更新;不要误以为“值变了页面就一定变”。

state 的两种定义方式 ​

为什么需要:类组件需要初始化状态,写法要清楚且便于维护。
怎么想:state 本质上是类实例上的特殊属性,所以既可以在 constructor 里写,也可以用类字段直接赋值。
怎么做:方式一在构造函数中定义;方式二直接写 state = { count: 100 }。转写稿演示两种方式效果一致,并推荐方式二,因为不用写 constructor、super、this。

jsx
class App extends React.Component {
  state = { count: 100, msg: 'hello' }

  render() {
    return <div>{this.state.count}</div>
  }
}

易错点:方式二本质上等价于在 constructor 中执行 this.state = {...};不要写成普通变量,也不要在 render 里重新定义状态。

事件回调里的 this 为什么会丢 ​

为什么需要:点击按钮修改状态时,方法里的 this 必须指向当前组件实例,才能访问 this.state 和 this.setState。
怎么想:普通函数的 this 由调用者决定。onClick={this.click1} 只是把函数引用交给事件系统,真正调用时不是组件实例在调用,转写稿调试打印得到 undefined,所以不能直接这样写。
怎么做:可行方案有三种:onClick={this.click2.bind(this)};onClick={() => this.click3()};把方法直接定义成箭头函数 click4 = () => {}。转写稿还演示了在 constructor 中 this.click5 = this.click5.bind(this)。

jsx
click2() { console.log(this) }
click3 = () => { console.log(this) }

<button onClick={this.click2.bind(this)}>click2</button>
<button onClick={() => this.click3()}>click3</button>

易错点:直接写 onClick={this.click1} 会丢 this;bind 会返回新函数,不能写成 this.click2.bind(this) 后又在别处误用;直接箭头类字段每个实例都有一份,转写稿从“是否传参、是否占用内存”角度更推荐 bind 或箭头包裹。

setState 才是修改状态并触发渲染的入口 ​

为什么需要:直接改 this.state.count 虽然值会变,但页面不刷新;必须让组件重新 render。
怎么想:转写稿先故意直接赋值,发现数据变成 102、105,但页面没变,render 也没有重新执行;说明需要 React 提供的入口。
怎么做:用 this.setState({ count: this.state.count + num }) 修改;修改 msg 可写 this.setState({ msg: this.state.msg + '!' })。

jsx
addCount = (num) => {
  this.setState({ count: this.state.count + num })
}

易错点:不要直接 this.state.count += num;setState 接收对象,键名要和状态字段一致;转写稿中即使写入相同值,也会触发 render,但业务上仍应避免无意义更新。

父组件通过 props 给类子组件传数据 ​

为什么需要:多个组件可能使用同一份数据,例如列表组件和底部统计组件都要用同一个数组,需要父子通信。
怎么想:父组件通过子组件标签属性传值;子组件通过固定属性 this.props 接收。
怎么做:父组件写 <ClassCom count={this.state.count} msg={this.state.msg} school="硅谷" />;子组件用 const { count, msg, school } = this.props 解构使用。父组件 setState 后重新 render,子组件收到新的 props 也会重新渲染。

jsx
// 父组件
<ClassCom count={this.state.count} msg={this.state.msg} school="硅谷" />

// 类子组件
const { count, msg, school } = this.props

易错点:props 是外部传入数据,不能像 state 那样直接改;标签属性名和子组件接收名要对应,转写稿特意用不同名字说明“传什么属性名,props 里就有什么属性名”。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:类组件与函数组件的定义和调用(12-1 / 12-2) ​

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>类组件与函数组件</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">
    const root = ReactDOM.createRoot(document.querySelector("#root"));

    // ---------- 类组件 ----------
    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>;
      }
    }

    // ---------- 函数组件 ----------
    function FHeader() {
      console.log('FHeader run');
      console.log('FHeader this: ', this); // 函数组件中 this 指向 undefined
      return <div>我是 Header 函数组件</div>;
    }
    function FMain() {
      return <div>我是 Main 函数组件</div>;
    }
    function FFooter() {
      return <div>我是 Footer 函数组件</div>;
    }

    root.render(
      <>
        <Header></Header>
        <Header />
        <Main></Main>
        <Footer></Footer>

        <FHeader />
        <FMain></FMain>
        <FFooter />
      </>
    );
  </script>
</body>
</html>

关键行与易错点

  • class Header extends React.Component:类组件必须继承 React.Component,且必须写 render(),render 里返回 React 元素(JSX)。
  • 类名/函数名首字母必须大写:JSX 遇到小写标签会当成原生 HTML 标签处理,只有首字母大写才会被当作组件去查找定义。
  • 类组件 render 中的 this 永远是当前实例:render 是实例调用的,所以实例上的属性都能用 this.xxx 取到。
  • 函数组件里的 this 是 undefined:函数组件没有实例,不要在里面用 this。
  • <Header></Header> 与 <Header/> 完全等价,两种调用写法都可以。
  • 调用过程:JSX 发现大写标签 → 若是类组件就实例化并调用 render,若是函数组件就直接调用函数 → 用返回值替换掉标签位置。
  • 页面上直接跑 JSX 必须引 babel 并且 script 写 type="text/babel",否则浏览器报语法错误。

示例二:state 的定义、事件回调 this 指向、setState 修改状态(12-3 / 12-4 / 12-5) ​

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>state 与 setState</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">
    const root = ReactDOM.createRoot(document.querySelector("#root"));

    class App extends React.Component {
      // 定义状态方式二:直接赋值(相当于写在 constructor 里 this.state = {...})
      state = {
        count: 100,
        msg: '艾特硅谷'
      };

      // 定义状态方式一(对照):构造函数中定义
      // constructor() {
      //   super();
      //   this.state = { count: 100, msg: '艾特硅谷' };
      // }

      // 箭头函数定义的方法:this 一出生就绑定当前实例
      addCount = (num) => {
        this.setState({
          count: this.state.count + num
        });
      };

      changeMsg = () => {
        this.setState({
          msg: this.state.msg + '!'
        });
      };

      // 普通方法:this 由调用者决定
      click1() {
        console.log('click1 this: ', this); // 直接当回调传 → 丢失 this
      }
      click2() {
        console.log('click2 this: ', this); // bind 后 → 当前实例
      }

      render() {
        console.log('App render run');
        return (
          <div>
            <p>count: {this.state.count}</p>
            <p>msg: {this.state.msg}</p>

            <button onClick={() => this.addCount(3)}>每次 +3</button>
            <button onClick={this.changeMsg}>改 msg</button>

            <hr />
            <button onClick={this.click1}>普通方法(this 丢失)</button>
            <button onClick={this.click2.bind(this)}>bind 绑定 this</button>
            <button onClick={() => this.click2()}>箭头包裹调用</button>
          </div>
        );
      }
    }

    root.render(<App />);
  </script>
</body>
</html>

关键行与易错点

  • 状态必须定义在 state 上:this.state.count = xxx 这种直接赋值虽然能改掉数据,但不会触发组件重新渲染(render 不会重新执行),页面永远显示旧值。
  • 两种定义方式等价:state = {...} 等价于在 constructor 里 this.state = {...},直接赋值写法更常用、更简单(constructor 里必须先 super())。
  • 修改状态只能用 this.setState({...}):它做两件事——改状态值 + 触发 render 重新调用,从而页面更新。
  • 普通方法的 this 由调用者决定:onClick={this.click1} 只是把函数引用交出去,事件触发时调用者不是组件实例,this 就丢了(严格模式下为 undefined)。
  • bind 是「改变 this 指向并返回一个新函数」,不会立即调用,所以能直接写在 onClick 里;call/apply 会立即调用,onClick={this.click2.call(this)} 是错的。
  • onClick={() => this.click2()} 用的是箭头函数,箭头函数没有自己的 this,沿用 render 里的 this(即实例),所以也能拿到正确的 this。
  • 直接在类里 click4 = () => {} 也是可行的,但每个实例都会各自复制一份方法,占内存;普通方法写在原型上更省内存。

示例三:父组件向类子组件传值 props(12-6) ​

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>props 父子传值</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">
    const root = ReactDOM.createRoot(document.querySelector("#root"));

    // 父组件:数据定义在自己身上
    class App extends React.Component {
      state = {
        count: 100,
        msg: '艾特硅谷'
      };

      addCount = () => {
        this.setState({
          count: this.state.count + 1
        });
      };

      render() {
        return (
          <div>
            <p>父组件 count:{this.state.count}</p>
            <button onClick={this.addCount}>count++</button>

            {/* 通过子组件标签属性的方式传值:状态数据 + 写死的值都能传 */}
            <ClassCom count={this.state.count} msg={this.state.msg} school="艾特硅谷" />
            <FuncCom count={this.state.count} msg={this.state.msg} />
          </div>
        );
      }
    }

    // 类子组件:通过固定的 this.props 接收外部数据
    class ClassCom extends React.Component {
      render() {
        console.log('this.props: ', this.props);

        const { count, msg, school } = this.props;
        return (
          <div>
            类子组件:{count} —— {msg} —— {school}
          </div>
        );
      }
    }

    // 函数子组件:通过形参 props 接收
    function FuncCom(props) {
      const { count, msg } = props;
      return (
        <div>
          函数子组件:{count} —— {msg}
        </div>
      );
    }

    root.render(<App />);
  </script>
</body>
</html>

关键行与易错点

  • 父传子的唯一入口是「子组件标签的属性」:<ClassCom count={this.state.count} />,属性名自己定,但一般与状态变量名保持一致,避免对不上。
  • 类子组件用固定的 this.props 接收,this.props 是一个对象,键就是父组件写的属性名;函数子组件用形参 props 接收。
  • 传进去的既可以是状态数据(会随 setState 变化),也可以是写死的值,比如 school="艾特硅谷"。
  • 父组件 setState 后重新 render,子组件也会跟着重新 render,此时 props 拿到的是最新值,所以父子两边的数字会同步变化。
  • props 是只读的外部数据,不要在子组件里修改它;需要自己变化的数据应该定义成子组件自己的 state。
  • 数据多时先解构再使用(const { count, msg } = this.props),比反复写 this.props.count 更清晰。

常见错误 ​

  1. 现象:点击按钮后 this.state.count 变了,但页面不更新。
    原因:直接给 this.state.xxx 赋值,绕过了 React 的更新机制。
    修法:改用 this.setState({ xxx: 新值 }),需要基于旧值时先读取再计算。

  2. 现象:事件回调里 this 不是组件实例,访问 this.state 报错或取不到值。
    原因:普通方法作为事件回调传递后丢失调用者,this 不再指向实例。
    修法:用箭头函数包裹调用、类字段箭头函数,或在构造函数中 bind(this)。

  3. 现象:子组件能渲染,但拿不到父组件传的值。
    原因:父组件标签属性名与子组件读取的 this.props.xxx 不一致,或忘记传参。
    修法:统一属性名,子组件通过 this.props 读取;需要多次使用时先解构。

  4. 现象:把 state 定义成普通实例属性,如 this.count = 0,修改后视图不响应。
    原因:React 只把 state 作为状态源处理,普通属性变化不会触发重新渲染。
    修法:状态统一放到 state 中,通过 setState 更新。

  5. 现象:类组件报 “render is not a function” 或组件不显示。
    原因:类组件未继承 React.Component、缺少 render,或 render 未返回 React 元素。
    修法:补齐继承与 render,确保返回 JSX/React 元素;组件名首字母大写。

  6. 现象:函数组件里写 this.state 或试图用 this 拿数据。
    原因:函数组件没有组件实例,this 不是数据载体。
    修法:函数组件用参数 props 接收外部数据;需要状态时使用 Hooks,不要照搬类组件写法。

练习 ​

  1. 改错题:下面代码点击后数字变了但页面不变,请指出问题并改为正确写法。

    jsx
    add = () => &#123;
      this.state.count += 1;
    &#125;;
  2. 改错题:给按钮绑定 onClick={this.handleClick} 后,handleClick 中 this 丢失。请给出至少两种修复方式,并说明推荐场景。

  3. 组件通信题:父组件有 state = { list: [...] },请把 list 传给类子组件,并在子组件中解构后渲染长度。

  4. 判断题:以下写法分别有什么问题?

    jsx
    class header extends React.Component &#123;&#125;
    function main() &#123; return &lt;div&gt;main&lt;/div&gt;; &#125;

    要求指出命名、继承、render、调用方式相关风险。

  5. 综合题:实现一个类组件:state 中有 count,两个按钮分别“+1”和“重置为 0”,要求事件回调中 this 正确,且视图随状态更新。

原视频对照 ​

#视频时长
1类组件状态数据的理解及为什么一定要定义在state属性上02:18
2定义状态的方式二01:57
3类复习12:00
4类组件事件回调this指向问题16:02
5setState修改状态数据09:07
6类的子组件如何接收父组件传递的外部数据13:01

小结 ​

  1. 类组件排错先看三件事:是否继承 React.Component、是否有 render、render 是否返回 React 元素。
  2. 状态数据只认 state,更新只认 setState;直接改 state 是“值变了、视图不动”的典型根因。
  3. 事件回调中的 this 问题要在绑定阶段解决,不要等到函数体里再猜 this。
  4. 类字段箭头函数写法简洁,但每个实例一份;普通方法在原型上更省内存,选择时结合传参和绑定成本。
  5. props 是父传子的单向数据通道,属性名要与子组件读取名一致,子组件不应直接改 props。
  6. 组件名首字母大写是 JSX 识别组件的基础规则,函数组件和类组件都一样。
  7. 多个组件共用同一份数据时,优先把数据提升到共同父级,再通过 props 分发。