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)
// 函数式组件 — 用函数定义组件
function MyComponent() {
console.log(this) // undefined(babel 编译后开启了严格模式)
return <h2>我是用函数定义的组件(适用于简单组件)</h2>
}
// 渲染组件到页面
ReactDOM.render(<MyComponent/>, document.getElementById('test'))
执行流程:
- React 解析组件标签,找到
MyComponent组件 - 发现组件是函数定义的,随后调用该函数
- 将返回的虚拟 DOM 转为真实 DOM,渲染到页面
⚠️ 注意:函数式组件中的
this是undefined,因为 Babel 编译后默认开启严格模式。函数式组件在 Hooks 出现之前只能做"简单组件"(无状态),Hooks 出现后函数式组件也能拥有状态了。
三、类的基础知识复习(P10)
在学习类式组件之前,需要先回顾 ES6 的 class 语法:
// 创建一个 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}年级`)
}
}
类的核心知识点:
- 类中的构造器不是必须写的,只有对实例进行初始化操作时才写
- 子类继承父类,如果写了构造器,
super必须在构造器最前面调用 - 类中定义的方法都放在了原型对象上,供实例使用
四、类式组件(P11)
// 类式组件 — 必须继承 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'))
执行流程:
- React 解析组件标签,找到
MyComponent组件 - 发现组件是类定义的,随后
new出该类的实例,并通过该实例调用原型上的render方法 - 将
render返回的虚拟 DOM 转为真实 DOM,渲染到页面
🔗 概念扩展:组件实例对象上有三个重要属性 —
state、props、refs,这就是接下来要学的三大核心属性。
五、State — 组件状态(P12-P19)
5.1 理解 state(P12)
- state 是组件对象最重要的属性,值是对象(可以包含多个 key-value)
- 组件被称为"状态机",通过更新组件的 state 来更新对应的页面显示(重新渲染组件)
- 数据驱动视图:数据变了 → 视图自动更新
5.2 初始化 state(P13)
class Weather extends React.Component {
constructor(props) {
super(props)
// 初始化状态
this.state = { isHot: true }
}
render() {
return <h1>今天天气很{this.state.isHot ? '炎热' : '凉爽'}</h1>
}
}
5.3 React 中的事件绑定(P14)
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 原生事件绑定:
// 原生 JS
<button onclick="demo()"> // 注意是小写 onclick,值是字符串
// React JSX
<button onClick={demo}> // 注意是大驼峰 onClick,值是函数引用(不加括号)
5.4 类中方法的 this 指向问题(P15-P16)
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?
- 事件回调函数不是通过实例调用的,而是直接调用
- 类中的方法默认开启了局部严格模式
- 严格模式下,直接调用函数 this 不会指向 window,而是 undefined
- 解决方案:在构造器中用
bind绑定 this
5.5 setState 的使用(P17)
changeWeather() {
// ❌ 错误!直接修改 state 不会触发重新渲染
// this.state.isHot = false
// ✅ 正确!必须通过 setState 修改状态
const isHot = this.state.isHot
this.setState({ isHot: !isHot })
}
setState 的核心规则:
- 状态(state)不可直接更改,必须通过
setState()修改 setState是一次合并操作,不是替换。只会更新你传入的属性,其他属性保持不变setState会触发render重新调用
5.6 state 的简写方式(P18)
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)
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)
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)
const person = { name: 'Tom', age: 18, sex: '男' }
// 使用展开运算符批量传递
ReactDOM.render(<Person {...person}/>, document.getElementById('test'))
⚠️ 注意:这里的
{...person}不是 JS 的展开运算符语法。在原生 JS 中,...不能展开对象(只能在字面量中使用)。这是 React + Babel 的特殊语法,仅适用于标签属性的传递。
6.3 对 props 进行限制(P22)
// 需要引入 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)
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)
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)
// 函数式组件只能使用 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)
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)— 已过时
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)
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 元素
// 内联写法 — 更新时会调用两次(无关紧要,不影响功能)
<input ref={c => this.input1 = c} />
// class 绑定写法 — 更新时只调用一次
saveInput = (c) => {
this.input1 = c
}
<input ref={this.saveInput} />
💡 大多数情况下,内联写法的两次调用是无关紧要的。
7.3 createRef 的使用(P30)— 推荐
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)
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 对应一个节点。
八、三大属性对比总结
组件实例三大核心属性:
┌──────────┬──────────────────────────────────────┐
│ 属性 │ 说明 │
├──────────┼──────────────────────────────────────┤
│ state │ 组件内部的状态数据,驱动视图更新 │
│ │ 通过 setState() 修改,触发重新渲染 │
├──────────┼──────────────────────────────────────┤
│ props │ 外部传入的数据,组件内只读 │
│ │ 父子组件通信的桥梁 │
├──────────┼──────────────────────────────────────┤
│ refs │ 获取 DOM 节点的引用 │
│ │ 推荐 createRef,尽量少用 │
└──────────┴──────────────────────────────────────┘
🔗 概念扩展:React 的数据流是单向的
- 父组件通过 props 向子组件传递数据
- 子组件不能直接修改 props
- 如果子组件需要改变父组件的数据,父组件需要通过 props 传递一个回调函数给子组件
- 这就是 React 的"单向数据流"设计哲学
📌 下一篇:[React全家桶笔记(三):React进阶 — 事件处理、表单与生命周期] 将深入受控/非受控组件、高阶函数、以及 React 最重要的生命周期机制。