React全家桶笔记(二):React组件核心 — State、Props、Refs

React全家桶笔记(二):React组件核心 --- State、Props、Refs

本篇覆盖 React 两种组件定义方式,以及组件实例的三大核心属性:state、props、refs。这是 React 开发的基石。 📺 对应视频:张天禹react全家桶P8 - P31


一、开发者工具安装(P8)

Chrome 安装 React Developer Tools 扩展,安装后浏览器右上角会出现 React 图标:

  • 🔴 红色:当前页面使用了未压缩的 React(开发环境)
  • 🔵 蓝色:当前页面使用了压缩后的 React(生产环境)
  • 灰色:当前页面没有使用 React

安装后 DevTools 会多出两个面板:Components (组件树)和 Profiler(性能分析)。


二、函数式组件(P9)

javascript 复制代码
// 函数式组件 --- 用函数定义组件
function MyComponent() {
  console.log(this) // undefined(babel 编译后开启了严格模式)
  return <h2>我是用函数定义的组件(适用于简单组件)</h2>
}

// 渲染组件到页面
ReactDOM.render(<MyComponent/>, document.getElementById('test'))

执行流程

  1. React 解析组件标签,找到 MyComponent 组件
  2. 发现组件是函数定义的,随后调用该函数
  3. 将返回的虚拟 DOM 转为真实 DOM,渲染到页面

⚠️ 注意 :函数式组件中的 thisundefined,因为 Babel 编译后默认开启严格模式。函数式组件在 Hooks 出现之前只能做"简单组件"(无状态),Hooks 出现后函数式组件也能拥有状态了。


三、类的基础知识复习(P10)

在学习类式组件之前,需要先回顾 ES6 的 class 语法:

javascript 复制代码
// 创建一个 Person 类
class Person {
  // 构造器方法
  constructor(name, age) {
    // this 指向类的实例对象
    this.name = name
    this.age = age
  }

  // 一般方法 --- 放在类的原型对象上,供实例使用
  speak() {
    // speak 方法通过 Person 实例调用时,this 指向实例
    console.log(`我叫${this.name},今年${this.age}岁`)
  }
}

// 创建实例
const p1 = new Person('Tom', 18)
p1.speak() // 我叫Tom,今年18岁

// 继承
class Student extends Person {
  constructor(name, age, grade) {
    super(name, age) // 必须在最前面调用 super
    this.grade = grade
  }

  // 重写从父类继承的方法
  speak() {
    console.log(`我叫${this.name},读${this.grade}年级`)
  }
}

类的核心知识点

  1. 类中的构造器不是必须写的,只有对实例进行初始化操作时才写
  2. 子类继承父类,如果写了构造器,super 必须在构造器最前面调用
  3. 类中定义的方法都放在了原型对象上,供实例使用

四、类式组件(P11)

scala 复制代码
// 类式组件 --- 必须继承 React.Component
class MyComponent extends React.Component {
  render() {
    // render 放在 MyComponent 的原型对象上,供实例使用
    // render 中的 this 指向 MyComponent 的组件实例对象
    console.log('render中的this:', this)
    return <h2>我是用类定义的组件(适用于复杂组件)</h2>
  }
}

ReactDOM.render(<MyComponent/>, document.getElementById('test'))

执行流程

  1. React 解析组件标签,找到 MyComponent 组件
  2. 发现组件是类定义的,随后 new 出该类的实例,并通过该实例调用原型上的 render 方法
  3. render 返回的虚拟 DOM 转为真实 DOM,渲染到页面

🔗 概念扩展 :组件实例对象上有三个重要属性 --- statepropsrefs,这就是接下来要学的三大核心属性。


五、State --- 组件状态(P12-P19)

5.1 理解 state(P12)

  • state 是组件对象最重要的属性,值是对象(可以包含多个 key-value)
  • 组件被称为"状态机",通过更新组件的 state 来更新对应的页面显示(重新渲染组件)
  • 数据驱动视图:数据变了 → 视图自动更新

5.2 初始化 state(P13)

scala 复制代码
class Weather extends React.Component {
  constructor(props) {
    super(props)
    // 初始化状态
    this.state = { isHot: true }
  }

  render() {
    return <h1>今天天气很{this.state.isHot ? '炎热' : '凉爽'}</h1>
  }
}

5.3 React 中的事件绑定(P14)

scala 复制代码
class Weather extends React.Component {
  constructor(props) {
    super(props)
    this.state = { isHot: true }
  }

  render() {
    // React 事件绑定:onClick(注意大小写,不是 onclick)
    return <h1 onClick={demo}>今天天气很{this.state.isHot ? '炎热' : '凉爽'}</h1>
  }
}

function demo() {
  console.log('标题被点击了')
}

React 事件绑定 vs 原生事件绑定

ini 复制代码
// 原生 JS
<button onclick="demo()">    // 注意是小写 onclick,值是字符串

// React JSX
<button onClick={demo}>      // 注意是大驼峰 onClick,值是函数引用(不加括号)

5.4 类中方法的 this 指向问题(P15-P16)

kotlin 复制代码
class Weather extends React.Component {
  constructor(props) {
    super(props)
    this.state = { isHot: true }
    // 🔑 关键:解决 this 指向问题
    // 在原型上的 changeWeather 基础上,生成一个绑定了 this 的新函数,挂到实例自身
    this.changeWeather = this.changeWeather.bind(this)
  }

  render() {
    return <h1 onClick={this.changeWeather}>
      今天天气很{this.state.isHot ? '炎热' : '凉爽'}
    </h1>
  }

  changeWeather() {
    // 如果不做 bind 处理,这里的 this 是 undefined
    // 原因:changeWeather 不是通过实例调用的,而是作为回调函数直接调用
    // 加上类中默认开启了严格模式,所以 this 不会指向 window,而是 undefined
    console.log(this)
  }
}

🎯 面试高频:为什么类组件方法中的 this 是 undefined?

  1. 事件回调函数不是通过实例调用的,而是直接调用
  2. 类中的方法默认开启了局部严格模式
  3. 严格模式下,直接调用函数 this 不会指向 window,而是 undefined
  4. 解决方案:在构造器中用 bind 绑定 this

5.5 setState 的使用(P17)

javascript 复制代码
changeWeather() {
  // ❌ 错误!直接修改 state 不会触发重新渲染
  // this.state.isHot = false

  // ✅ 正确!必须通过 setState 修改状态
  const isHot = this.state.isHot
  this.setState({ isHot: !isHot })
}

setState 的核心规则

  1. 状态(state)不可直接更改,必须通过 setState() 修改
  2. setState 是一次合并操作,不是替换。只会更新你传入的属性,其他属性保持不变
  3. setState 会触发 render 重新调用

5.6 state 的简写方式(P18)

kotlin 复制代码
class Weather extends React.Component {
  // ✅ 简写:直接用赋值语句初始化 state(类中可以直接写赋值语句)
  state = { isHot: true, wind: '微风' }

  render() {
    const { isHot, wind } = this.state
    return <h1 onClick={this.changeWeather}>
      今天天气很{isHot ? '炎热' : '凉爽'},{wind}
    </h1>
  }

  // ✅ 简写:用箭头函数定义方法,箭头函数没有自己的 this,会找外层的 this
  // 这样就不需要在构造器中 bind 了
  changeWeather = () => {
    const isHot = this.state.isHot
    this.setState({ isHot: !isHot })
  }
}

🔗 概念扩展 :为什么箭头函数能解决 this 问题? 箭头函数没有自己的 this,它会捕获其所在上下文(定义时的位置)的 this 值。在类中用赋值语句 + 箭头函数,相当于在实例上直接定义了一个方法,其 this 永远指向该实例。

5.7 state 总结(P19)

perl 复制代码
state 要点:
├── state 是对象,包含多个 key-value
├── 通过 setState() 修改状态,不能直接赋值
├── setState 是合并操作,不是替换
├── setState 会触发 render 重新执行
├── 构造器调用 1 次,render 调用 1+n 次(初始化1次 + 每次setState)
└── this 指向问题的两种解决方案:
    ├── 方案1:构造器中 bind
    └── 方案2:箭头函数(推荐 ✅)

六、Props --- 组件属性(P20-P26)

6.1 props 的基本使用(P20)

javascript 复制代码
class Person extends React.Component {
  render() {
    const { name, age, sex } = this.props
    return (
      <ul>
        <li>姓名:{name}</li>
        <li>性别:{sex}</li>
        <li>年龄:{age}</li>
      </ul>
    )
  }
}

// 渲染时传递 props
ReactDOM.render(
  <Person name="Tom" age="18" sex="男"/>,
  document.getElementById('test')
)

核心理解

  • state 是组件内部的数据(自己管理)
  • props 是从组件外部传入的数据(父组件传递)
  • props 是只读的,组件内部不能修改自己的 props

6.2 批量传递 props(P21)

javascript 复制代码
const person = { name: 'Tom', age: 18, sex: '男' }

// 使用展开运算符批量传递
ReactDOM.render(<Person {...person}/>, document.getElementById('test'))

⚠️ 注意 :这里的 {...person} 不是 JS 的展开运算符语法。在原生 JS 中,... 不能展开对象(只能在字面量中使用)。这是 React + Babel 的特殊语法,仅适用于标签属性的传递。

6.3 对 props 进行限制(P22)

javascript 复制代码
// 需要引入 prop-types 库
// <script src="prop-types.js"></script>

class Person extends React.Component {
  render() {
    const { name, age, sex } = this.props
    return (
      <ul>
        <li>姓名:{name}</li>
        <li>性别:{sex}</li>
        <li>年龄:{age + 1}</li>
      </ul>
    )
  }
}

// 对标签属性进行类型、必要性的限制
Person.propTypes = {
  name: PropTypes.string.isRequired, // 字符串类型,必传
  sex: PropTypes.string,             // 字符串类型
  age: PropTypes.number,             // 数值类型
  speak: PropTypes.func              // 函数类型(注意是 func 不是 function)
}

// 指定默认值
Person.defaultProps = {
  sex: '未知',
  age: 18
}

6.4 props 的简写方式(P23)

javascript 复制代码
class Person extends React.Component {
  // 使用 static 关键字将限制规则写在类内部
  static propTypes = {
    name: PropTypes.string.isRequired,
    sex: PropTypes.string,
    age: PropTypes.number,
  }

  static defaultProps = {
    sex: '未知',
    age: 18,
  }

  render() {
    const { name, age, sex } = this.props
    return (
      <ul>
        <li>姓名:{name}</li>
        <li>性别:{sex}</li>
        <li>年龄:{age + 1}</li>
      </ul>
    )
  }
}

6.5 类式组件中的构造器与 props(P24)

scala 复制代码
class Person extends React.Component {
  constructor(props) {
    // 如果写了构造器,是否接收 props 并传给 super,取决于:
    // 你是否希望在构造器中通过 this.props 访问 props
    super(props)
    console.log(this.props) // ✅ 有值
  }
  // ...
}

class Person2 extends React.Component {
  constructor() {
    super()
    console.log(this.props) // ❌ undefined
  }
  // 但在 render 等其他方法中 this.props 仍然可用
}

💡 实际开发中:构造器几乎不写。state 用赋值语句初始化,方法用箭头函数定义,完全不需要构造器。

6.6 函数式组件使用 props(P25)

javascript 复制代码
// 函数式组件只能使用 props(在 Hooks 之前)
function Person(props) {
  const { name, age, sex } = props
  return (
    <ul>
      <li>姓名:{name}</li>
      <li>性别:{sex}</li>
      <li>年龄:{age}</li>
    </ul>
  )
}

// 限制仍然写在函数外面
Person.propTypes = {
  name: PropTypes.string.isRequired,
}
Person.defaultProps = {
  sex: '未知',
}

6.7 props 总结(P26)

java 复制代码
props 要点:
├── 从组件外部传入数据,组件内部只读
├── 批量传递:<Person {...obj}/>(React+Babel 特殊语法)
├── 类型限制:propTypes(需引入 prop-types 库)
├── 默认值:defaultProps
├── 简写:static propTypes / static defaultProps
├── 构造器中要用 this.props → 必须 super(props)
├── 函数式组件通过参数接收 props
└── props 是只读的!不能在组件内部修改

七、Refs --- 组件引用(P27-P31)

Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。

7.1 字符串形式的 ref(P27)--- 已过时

scala 复制代码
class Demo extends React.Component {
  showData = () => {
    // 通过 this.refs 获取 DOM 节点
    const { input1 } = this.refs
    alert(input1.value)
  }

  render() {
    return (
      <div>
        <input ref="input1" type="text" placeholder="点击按钮提示数据" />
        <button onClick={this.showData}>点我提示左侧数据</button>
      </div>
    )
  }
}

⚠️ 注意 :字符串形式的 ref 已被 React 官方标记为过时 API,存在效率问题,不推荐使用。了解即可。

7.2 回调形式的 ref(P28-P29)

scala 复制代码
class Demo extends React.Component {
  showData = () => {
    const { input1 } = this
    alert(input1.value)
  }

  render() {
    return (
      <div>
        {/* 回调 ref:React 会在渲染时调用这个回调,把 DOM 节点传进来 */}
        <input ref={c => this.input1 = c} type="text" placeholder="点击按钮提示数据" />
        <button onClick={this.showData}>点我提示左侧数据</button>
      </div>
    )
  }
}

回调 ref 的调用次数问题(P29)

如果 ref 回调函数是以内联函数 的方式定义的,在更新过程中它会被执行两次

  • 第一次传入 null(清空旧的 ref)
  • 第二次传入 DOM 元素
ini 复制代码
// 内联写法 --- 更新时会调用两次(无关紧要,不影响功能)
<input ref={c => this.input1 = c} />

// class 绑定写法 --- 更新时只调用一次
saveInput = (c) => {
  this.input1 = c
}
<input ref={this.saveInput} />

💡 大多数情况下,内联写法的两次调用是无关紧要的。

7.3 createRef 的使用(P30)--- 推荐

scala 复制代码
class Demo extends React.Component {
  // React.createRef() 返回一个容器
  // 该容器可以存储被 ref 所标识的节点
  // 该容器是"专人专用"的,一个 createRef 只能存一个节点
  myRef = React.createRef()
  myRef2 = React.createRef()

  showData = () => {
    alert(this.myRef.current.value)
  }

  showData2 = () => {
    alert(this.myRef2.current.value)
  }

  render() {
    return (
      <div>
        <input ref={this.myRef} type="text" placeholder="点击按钮提示数据" />
        <button onClick={this.showData}>点我提示左侧数据</button>
        <input ref={this.myRef2} onBlur={this.showData2} type="text" placeholder="失去焦点提示数据" />
      </div>
    )
  }
}

7.4 refs 总结(P31)

csharp 复制代码
refs 三种形式:
├── 字符串 ref:<input ref="input1"/>
│   └── ❌ 已过时,存在效率问题,不推荐
├── 回调 ref:<input ref={c => this.input1 = c}/>
│   ├── 内联写法:更新时调用两次(无影响)
│   └── class 绑定写法:更新时调用一次
└── createRef:myRef = React.createRef()
    ├── <input ref={this.myRef}/>
    ├── 通过 this.myRef.current 获取节点
    └── ✅ React 最推荐的方式

🎯 面试高频:三种 ref 的区别和推荐度? 字符串 ref 最简单但已过时;回调 ref 灵活但有调用次数的小问题;createRef 是官方最推荐的方式,语义清晰,一个 ref 对应一个节点。


八、三大属性对比总结

scss 复制代码
组件实例三大核心属性:
┌──────────┬──────────────────────────────────────┐
│ 属性      │ 说明                                  │
├──────────┼──────────────────────────────────────┤
│ state    │ 组件内部的状态数据,驱动视图更新          │
│          │ 通过 setState() 修改,触发重新渲染        │
├──────────┼──────────────────────────────────────┤
│ props    │ 外部传入的数据,组件内只读                │
│          │ 父子组件通信的桥梁                       │
├──────────┼──────────────────────────────────────┤
│ refs     │ 获取 DOM 节点的引用                     │
│          │ 推荐 createRef,尽量少用                 │
└──────────┴──────────────────────────────────────┘

🔗 概念扩展 :React 的数据流是单向

  • 父组件通过 props 向子组件传递数据
  • 子组件不能直接修改 props
  • 如果子组件需要改变父组件的数据,父组件需要通过 props 传递一个回调函数给子组件
  • 这就是 React 的"单向数据流"设计哲学

📌 下一篇:[React全家桶笔记(三):React进阶 --- 事件处理、表单与生命周期] 将深入受控/非受控组件、高阶函数、以及 React 最重要的生命周期机制。

相关推荐
Jenlybein2 小时前
一文了解 pnpm,并快速上手操作!
前端·javascript·npm
大萝卜呼呼2 小时前
Next.js第二课 - 项目结构详解 - 优栈
前端·next.js
skywalkzf2 小时前
全志 V853 开发:lunch 不显示项目列表问题排查与解决
前端·chrome
踩着两条虫2 小时前
VTJ.PRO 在线应用开发平台的项目模板(Web、H5、UniApp)
前端·低代码·ai编程
云原生指北2 小时前
测试文章标题 - Omnipub 自动发布测试
前端
无责任此方_修行中2 小时前
"JavaScript"这个名字,到底属于谁?一场价值74亿美元的法律战争
前端·javascript·程序员
CharlesY2 小时前
网页排版与编码的隐形神器:HTML字符实体从入门到精通
前端
我命由我123452 小时前
React - useEffect、useRef、Fragment
开发语言·前端·javascript·react.js·前端框架·ecmascript·js
云原生指北2 小时前
测试文章 - 摘要自定义填充验证
前端