每一次渲染都有它自己的state和Props

原文

先看例子:

  1. function Counter() {
  2. const [count, setCount] = useState(0);
  3. return (
  4. <div>
  5. // 注意下面使用了 count 的这一行
  6. <p>You clicked {count} times</p>
  7. <button onClick={() => setCount(count + 1)}>
  8. Click me
  9. </button>
  10. </div>
  11. );
  12. }

count 会“监听”状态的变化并自动更新吗?这么想可能是学习React的时候有用的第一直觉,但它并不是精确的心智模型

上面例子中,count仅是一个数字而已。它不是神奇的“data binding”, “watcher”, “proxy”,或者其他任何东西。它就是一个普通的数字像下面这个一样:

  1. const count = 42;
  2. // ...
  3. <p>You clicked {count} times</p>
  4. // ...

我们的组件第一次渲染的时候,从useState()拿到count的初始值0。当我们调用setCount(1),React会再次渲染组件,这一次count是1。如此等等:

  1. // 第一次执行
  2. function Counter() {
  3. const count = 0; // Returned by useState()
  4. // ...
  5. <p>You clicked {count} times</p>
  6. // ...
  7. }
  8. // 再点击一次,函数再一次调用
  9. function Counter() {
  10. const count = 1; // Returned by useState()
  11. // ...
  12. <p>You clicked {count} times</p>
  13. // ...
  14. }
  15. // 再点击一次,函数再一次调用
  16. function Counter() {
  17. const count = 2; // Returned by useState()
  18. // ...
  19. <p>You clicked {count} times</p>
  20. // ...
  21. }

当我们更新状态的时候,React会重新渲染组件。每一次渲染都能拿到独立的count 状态,这个状态值是函数中的一个常量。


所以下面的这行代码没有做任何特殊的数据绑定:

  1. <p>You clicked {count} times</p>


它仅仅只是在渲染输出中插入了count这个数字。这个数字由React提供。当setCount的时候,React会带着一个不同的count值再次调用组件。然后,React会更新DOM以保持和渲染输出一致。

这里关键的点在于任意一次渲染中的count常量都不会随着时间改变。渲染输出会变是因为我们的组件被一次次调用,而每一次调用引起的渲染中,它包含的count值独立于其他渲染。

(关于这个过程更深入的探讨可以查看我的另一篇文章 React as a UI Runtime。)

笔记

感觉函数组件的数据更新更像是通过闭包实现

  1. // 普通的一个闭包函数
  2. function ShowNum(x=0){
  3. return function(){
  4. console.log(x)
  5. }
  6. }
  7. const ShowNum10 = ShowNum(10)
  8. ShowNum10()
  9. const ShowNum20 = ShowNum(20)
  10. ShowNum20()
  11. // 函数组件
  12. function Counter(props) {
  13. const [count, setCount] = useState(0);
  14. return (
  15. <div>
  16. // 注意下面使用了 count 的这一行
  17. <p>数值:{count} </p>
  18. <button onClick={() => setCount(props.value)}>
  19. Click me
  20. </button>
  21. </div>
  22. );
  23. }

假设Counter是一个函数组件,实参x就是函数组件的state或者props
当我们执行了一次下面代码

  1. const ShowNum10 = ShowNum(10)

就相当于函数组件Counter点击了一次按钮,执行了下面代码

  1. // props.value 等于10
  2. setCount(props.value)

所以原文例子中每一次点击应该都类似于执行了一次次闭包函数

根据原文解释 当我们更新状态的时候,React会重新渲染组件。每一次渲染都能拿到独立的count 状态,这个状态值是函数中的一个常量。

所以函数组件每一次更新状态应该就是重新调用一次函数组件,更新闭包的值。类似于下面代码

  1. const ShowNum10 = ShowNum(10)
  2. ShowNum10()
  3. const ShowNum20 = ShowNum(20)
  4. ShowNum20()

任意一次渲染中的count常量都不会随着时间改变。渲染输出会变是因为我们的组件被一次次调用,而每一次调用引起的渲染中,它包含的count值独立于其他渲染。

:::info 以上的都是个人阅读文章后的个人理解(作为笔记,可能描述和逻辑表述不清见谅),如果有误欢迎指导 :::

每一次渲染都有它自己的事件处理函数

原文

到目前为止一切都还好。那么事件处理函数呢?

看下面的这个例子。它在三秒后会alert点击次数count:

  1. function Counter() {
  2. const [count, setCount] = useState(0);
  3. function handleAlertClick() {
  4. setTimeout(() => {
  5. alert('You clicked on: ' + count);
  6. }, 3000);
  7. }
  8. return (
  9. <div>
  10. <p>You clicked {count} times</p>
  11. <button onClick={() => setCount(count + 1)}>
  12. Click me
  13. </button>
  14. <button onClick={handleAlertClick}>
  15. Show alert
  16. </button>
  17. </div>
  18. );
  19. }

如果我按照下面的步骤去操作:

  • 点击增加counter到3
  • 点击一下 “Show alert”
  • 点击增加 counter到5并且在定时器回调触发前完成

counter.gif

你猜alert会弹出什么呢?会是5吗?— 这个值是alert的时候counter的实时状态。或者会是3吗?— 这个值是我点击时候的状态。

来自己 试试吧!

如果结果和你预料不一样,你可以想象一个更实际的例子:一个聊天应用在state中保存了当前接收者的ID,以及一个发送按钮。 这篇文章深入探索了个中缘由。正确的答案就是3。

alert会“捕获”我点击按钮时候的状态。


(虽然有其他办法可以实现不同的行为,但现在我会专注于这个默认的场景。当我们在构建一种心智模型的时候,在可选的策略中分辨出“最小阻力路径”是非常重要的。)

但它究竟是如何工作的呢?

我们发现count在每一次函数调用中都是一个常量值。值得强调的是 — 我们的组件函数每次渲染都会被调用,但是每一次调用中count值都是常量,并且它被赋予了当前渲染中的状态值。


这并不是React特有的,普通的函数也有类似的行为:

  1. function sayHi(person) {
  2. const name = person.name;
  3. setTimeout(() => {
  4. alert('Hello, ' + name);
  5. }, 3000);
  6. }
  7. let someone = {name: 'Dan'};
  8. sayHi(someone);
  9. someone = {name: 'Yuzhi'};
  10. sayHi(someone);
  11. someone = {name: 'Dominic'};
  12. sayHi(someone);

这个例子中, 外层的someone会被赋值很多次(就像在React中,当前的组件状态会改变一样)。然后,在sayHi函数中,局部常量name会和某次调用中的person关联。因为这个常量是局部的,所以每一次调用都是相互独立的。结果就是,当定时器回调触发的时候,每一个alert都会弹出它拥有的name。

这就解释了我们的事件处理函数如何捕获了点击时候的count值。如果我们应用相同的替换原理,每一次渲染“看到”的是它自己的count:

  1. // During first render
  2. function Counter() {
  3. const count = 0; // Returned by useState()
  4. // ...
  5. function handleAlertClick() {
  6. setTimeout(() => {
  7. alert('You clicked on: ' + count);
  8. }, 3000);
  9. }
  10. // ...
  11. }
  12. // After a click, our function is called again
  13. function Counter() {
  14. const count = 1; // Returned by useState()
  15. // ...
  16. function handleAlertClick() {
  17. setTimeout(() => {
  18. alert('You clicked on: ' + count);
  19. }, 3000);
  20. }
  21. // ...
  22. }
  23. // After another click, our function is called again
  24. function Counter() {
  25. const count = 2; // Returned by useState()
  26. // ...
  27. function handleAlertClick() {
  28. setTimeout(() => {
  29. alert('You clicked on: ' + count);
  30. }, 3000);
  31. }
  32. // ...
  33. }

所以实际上,每一次渲染都有一个“新版本”的handleAlertClick。每一个版本的handleAlertClick“记住” 了它自己的 count:

  1. // During first render
  2. function Counter() {
  3. // ...
  4. function handleAlertClick() {
  5. setTimeout(() => {
  6. alert('You clicked on: ' + 0);
  7. }, 3000);
  8. }
  9. // ...
  10. <button onClick={handleAlertClick} /> // The one with 0 inside
  11. // ...
  12. }
  13. // After a click, our function is called again
  14. function Counter() {
  15. // ...
  16. function handleAlertClick() {
  17. setTimeout(() => {
  18. alert('You clicked on: ' + 1);
  19. }, 3000);
  20. }
  21. // ...
  22. <button onClick={handleAlertClick} /> // The one with 1 inside
  23. // ...
  24. }
  25. // After another click, our function is called again
  26. function Counter() {
  27. // ...
  28. function handleAlertClick() {
  29. setTimeout(() => {
  30. alert('You clicked on: ' + 2);
  31. }, 3000);
  32. }
  33. // ...
  34. <button onClick={handleAlertClick} /> // The one with 2 inside
  35. // ...
  36. }

这就是为什么在这个demo中中,事件处理函数“属于”某一次特定的渲染,当你点击的时候,它会使用那次渲染中counter的状态值。

在任意一次渲染中,props和state是始终保持不变的。如果props和state在不同的渲染中是相互独立的,那么使用到它们的任何值也是独立的(包括事件处理函数)。它们都“属于”一次特定的渲染。即便是事件处理中的异步函数调用“看到”的也是这次渲染中的count值。

备注:上面我将具体的count值直接内联到了handleAlertClick函数中。这种心智上的替换是安全的因为count 值在某次特定渲染中不可能被改变。它被声明成了一个常量并且是一个数字。这样去思考其他类型的值比如对象也同样是安全的,当然需要在我们都同意应该避免直接修改state这个前提下。通过调用setSomething(newObj)的方式去生成一个新的对象而不是直接修改它是更好的选择,因为这样能保证之前渲染中的state不会被污染。

每次渲染都有它自己的Effects