每一次渲染都有它自己的state和Props
原文
先看例子:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
// 注意下面使用了 count 的这一行
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
count 会“监听”状态的变化并自动更新吗?这么想可能是学习React的时候有用的第一直觉,但它并不是精确的心智模型。
上面例子中,count仅是一个数字而已。它不是神奇的“data binding”, “watcher”, “proxy”,或者其他任何东西。它就是一个普通的数字像下面这个一样:
const count = 42;
// ...
<p>You clicked {count} times</p>
// ...
我们的组件第一次渲染的时候,从useState()拿到count的初始值0。当我们调用setCount(1),React会再次渲染组件,这一次count是1。如此等等:
// 第一次执行
function Counter() {
const count = 0; // Returned by useState()
// ...
<p>You clicked {count} times</p>
// ...
}
// 再点击一次,函数再一次调用
function Counter() {
const count = 1; // Returned by useState()
// ...
<p>You clicked {count} times</p>
// ...
}
// 再点击一次,函数再一次调用
function Counter() {
const count = 2; // Returned by useState()
// ...
<p>You clicked {count} times</p>
// ...
}
当我们更新状态的时候,React会重新渲染组件。每一次渲染都能拿到独立的count 状态,这个状态值是函数中的一个常量。
所以下面的这行代码没有做任何特殊的数据绑定:
<p>You clicked {count} times</p>
它仅仅只是在渲染输出中插入了count这个数字。这个数字由React提供。当setCount的时候,React会带着一个不同的count值再次调用组件。然后,React会更新DOM以保持和渲染输出一致。
这里关键的点在于任意一次渲染中的count常量都不会随着时间改变。渲染输出会变是因为我们的组件被一次次调用,而每一次调用引起的渲染中,它包含的count值独立于其他渲染。
(关于这个过程更深入的探讨可以查看我的另一篇文章 React as a UI Runtime。)
笔记
感觉函数组件的数据更新更像是通过闭包实现
// 普通的一个闭包函数
function ShowNum(x=0){
return function(){
console.log(x)
}
}
const ShowNum10 = ShowNum(10)
ShowNum10()
const ShowNum20 = ShowNum(20)
ShowNum20()
// 函数组件
function Counter(props) {
const [count, setCount] = useState(0);
return (
<div>
// 注意下面使用了 count 的这一行
<p>数值:{count} </p>
<button onClick={() => setCount(props.value)}>
Click me
</button>
</div>
);
}
假设Counter是一个函数组件,实参x就是函数组件的state或者props
当我们执行了一次下面代码
const ShowNum10 = ShowNum(10)
就相当于函数组件Counter点击了一次按钮,执行了下面代码
// props.value 等于10
setCount(props.value)
所以原文例子中每一次点击应该都类似于执行了一次次闭包函数
根据原文解释 当我们更新状态的时候,React会重新渲染组件。每一次渲染都能拿到独立的count 状态,这个状态值是函数中的一个常量。
所以函数组件每一次更新状态应该就是重新调用一次函数组件,更新闭包的值。类似于下面代码
const ShowNum10 = ShowNum(10)
ShowNum10()
const ShowNum20 = ShowNum(20)
ShowNum20()
任意一次渲染中的count常量都不会随着时间改变。渲染输出会变是因为我们的组件被一次次调用,而每一次调用引起的渲染中,它包含的count值独立于其他渲染。
:::info 以上的都是个人阅读文章后的个人理解(作为笔记,可能描述和逻辑表述不清见谅),如果有误欢迎指导 :::
每一次渲染都有它自己的事件处理函数
原文
到目前为止一切都还好。那么事件处理函数呢?
看下面的这个例子。它在三秒后会alert点击次数count:
function Counter() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
如果我按照下面的步骤去操作:
- 点击增加counter到3
- 点击一下 “Show alert”
- 点击增加 counter到5并且在定时器回调触发前完成
你猜alert会弹出什么呢?会是5吗?— 这个值是alert的时候counter的实时状态。或者会是3吗?— 这个值是我点击时候的状态。
来自己 试试吧!
如果结果和你预料不一样,你可以想象一个更实际的例子:一个聊天应用在state中保存了当前接收者的ID,以及一个发送按钮。 这篇文章深入探索了个中缘由。正确的答案就是3。
alert会“捕获”我点击按钮时候的状态。
(虽然有其他办法可以实现不同的行为,但现在我会专注于这个默认的场景。当我们在构建一种心智模型的时候,在可选的策略中分辨出“最小阻力路径”是非常重要的。)
但它究竟是如何工作的呢?
我们发现count在每一次函数调用中都是一个常量值。值得强调的是 — 我们的组件函数每次渲染都会被调用,但是每一次调用中count值都是常量,并且它被赋予了当前渲染中的状态值。
这并不是React特有的,普通的函数也有类似的行为:
function sayHi(person) {
const name = person.name;
setTimeout(() => {
alert('Hello, ' + name);
}, 3000);
}
let someone = {name: 'Dan'};
sayHi(someone);
someone = {name: 'Yuzhi'};
sayHi(someone);
someone = {name: 'Dominic'};
sayHi(someone);
在 这个例子中, 外层的someone会被赋值很多次(就像在React中,当前的组件状态会改变一样)。然后,在sayHi函数中,局部常量name会和某次调用中的person关联。因为这个常量是局部的,所以每一次调用都是相互独立的。结果就是,当定时器回调触发的时候,每一个alert都会弹出它拥有的name。
这就解释了我们的事件处理函数如何捕获了点击时候的count值。如果我们应用相同的替换原理,每一次渲染“看到”的是它自己的count:
// During first render
function Counter() {
const count = 0; // Returned by useState()
// ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
// ...
}
// After a click, our function is called again
function Counter() {
const count = 1; // Returned by useState()
// ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
// ...
}
// After another click, our function is called again
function Counter() {
const count = 2; // Returned by useState()
// ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
// ...
}
所以实际上,每一次渲染都有一个“新版本”的handleAlertClick。每一个版本的handleAlertClick“记住” 了它自己的 count:
// During first render
function Counter() {
// ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + 0);
}, 3000);
}
// ...
<button onClick={handleAlertClick} /> // The one with 0 inside
// ...
}
// After a click, our function is called again
function Counter() {
// ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + 1);
}, 3000);
}
// ...
<button onClick={handleAlertClick} /> // The one with 1 inside
// ...
}
// After another click, our function is called again
function Counter() {
// ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + 2);
}, 3000);
}
// ...
<button onClick={handleAlertClick} /> // The one with 2 inside
// ...
}
这就是为什么在这个demo中中,事件处理函数“属于”某一次特定的渲染,当你点击的时候,它会使用那次渲染中counter的状态值。
在任意一次渲染中,props和state是始终保持不变的。如果props和state在不同的渲染中是相互独立的,那么使用到它们的任何值也是独立的(包括事件处理函数)。它们都“属于”一次特定的渲染。即便是事件处理中的异步函数调用“看到”的也是这次渲染中的count值。
备注:上面我将具体的count值直接内联到了handleAlertClick函数中。这种心智上的替换是安全的因为count 值在某次特定渲染中不可能被改变。它被声明成了一个常量并且是一个数字。这样去思考其他类型的值比如对象也同样是安全的,当然需要在我们都同意应该避免直接修改state这个前提下。通过调用setSomething(newObj)的方式去生成一个新的对象而不是直接修改它是更好的选择,因为这样能保证之前渲染中的state不会被污染。