函数式组件的渲染

1,086 阅读3分钟

"组件每一次渲染都会有自己的props和state。每一次渲染都会有自己的事件处理函数。"接下来,我们来深刻理解这句话。

function Counter(){
  const [count, setCount] = useState(0);
  return (
  	<div>
  		<p>you clicked {count} times</p>
    	<button onClick={() => setCount(count+1)}>+++</button>
  	</div>
  )
}

这段代码里,我们如果点击button,显示的count的值确实会增加。它的原理是count“监听”状态的变化然后自动更新吗?其实并不是。count不是双向绑定或者“watcher”或者其他任何东西,它只是一个普通的变量。

const count = 0;
...
<p>you clicked {count} times</p>

初始状态下,count值是0。当调用setCount(1)的时候,React重新渲染了组件,这次count的值是1。也就是说每次渲染拿到的count值是独立的,不是同一份。

// During first render
function Counter() {
  const count = 0; // Returned by useState()
  // ...
  <p>You clicked {count} times</p>
  // ...
}

// After a click, our function is called again
function Counter() {
  const count = 1; // Returned by useState()
  // ...
  <p>You clicked {count} times</p>
  // ...
}

// After another click, our function is called again
function Counter() {
  const count = 2; // Returned by useState()
  // ...
  <p>You clicked {count} times</p>
  // ...
}

关键在于,任意一次渲染中,const常量是不会变化的,我们看到的输出变化,是因为组件被重新调用了,并且传入了不同的参数,这个参数独立于其他任何一次渲染。

对于基本变量来说是这样的,那事件处理函数的情况呢?

答案其实一样,函数式组件的渲染,每次都会从上到下执行一遍代码。

function Counter(){
  const [count, setCount] = useState(0);
  function handleClick(){
    setTimeout(() => {
      console.log(count);
    },5000);
  }
  return (
    <>
      <p>you clicked {count} times</p>
      <button onClick={() => setCount(count+1)}>+++</button>
      <button onClick={handleClick}>console</button>
    </>
  )
}

如果我们在点击完console button后的5s时间内点击了多次+++按钮,请问会输出什么?答案是:0。

它的原理就类似于普通函数:

function sayHi(person){
	setTimeout(()=>{console.log(person.name)}, 1000);
}
let someone = {name: "Dan"};
sayHi(someone); // "Dan"

someone = {name: "Mike"};
sayHi(someone); // "Mike"

someone = {name: "John"};
sayHi(someone); // "John"

即使在console.log执行之前,someone已经被赋予了新值,但在sayHi函数内,person会和某次调用的someone关联,也就是说每次调用的person都是独立的,虽然名字一样,但保存在不同的空间里。

同样的,组件每次渲染都会有新的handleClick函数,它会记住这次渲染的count值。虽然名字和之前渲染的handleClick一样,但他们是独立的。也就是说,事件处理函数“属于”某一次特定的渲染。

再来看看useEffect这个Hook,它和上面分析的结果一样,每次渲染都会有自己的Effects。直接上代码:

function Counter(){
  const [count, setCount] = useState(0);
  useEffect(() => {
    setTimeout(()=>{
      console.log(count);
    }, 3000);
  })
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  )
}

这段代码,如果在3s内连续点击五次,会依次输出0,1,2,3,4,5。因为每次调用effect,它看到的都是属于本次渲染的唯一的count。

总的来说,每一次渲染的组件都会拥有它自己的所有东西(props、state、事件处理函数、effects、定时器、API调用等)。

看完这个,也了解了为什么说多个state最好写多次useState,而不是写在一个对象里。因为函数式组件的setState操作不会合并之前的state,而是用新的替换旧的。非要用对象的话,可以使用对象结构符。