Skip to content

组件的状态

React有两个核心:组件化和数据驱动视图。其中数据的状态就可以改变视图。
在React中,渲染视图一共分为三个步骤:

  1. 组件的初次渲染,底层会执行createRoot().render():
    Alt text 并且内部状态更新,触发渲染送入队列。
  2. 渲染你的组件: 在进行初次渲染时,React会调用根组件,内部状态更新,会渲染对应的函数组件。
  3. 提交到DOM上: 初次渲染,调用appendChild()的DOM API, 内部状态更新,更新差异的DOM节点。

1. 数据状态state

随时间变化的数据被称为状态(state),状态是可以进行数据驱动视图的,而普通变量是不行的。

jsx
function App() {
  let count = 0;
  const clickHandler = () => {
    count++;
  }
  return (
    <div>
      hello App
      <button onClick={clickHandler} >点击</button>
      <div>
        {count}
      </div>
    </div>
  )
}
export default App

运行结果:
Alt text React提供修改状态的方法setState方法:

jsx
import { useState } from 'react'

function App() {
  // 初始化count为0
  const [count, setCount] = useState(0)
  const clickHandler = () => {
    // count++; ❌
    setCount(count + 1)
  }
  return (
    <div>
      hello App
      <button onClick={clickHandler} >点击</button>
      <div>
        {count}
      </div>
    </div>
  )
}
export default App

页面效果:
Alt text

2. state改变视图原理

由上面可知,直接改变count变量不会改变视图,原因在于改变变量后并没有执行重新return,也就是说return里面的组件只会执行一次并且只在第一次页面加载在按钮点击之前执行:

jsx
function App() {
  let count = 0;
  const clickHandler = () => {
    count++;
    console.log(count)
  }
  console.log("开始执行return,进行渲染jsx....")
  return (
    <div>
      hello App
      <button onClick={clickHandler} >点击</button>
      <div>
        {count}
      </div>
    </div>
  )
}
export default App

执行效果:
Alt text 可以看到点击按钮并没有触发return执行,而我们使用useState中的setState(第二个参数就是设置状态的函数,一般取名为setXXX),会重新触发函数组件执行,并且state状态具备组件的记忆。使用useState并添加console.log()查看控制台:

jsx
import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  const clickHandler = () => {
    setCount(count + 1)
    console.log(count)
  }
  console.log("开始执行return,进行渲染jsx....")
  return (
    <div>
      hello App
      <button onClick={clickHandler} >点击</button>
      <div>
        {count}
      </div>
    </div>
  )
}
export default App

执行结果:
Alt text 可以看到点击按钮不仅触发事件的处理函数,而且还重新执行了App()函数。另外理解state状态具有记忆能力它会在之前结果基础上进行计算,相比普通变量在函数里面申明多次执行结果都是不变的,比如下面:

js
function foo(){
  let count = 0;
  count++;
  console.log(count);
}
foo(); // 结果1
foo(); // 结果还是1
foo(); // 结果还是1, 因为count每次都是新的变量

但是useState调用为何能被准确记忆呢?如下调用3次useState(0),但是点击按钮后返回值count会发生变化,count2,count3没有变化。

jsx
import { useState } from 'react'

let num=0;
function App() {
  const [count, setCount] = useState(0)
  if(num==0){
    // useState不能代码逻辑中
    const [count2, setCount2] = useState(0)
  }
  const [count3, setCount3] = useState(0)
  const clickHandler = () => {
    setCount(count3 + 1)
    num++;
  }
  console.log("开始执行return,进行渲染jsx....")
  return (
    <div>
      hello App
      <button onClick={clickHandler} >点击</button>
      <div>
        {count}, {count3}
      </div>
    </div>
  )
}
export default App

执行结果:
Alt text 原因是在React内部,为每个组件保存了一个数组,其中每一项都是一个state对。它维护当前state对的索引值,在渲染之前将其设置为"0"。每次调用useState时,React都会为你提供一个state对并增加索引值。

警告

不要在逻辑中调用useState,会改变内部对state的顺序。比如:

jsx
import { useState } from 'react'

let num=0

function App() {
  const [count, setCount] = useState(0)
  if(num==0){
    // useState的顺序受到逻辑代码影响
    const [count2, setCount2] = useState(0)
  }
  const [count3, setCount3] = useState(0)
  const clickHandler = () => {
    setCount(count3+ 1)
    num++
  }
  console.log("开始执行return,进行渲染jsx....")
  return (
    <div>
      hello App
      <button onClick={clickHandler} >点击</button>
      <div>
        {count}, {count3}
      </div>
    </div>
  )
}
export default App

运行报错: Alt text 可以看出如果改变useState状态不仅控制台报错,而且页面也会崩溃不会渲染任务内容。

State在其表现上更像是一张快照,设置它不会更改你已有的state变量,但会触发更新渲染。

jsx
import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  const clickHandler = () => {
    setCount(count+ 1)
    setCount(count+ 1)
    setCount(count+ 1)
    console.log(count) 
  }
  console.log("开始执行return,进行渲染jsx....")
  return (
    <div>
      hello App
      <button onClick={clickHandler} >点击</button>
      <div>
        {count}
      </div>
    </div>
  )
}
export default App

当点击一次按钮,发现打印count还是0,页面显示1:
Alt text 原因在于setCount()函数并没有直接更改count的值,所以count在console.log打印的时候count值并没有改变。setCount()函数的作用是影响下一次渲染和打印的值。在一次点击的触发事件如果多次执行setCount(),结果只会以最后一次setCount()为准,而return里面的组件使用的{count}就是在触发事件渲染才读取的,属于下一次读取count的范围(可以理解当前渲染读取已经发生了,发生了才有按钮点击触发事件)。但如果我是用定时器呢:

jsx
import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  const clickHandler = () => {
    setCount(count+ 1)
    setCount(count+ 1)
    setCount(count+ 1)
    // 异步执行在页面渲染完毕再执行
    setTimeout(() => { console.log(count) }, 5000)
  }
  console.log("开始执行return,进行渲染jsx....")
  return (
    <div>
      hello App
      <button onClick={clickHandler} >点击</button>
      <div>
        {count}
      </div>
    </div>
  )
}
export default App

执行结果:
Alt text React会保证state值在一次渲染的各个事件处理函数(包括异步)内部是固定不变的,底层利用了函数闭包的特性:
Alt text

3. 状态队列和自动批处理

React 会等到事件处理函数中的所有代码都运行完毕再处理你的state更新。队列都执行完毕后,再进行UI更新,这种特性就是自动批处理。
比如前面提到的setCount执行会触发React进行渲染,你会发现执行3次setCount(),页面只触发执行一次执行App()组件函数进行渲染:

jsx
import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  const clickHandler = () => {
    setCount(count+ 1)
    setCount(count+ 1)
    setCount(count+ 1)
    console.log(count)
  }
  console.log("开始执行return,进行渲染jsx....")
  return (
    <div>
      hello App
      <button onClick={clickHandler} >点击</button>
      <div>
        {count}
      </div>
    </div>
  )
}
export default App

点击一次按钮,执行结果:
Alt text 可以看到执行渲染只会走一次。但如果是在setCount中使用回调函数的话:

jsx
import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  const clickHandler = () => {
    // setCount(count+ 1)
    // setCount(count+ 1)
    // setCount(count+ 1)
    setCount((c)=> c + 1)
    setCount((c)=> c + 1)
    setCount((c)=> c + 1)
    console.log(count)
  }
  console.log("开始执行return,进行渲染jsx....")
  return (
    <div>
      hello App
      <button onClick={clickHandler} >点击</button>
      <div>
        {count}
      </div>
    </div>
  )
}
export default App

Alt text 在setCount中使用回调函数,就不再使用count值了,而是使用形参c,形参c在React调用回调函数的时候传入的是内部保存的count值,区别不是我们解构的count值。所以每次setCount都会改变内部的count值。实际上setCount(x)底层就是执行的setCount((c)=>x)运行逻辑,只不过没有用到形参c而已。

4. 渲染的条件

修改state状态的值如果没有发生变化,是不会触发渲染的:

jsx
import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  const clickHandler = () => {
    setCount(0)
    console.log(count)
  }
  console.log("开始执行return,进行渲染jsx....")
  return (
    <div>
      hello App
      <button onClick={clickHandler} >点击</button>
      <div>
        {count}
      </div>
    </div>
  )
}
export default App

运行点击按钮发现并没有进行渲染,也就是执行事件函数之外的console.log部分 Alt text 如果将setCount(0)改为setCount(1),会发现第二次的时候还是会执行(第一次会执行是因为state变化了0变成1)。

jsx
import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  const clickHandler = () => {
    setCount(1)
    console.log(count)
  }
  console.log("开始执行return,进行渲染jsx....")
  return (
    <div>
      hello App
      <button onClick={clickHandler} >点击</button>
      <div>
        {count}, {Math.random()}
      </div>
    </div>
  )
}
export default App

点击按钮运行结果:
Alt text 但是虽然第二次会执行,但是会发现渲染页面上的随机数没有发生改变,这是因为第二次并没有真正的执行,底层是React执行了自检流程。所以修改状态的值没有发生改变的时候,函数组件并不会重新渲染。