普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月7日首页

【AJAX-Day2】Promise与回调地狱

2026年4月6日 22:12

【AJAX-Day2】Promise与回调地狱

🎯 核心目标:彻底理解 Promise 三种状态、链式调用、async/await、以及如何解决回调地狱


一、回调地狱(Callback Hell)

1.1 问题的产生

当多个异步操作有依赖关系时(必须等上一个完成才能执行下一个),就会产生层层嵌套的回调:

// 场景:登录 → 获取用户信息 → 获取用户的订单 → 获取订单详情
// 每一步都依赖上一步的结果

// 原生 XHR 回调地狱版本
login({ username, password }, function(userData) {
  getUserInfo(userData.id, function(userInfo) {
    getOrders(userInfo.id, function(orders) {
      getOrderDetail(orders[0].id, function(detail) {
        // 终于拿到了……但代码已经缩进到了宇宙深处
        console.log(detail)
        // 更深的嵌套...
      }, function(err) { console.error(err) })
    }, function(err) { console.error(err) })
  }, function(err) { console.error(err) })
}, function(err) { console.error(err) })

回调 地狱的问题:

  • 代码横向扩展,难以阅读
  • 错误处理分散,难以维护
  • 代码逻辑难以复用
  • 无法使用 try/catch

1.2 Promise 的诞生

Promise 是 ES6 引入的异步编程 解决方案,它将异步操作封装成一个对象,通过链式调用解决回调地狱。


二、Promise 详解

2.1 Promise 的三种状态

pending(等待中)
    ↓
  成功 → fulfilled(已完成)→ 执行 .then() 的回调
  失败 → rejected(已拒绝)→ 执行 .catch() 的回调

⚠️ 状态一旦改变,就不会再变(不可逆)
// 创建 Promise
const p = new Promise((resolve, reject) => {
  // 执行异步操作
  setTimeout(() => {
    const success = Math.random() > 0.5

    if (success) {
      resolve('成功的数据')  // 状态:pending → fulfilled
    } else {
      reject(new Error('失败的原因'))  // 状态:pending → rejected
    }
  }, 1000)
})

// 消费 Promise
p.then(data => {
  console.log('成功:', data)  // 'success的数据'
}).catch(err => {
  console.error('失败:', err.message)
})

2.2 Promise 链式调用

then() 返回一个新的 Promise,这是链式调用的关键。

// 链式调用解决回调地狱
axios.get('/api/login', { params: { username, password } })
  .then(({ data }) => {
    // 第一步成功,返回值会传给下一个 then
    return axios.get(`/api/users/${data.userId}`)
  })
  .then(({ data }) => {
    // 第二步成功
    return axios.get(`/api/orders?userId=${data.id}`)
  })
  .then(({ data }) => {
    // 第三步成功
    console.log('订单列表:', data)
  })
  .catch(err => {
    // 任意一步失败都会被这里捕获
    console.error('出错了:', err.message)
  })

then() 的返回值规则:

promise
  .then(data => {
    return 123           // 返回普通值 → 下一个 then 收到 123
  })
  .then(data => {
    return axios.get('/api/xxx')  // 返回 Promise → 等待该 Promise 完成
  })
  .then(data => {
    // data 是上面 axios.get 的结果
    throw new Error('主动抛出错误')  // 抛出错误 → 跳转到 catch
  })
  .catch(err => { ... })

2.3 Promise 静态方法

// Promise.resolve():创建一个立即成功的 Promise
const p1 = Promise.resolve(42)
p1.then(v => console.log(v))  // 42

// Promise.reject():创建一个立即失败的 Promise
const p2 = Promise.reject(new Error('失败'))
p2.catch(e => console.error(e))

// Promise.all():所有都成功才成功,有一个失败就失败
Promise.all([
  axios.get('/api/users'),
  axios.get('/api/posts'),
  axios.get('/api/comments')
]).then(([users, posts, comments]) => {
  // 三个请求都完成后才执行
  console.log(users.data, posts.data, comments.data)
}).catch(err => {
  // 任意一个失败则触发
  console.error(err)
})

// Promise.allSettled():所有都完成(无论成功失败)才结束(ES2020)
Promise.allSettled([
  axios.get('/api/users'),
  axios.get('/api/will-fail')
]).then(results => {
  results.forEach(result => {
    if (result.status === 'fulfilled') {
      console.log('成功:', result.value)
    } else {
      console.log('失败:', result.reason)
    }
  })
})

// Promise.race():第一个完成的(无论成功失败)决定结果
// 应用:超时控制
function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('请求超时')), ms)
  )
  return Promise.race([promise, timeout])
}

// Promise.any():第一个成功的决定结果(ES2021)
Promise.any([
  fetch('https://api1.example.com'),  // 可能失败
  fetch('https://api2.example.com'),  // 备用地址
]).then(response => {
  // 用最先成功的那个
})

三、async / await

3.1 什么是 async/await?

async/await 是 ES2017 引入的语法糖,让异步代码写起来像同步代码一样直观。

// Promise 链式调用版
function getData() {
  return axios.get('/api/step1')
    .then(r1 => axios.get(`/api/step2/${r1.data.id}`))
    .then(r2 => axios.get(`/api/step3/${r2.data.id}`))
    .then(r3 => r3.data)
    .catch(err => console.error(err))
}

// async/await 版(更清晰!)
async function getData() {
  try {
    const r1 = await axios.get('/api/step1')
    const r2 = await axios.get(`/api/step2/${r1.data.id}`)
    const r3 = await axios.get(`/api/step3/${r2.data.id}`)
    return r3.data
  } catch (err) {
    console.error(err)
  }
}

3.2 async 函数的特点

// async 函数总是返回一个 Promise
async function fn() {
  return 42  // 等价于 return Promise.resolve(42)
}
fn().then(v => console.log(v))  // 42

// await 只能在 async 函数内使用
async function example() {
  // await 等待 Promise resolve,拿到值
  const result = await Promise.resolve('hello')
  console.log(result)  // 'hello'
  
  // await 暂停当前 async 函数,但不阻塞主线程!
  const data = await axios.get('/api/data')
  // data 就是 axios 响应对象(不是 Promise)
  console.log(data.data)
}

3.3 错误处理

// 方式一:try/catch(推荐,清晰)
async function fetchUser(id) {
  try {
    const { data } = await axios.get(`/api/users/${id}`)
    return data
  } catch (error) {
    console.error('获取用户失败:', error.response?.data?.message)
    return null
  }
}

// 方式二:.catch() 链(适合单个请求)
async function fetchUser(id) {
  const { data } = await axios.get(`/api/users/${id}`)
    .catch(err => {
      console.error(err)
      return { data: null }
    })
  return data
}

// 方式三:封装 await 的错误处理(进阶)
async function to(promise) {
  try {
    const result = await promise
    return [null, result]
  } catch (error) {
    return [error, null]
  }
}

async function fetchUser(id) {
  const [err, { data }] = await to(axios.get(`/api/users/${id}`))
  if (err) {
    console.error(err)
    return null
  }
  return data
}

3.4 并发请求(避免串行等待)

// ❌ 串行(效率低,共等待 3 秒)
async function slowVersion() {
  const r1 = await axios.get('/api/a')  // 等 1 秒
  const r2 = await axios.get('/api/b')  // 等 1 秒
  const r3 = await axios.get('/api/c')  // 等 1 秒
  return [r1.data, r2.data, r3.data]
}

// ✅ 并发(效率高,只等最长的那个)
async function fastVersion() {
  const [r1, r2, r3] = await Promise.all([
    axios.get('/api/a'),
    axios.get('/api/b'),
    axios.get('/api/c')
  ])
  return [r1.data, r2.data, r3.data]
}

四、Promise 实现原理(简版)

class MyPromise {
  constructor(executor) {
    this.state = 'pending'
    this.value = undefined
    this.reason = undefined
    this.onFulfilledCallbacks = []
    this.onRejectedCallbacks = []

    const resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled'
        this.value = value
        this.onFulfilledCallbacks.forEach(fn => fn(value))
      }
    }

    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected'
        this.reason = reason
        this.onRejectedCallbacks.forEach(fn => fn(reason))
      }
    }

    try {
      executor(resolve, reject)
    } catch (e) {
      reject(e)
    }
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        try {
          const result = onFulfilled(this.value)
          resolve(result)
        } catch (e) { reject(e) }
      }
      if (this.state === 'rejected') {
        try {
          const result = onRejected(this.reason)
          resolve(result)
        } catch (e) { reject(e) }
      }
      if (this.state === 'pending') {
        this.onFulfilledCallbacks.push(v => {
          try { resolve(onFulfilled(v)) } catch (e) { reject(e) }
        })
        this.onRejectedCallbacks.push(r => {
          try { resolve(onRejected(r)) } catch (e) { reject(e) }
        })
      }
    })
  }
}

五、知识图谱

Promise 与回调地狱
├── 回调地狱
│   ├── 原因:异步依赖导致嵌套
│   └── 问题:可读性差、错误分散、难维护
├── Promise
│   ├── 三种状态:pending → fulfilled/rejected(不可逆)
│   ├── 创建:new Promise((resolve, reject) => {...})
│   ├── 消费:.then(成功) / .catch(失败) / .finally(都执行)
│   ├── 链式:then 返回新 Promise,可串联
│   └── 静态方法
│       ├── all:全部成功才成功
│       ├── allSettled:全部完成(不管成败)
│       ├── race:第一个完成的决定结果
│       └── any:第一个成功的决定结果
└── async/await
    ├── async 函数返回 Promise
    ├── await 等待 Promise,暂停当前函数(不阻塞线程)
    ├── 错误处理:try/catch
    └── 并发:Promise.all 并行,避免串行等待

六、高频面试题

Q1:Promise 的三种状态是什么?状态能否改变?

pending(等待)、fulfilled(成功)、rejected(失败)。状态一旦从 pending 变为 fulfilled 或 rejected,就不可再改变(不可逆)。

Q2:async/await 和 Promise 的关系?

async/await 是 Promise 的语法糖。async 函数返回一个 Promise;await 只是 .then() 的更优雅写法,它暂停当前 async 函数执行,等待 Promise resolve 后继续,但不阻塞 JS 线程(事件循环继续运行)。

Q3:Promise.all 和 Promise.allSettled 的区别?

Promise.all:只要有一个 rejected 就立即失败,适合所有请求都必须成功的场景;Promise.allSettled:等所有 Promise 都完成(无论成败),返回每个的状态和值/原因,适合需要知道所有结果的场景。

Q4:以下代码的输出顺序?

console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('4')
// 输出:1 4 3 2
// 同步先执行(1,4),微任务优先(3),最后宏任务(2)

⬅️ 上一篇Day1 - HTTP协议与XHR基础 ➡️ 下一篇Day3 - Axios深入与请求拦截

昨天以前首页

React全家桶笔记(三):React进阶 — 事件处理、表单与生命周期

2026年4月1日 21:33

React全家桶笔记(三):React进阶 — 事件处理、表单与生命周期

本篇涵盖 React 事件处理机制、受控/非受控组件、高阶函数与柯里化、生命周期(新旧对比)、Diffing 算法。这些是从"会用"到"理解原理"的关键跨越。 📺 对应张天禹react全家桶视频:P32 - P48


一、React 中的事件处理(P32)

1.1 事件处理机制

class Demo extends React.Component {
  myRef = React.createRef()

  // 发生事件的元素正好是你要操作的元素 → 可以省略 ref
  showData = (event) => {
    alert(event.target.value)
  }

  render() {
    return (
      <div>
        <input onBlur={this.showData} type="text" placeholder="失去焦点提示" />
      </div>
    )
  }
}

React 事件处理的两个要点

  1. React 使用的是自定义(合成)事件,而不是原生 DOM 事件 — 为了更好的兼容性
  2. React 中的事件是通过事件委托方式处理的(委托给组件最外层的元素) — 为了高效

💡 实践建议:不要过度使用 ref。当发生事件的元素就是你要操作的元素时,可以通过 event.target 获取 DOM,不需要 ref。


二、受控组件与非受控组件(P33-P34)

2.1 非受控组件(P33)

表单数据在需要时才通过 ref "现取现用":

class Login extends React.Component {
  handleSubmit = (event) => {
    event.preventDefault() // 阻止表单默认提交行为
    const { username, password } = this
    alert(`用户名:${username.value},密码:${password.value}`)
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        用户名:<input ref={c => this.username = c} type="text" name="username" />
        密码:<input ref={c => this.password = c} type="password" name="password" />
        <button>登录</button>
      </form>
    )
  }
}

特点:输入类 DOM 的值是"现用现取"的,页面中的表单数据由 DOM 自身管理。

2.2 受控组件(P34)— 推荐

表单数据随着输入实时维护到 state 中,需要时从 state 取:

class Login extends React.Component {
  state = {
    username: '',
    password: ''
  }

  saveUsername = (event) => {
    this.setState({ username: event.target.value })
  }

  savePassword = (event) => {
    this.setState({ password: event.target.value })
  }

  handleSubmit = (event) => {
    event.preventDefault()
    const { username, password } = this.state
    alert(`用户名:${username},密码:${password}`)
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        用户名:<input onChange={this.saveUsername} type="text" name="username" />
        密码:<input onChange={this.savePassword} type="password" name="password" />
        <button>登录</button>
      </form>
    )
  }
}

特点:输入类 DOM 的值实时存入 state,等于 Vue 中的双向绑定效果。推荐使用,因为可以省略 ref。

🎯 面试高频:受控组件 vs 非受控组件

  • 受控组件:表单数据由 React 的 state 管理,输入即存储,类似 Vue 的 v-model
  • 非受控组件:表单数据由 DOM 自身管理,需要时通过 ref 获取
  • 推荐受控组件,因为数据集中管理,且不需要大量 ref

三、高阶函数与函数柯里化(P35-P36)

3.1 问题引出

上面的受控组件中,每个表单项都要写一个 saveXxx 方法,如果有 20 个表单项就要写 20 个方法,太冗余了。

3.2 用柯里化优化(P35)

class Login extends React.Component {
  state = { username: '', password: '' }

  // 高阶函数 + 柯里化:返回一个函数作为事件回调
  saveFormData = (dataType) => {
    return (event) => {
      this.setState({ [dataType]: event.target.value })
    }
  }

  handleSubmit = (event) => {
    event.preventDefault()
    const { username, password } = this.state
    alert(`用户名:${username},密码:${password}`)
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        {/* 注意:onChange 的值必须是一个函数,这里 saveFormData('username') 的返回值就是一个函数 */}
        用户名:<input onChange={this.saveFormData('username')} type="text" />
        密码:<input onChange={this.saveFormData('password')} type="password" />
        <button>登录</button>
      </form>
    )
  }
}

概念解析

高阶函数:满足以下任一条件的函数

  • 接收的参数是一个函数(如 PromisesetTimeoutarr.map()
  • 返回值是一个函数(如上面的 saveFormData

函数柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式

// 普通函数
function sum(a, b, c) { return a + b + c }
sum(1, 2, 3) // 6

// 柯里化
function sum(a) {
  return (b) => {
    return (c) => {
      return a + b + c
    }
  }
}
sum(1)(2)(3) // 6

3.3 不用柯里化的写法(P36)

class Login extends React.Component {
  state = { username: '', password: '' }

  saveFormData = (dataType, event) => {
    this.setState({ [dataType]: event.target.value })
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        {/* 用箭头函数包一层,在回调中自己调用 saveFormData */}
        用户名:<input onChange={event => this.saveFormData('username', event)} type="text" />
        密码:<input onChange={event => this.saveFormData('password', event)} type="password" />
        <button>登录</button>
      </form>
    )
  }
}

🔗 概念扩展:两种写法的本质 不管是柯里化还是箭头函数包裹,核心目的都是一样的:确保 onChange 的值是一个函数,同时能把额外的参数(dataType)传进去。


四、生命周期(P37-P47)

4.1 引出生命周期(P37)

class Life extends React.Component {
  state = { opacity: 1 }

  // 组件挂载完毕后调用
  componentDidMount() {
    this.timer = setInterval(() => {
      let { opacity } = this.state
      opacity -= 0.1
      if (opacity <= 0) opacity = 1
      this.setState({ opacity })
    }, 200)
  }

  // 组件将要卸载时调用 — 适合做收尾工作(清除定时器、取消订阅等)
  componentWillUnmount() {
    clearInterval(this.timer)
  }

  death = () => {
    // 卸载组件
    ReactDOM.unmountComponentAtNode(document.getElementById('test'))
  }

  render() {
    return (
      <div>
        <h2 style={{opacity: this.state.opacity}}>React 学不会怎么办?</h2>
        <button onClick={this.death}>不活了</button>
      </div>
    )
  }
}

生命周期(又叫生命周期回调函数、生命周期钩子函数):React 组件从创建到销毁会经历一系列特定阶段,React 会在特定时刻调用特定的方法,这些方法就是生命周期钩子。

4.2 生命周期(旧)— React 16 之前(P38-P42)

挂载阶段(Mount)— P38

constructor()                → 构造器
componentWillMount()         → 组件将要挂载
render()                     → 渲染
componentDidMount()          → 组件挂载完毕 ⭐ 常用

更新阶段(Update)— P39-P41

三种触发更新的方式:

方式1setState() — P39
shouldComponentUpdate()      → 组件是否应该更新(返回 true/false,默认返回 true)
componentWillUpdate()        → 组件将要更新
render()                     → 重新渲染
componentDidUpdate()         → 组件更新完毕

方式2forceUpdate() — P40(强制更新,跳过 shouldComponentUpdate)
componentWillUpdate()        → 组件将要更新
render()                     → 重新渲染
componentDidUpdate()         → 组件更新完毕

方式3:父组件重新 render — P41
componentWillReceiveProps()  → 组件将要接收新的 props ⚠️ 第一次不算
shouldComponentUpdate()      → 组件是否应该更新
componentWillUpdate()        → 组件将要更新
render()                     → 重新渲染
componentDidUpdate()         → 组件更新完毕

卸载阶段(Unmount)

componentWillUnmount()       → 组件将要卸载 ⭐ 常用

旧版生命周期总结(P42)

旧版生命周期流程图:
┌─ 挂载时 ──────────────────────────────────┐
│  constructor → componentWillMount → render │
│  → componentDidMount                       │
└────────────────────────────────────────────┘
┌─ 更新时 ──────────────────────────────────────────────────┐
│  父组件render → componentWillReceiveProps                  │
│  → shouldComponentUpdate(true) → componentWillUpdate       │
│  → render → componentDidUpdate                             │
│                                                            │
│  setState → shouldComponentUpdate(true)                    │
│  → componentWillUpdate → render → componentDidUpdate       │
│                                                            │
│  forceUpdate → componentWillUpdate → render                │
│  → componentDidUpdate                                      │
└────────────────────────────────────────────────────────────┘
┌─ 卸载时 ──────────────────────────────────┐
│  componentWillUnmount                      │
└────────────────────────────────────────────┘

4.3 对比新旧生命周期(P43)

React 17+ 中,三个带 Will 的钩子被标记为不安全(UNSAFE),需要加 UNSAFE_ 前缀才能使用:

❌ componentWillMount        → UNSAFE_componentWillMount
❌ componentWillUpdate       → UNSAFE_componentWillUpdate
❌ componentWillReceiveProps → UNSAFE_componentWillReceiveProps

为什么废弃? 这三个钩子经常被误用/滥用,在 React 未来的异步渲染(Fiber)中可能会出问题。

新版生命周期新增了两个钩子:

  • getDerivedStateFromProps — 从 props 派生 state
  • getSnapshotBeforeUpdate — 在更新前获取快照

4.4 getDerivedStateFromProps(P44)

class Count extends React.Component {
  state = { count: 0 }

  // 注意:这是一个 static 方法,接收 props 和 state
  // 返回一个状态对象或 null
  static getDerivedStateFromProps(props, state) {
    console.log('getDerivedStateFromProps', props, state)
    // 返回的对象会与 state 合并
    // 如果返回 null,则不影响 state
    return null
  }

  render() {
    return <h1>当前求和为:{this.state.count}</h1>
  }
}

使用场景:state 的值在任何时候都取决于 props 时使用(极少用)。一旦使用,state 就会被 props "控制"住。

⚠️ 这个钩子使用场景非常罕见,了解即可。

4.5 getSnapshotBeforeUpdate(P45-P46)

class NewsList extends React.Component {
  state = { newsArr: [] }

  componentDidMount() {
    setInterval(() => {
      const { newsArr } = this.state
      const news = `新闻${newsArr.length + 1}`
      this.setState({ newsArr: [news, ...newsArr] })
    }, 1000)
  }

  // 在更新之前获取快照(DOM 更新前的信息)
  // 返回值会作为 componentDidUpdate 的第三个参数
  getSnapshotBeforeUpdate() {
    return this.refs.list.scrollHeight
  }

  componentDidUpdate(prevProps, prevState, snapshotValue) {
    // snapshotValue 就是 getSnapshotBeforeUpdate 的返回值
    // 用来保持滚动位置不变
    this.refs.list.scrollTop += this.refs.list.scrollHeight - snapshotValue
  }

  render() {
    return (
      <div ref="list" className="list">
        {this.state.newsArr.map((n, index) => (
          <div key={index} className="news">{n}</div>
        ))}
      </div>
    )
  }
}

使用场景:在 DOM 更新前捕获一些信息(如滚动位置),传递给 componentDidUpdate 使用。

4.6 新版生命周期总结(P47)

新版生命周期流程图:
┌─ 挂载时 ──────────────────────────────────────────────┐
│  constructor → getDerivedStateFromProps → render        │
│  → componentDidMount ⭐                                │
└────────────────────────────────────────────────────────┘
┌─ 更新时 ──────────────────────────────────────────────────────────┐
│  getDerivedStateFromProps → shouldComponentUpdate(true)             │
│  → render → getSnapshotBeforeUpdate → componentDidUpdate           │
└────────────────────────────────────────────────────────────────────┘
┌─ 卸载时 ──────────────────────────────────────────────┐
│  componentWillUnmount ⭐                               │
└────────────────────────────────────────────────────────┘

最重要的三个钩子

componentDidMount    → 组件挂载完毕
  常用于:发送网络请求、订阅消息、开启定时器

componentDidUpdate   → 组件更新完毕
  常用于:根据更新后的 props/state 做操作

componentWillUnmount → 组件将要卸载
  常用于:清除定时器、取消订阅、清理工作

🎯 面试高频:React 新旧生命周期的区别?

  1. 废弃了三个 Will 钩子(componentWillMount/Update/ReceiveProps)
  2. 新增了 getDerivedStateFromProps(从 props 派生 state)和 getSnapshotBeforeUpdate(更新前快照)
  3. 废弃原因:为 React 未来的异步渲染(Concurrent Mode)做准备
  4. 最常用的仍然是:componentDidMountcomponentWillUnmount

五、DOM 的 Diffing 算法(P48)

5.1 Diffing 算法的最小粒度

Diffing 算法对比的最小粒度是标签(节点) ,不是整棵树。

// 假设 state 中 time 每秒更新一次
render() {
  return (
    <div>
      <h1>Hello</h1>
      <span>
        现在是:{this.state.time}
      </span>
    </div>
  )
}
// Diffing 对比时:
// <h1>Hello</h1> → 没变,不更新
// <span>现在是:xxx</span> → 内容变了,只更新这个 span

5.2 key 的作用

// 用 index 作为 key 的问题演示
// 初始数据:
//   { id: 1, name: '小张', age: 18 }
//   { id: 2, name: '小李', age: 19 }

// 初始虚拟 DOM(用 index 作 key):
<li key={0}>小张---18 <input type="text"/></li>
<li key={1}>小李---19 <input type="text"/></li>

// 在头部插入 { id: 3, name: '小王', age: 20 } 后:
<li key={0}>小王---20 <input type="text"/></li>  // key=0 对比:内容变了,更新!
<li key={1}>小张---18 <input type="text"/></li>  // key=1 对比:内容变了,更新!
<li key={2}>小李---19 <input type="text"/></li>  // key=2:新增

// 如果用 id 作 key:
<li key={3}>小王---20 <input type="text"/></li>  // key=3:新增,只创建这一个
<li key={1}>小张---18 <input type="text"/></li>  // key=1 对比:没变,复用!
<li key={2}>小李---19 <input type="text"/></li>  // key=2 对比:没变,复用!

5.3 用 index 作为 key 的问题

  1. 效率问题:逆序添加、逆序删除等破坏顺序的操作,会产生没有必要的真实 DOM 更新(界面没问题,但效率低)
  2. 严重 Bug:如果结构中包含输入类 DOM(input),会产生错误的 DOM 更新(输入框内容错位)

5.4 key 的选择原则

key 的选择:
├── 最好使用数据的唯一标识(id、手机号、身份证号等)
├── 如果只是简单的展示数据(不涉及逆序操作),用 index 也可以
└── 绝对不要用 Math.random() 作为 key

🎯 面试高频:React/Vue 中 key 的作用和原理?

  1. key 是虚拟 DOM 对象的标识,在更新时起关键作用

  2. 当数据变化时,React 生成新的虚拟 DOM,然后与旧的进行 Diff 对比

  3. 对比规则:

    1. 旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key:

      • 内容没变 → 直接复用之前的真实 DOM
      • 内容变了 → 生成新的真实 DOM,替换掉旧的
    2. 旧虚拟 DOM 中未找到与新虚拟 DOM 相同的 key:

      • 创建新的真实 DOM,渲染到页面

本章知识图谱

React 进阶
├── 事件处理
│   ├── 合成事件(非原生事件)
│   ├── 事件委托机制
│   └── event.target 可以省略 ref
├── 表单处理
│   ├── 非受控组件:ref 现用现取
│   └── 受控组件:onChange + state 实时存储(推荐)
├── 高阶函数与柯里化
│   ├── 高阶函数:参数或返回值是函数
│   ├── 柯里化:多次接收参数,最后统一处理
│   └── 替代方案:箭头函数包裹
├── 生命周期
│   ├── 旧版:Will 系列 + Did 系列
│   ├── 新版:废弃 3 个 Will,新增 2get
│   ├── 最常用:componentDidMount / componentWillUnmount
│   └── 废弃原因:为异步渲染(Concurrent Mode)铺路
└── Diffing 算法
    ├── 最小对比粒度:标签(节点)
    ├── key 是虚拟 DOM 的标识
    ├── 用 id 作 key(推荐)
    └── 用 index 作 key 的两个问题:效率低 + 输入框错位

📌 下一篇:[React全家桶笔记(四):React脚手架与TodoList实战] 将进入工程化开发阶段,学习 Create React App 脚手架和第一个完整的实战案例。

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

2026年4月1日 21:32

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'))

执行流程

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

⚠️ 注意:函数式组件中的 thisundefined,因为 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}年级`)
  }
}

类的核心知识点

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

四、类式组件(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'))

执行流程

  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)

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?

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

5.5 setState 的使用(P17)

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)

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 绑定写法:更新时调用一次
└── createRefmyRef = 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 最重要的生命周期机制。

❌
❌