每一次渲染都有它自己的 Props and State
看一个简单的计数器组件Counter
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
仔细查看例子中的count,count只是一个数字而已,也不是“data binding”,“watcher”,只是一个单纯的count数字。
我们的组件第一次渲染的时候,从useState()拿到count的初始值0。当我们调用setCount(1),React会再次渲染组件,这一次count是1。
在每一次的点击button,React会重新渲染组件,每一次渲染都可以拿到独立的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>
);
}
当我们在点击Click me三次后,再去点击Show alert,再点击Click me 2次,alert会捕捉点击按钮时候的count的状态信息,所以alert会弹出‘You clicked on: 3’,而不是5。
但是在类组件中会产生不同的结果详情查看:overreacted.io/zh-hans/how…
函数式组件和类组件的区别
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
- 点击 其中某一个 Follow 按钮。
- 在3秒内 切换 选中的账号。
- 查看 弹出的文本。
你将看到一个奇特的区别:
- 当使用 函数式组件 实现的 ProfilePage, 当前账号是 Dan 时点击 Follow 按钮,然后立马切换当前账号到 Sophie,弹出的文本将依旧是 'Followed Dan'。
- 当使用 类组件 实现的 ProfilePage, 弹出的文本将是 'Followed Sophie':
但是为什么会有这样的表现呢
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
这个类方法从 this.props.user 中读取数据。在 React 中 Props 是不可变(immutable)的,所以他们永远不会改变。然而, this 是,而且永远是,可变(mutable)的。
事实上,这就是类组件 this 存在的意义。React本身会随着时间的推移而改变,以便你可以在渲染方法以及生命周期方法中得到最新的实例。
所以如果在请求已经发出的情况下我们的组件进行了重新渲染,this.props将会改变。showMessage方法从一个“更新的”的props中得到了user。
所以this不是可靠的,props是不可变的,可以使用删除类来编写。
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
在任意一次渲染中,props和state是始终保持不变的。 如果props和state在不同的渲染中是相互独立的,那么使用到它们的任何值也是独立的(包括事件处理函数)。它们都“属于”一次特定的渲染。即便是事件处理中的异步函数调用“看到”的也是这次渲染中的count值。
如何能拿到最新的状态和值就需要使用useRef
function MessageThread() {
const [message, setMessage] = useState('');
// 保持追踪最新的值。
const latestMessage = useRef('');
useEffect(() => {
latestMessage.current = message;
});
const showMessage = () => {
alert('You said: ' + latestMessage.current);
};
每次渲染都有它自己的Effects
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
我们已经知道count是某个特定渲染中的常量。事件处理函数“看到”的是属于它那次特定渲染中的count状态值。对于effects也同样如此:
并不是 count 的值在“不变”的effect中发生了改变,而是 effect 函数本身 在每一次渲染中都不相同。
每一个effect版本“看到”的count值都来自于它属于的那次渲染:
为了确保我们已经有了扎实的理解,我们再回顾一下第一次的渲染过程:
- React: 给我状态为 0时候的UI。
- 你的组件:
-
- 给你需要渲染的内容:
You clicked 0 times
。 - 记得在渲染完了之后调用这个effect: () => { document.title = 'You clicked 0 times' }。
- 给你需要渲染的内容:
- React: 没问题。开始更新UI,喂浏览器,我要给DOM添加一些东西。
- 浏览器: 酷,我已经把它绘制到屏幕上了。
- React: 好的, 我现在开始运行给我的effect
-
- 运行 () => { document.title = 'You clicked 0 times' }。
现在我们回顾一下我们点击之后发生了什么:
- 你的组件: 喂 React, 把我的状态设置为1。
- React: 给我状态为 1时候的UI。
- 你的组件:
-
- 给你需要渲染的内容:
You clicked 1 times
。 - 记得在渲染完了之后调用这个effect: () => { document.title = 'You clicked 1 times' }。
- 给你需要渲染的内容:
- React: 没问题。开始更新UI,喂浏览器,我修改了DOM。
- Browser: 酷,我已经将更改绘制到屏幕上了。
- React: 好的, 我现在开始运行属于这次渲染的effect
-
- 运行 () => { document.title = 'You clicked 1 times' }。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
import React, {Component} from "react";
import ReactDOM from "react-dom";
class Example extends Component {
state = {
count: 0
};
componentDidMount() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({
count: this.state.count + 1
})}>
Click me
</button>
</div>
)
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Example />, rootElement);
函数式组件点击效果:
类组件点击效果:
因为在每一次渲染中拿到的都是每一次的props和state,而在类组件中this.state.count总是指向最新的count值,也就是通过ref自动更改了state.count的值
function MessageThread() {
const [message, setMessage] = useState('');
// 保持追踪最新的值。
const latestMessage = useRef('');
useEffect(() => {
latestMessage.current = message;
});
const showMessage = () => {
alert('You said: ' + latestMessage.current);
};
如果设置了错误的依赖会怎么样
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
在第一次渲染中,count是0。因此,setCount(count + 1)在第一次渲染中等价于setCount(0 + 1)。既然我们设置了 [] 依赖,effect不会再重新运行,它后面每一秒都会调用 setCount(0 + 1) :
修改方式:
第一种策略是在依赖中包含所有effect中用到的组件内的值。 让我们在依赖中包含count:
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
这能解决问题但是我们的定时器会在每一次count改变后清除和重新设定。这应该不是我们想要的结果:
第二种策略是在effect中不需要使用count,当想要更新状态的时候使用seState中函数形式
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
把函数移到Effects里
function SearchResults() {
const [query, setQuery] = useState('react');
useEffect(() => {
function getFetchUrl() {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}
fetchData();
}, [query]); // ✅ Deps are OK
// ...
}
通过query更新来重新执行useEffect,但是如果组件中有多个effect使用了相同的函数,就需要把函数放到effects外部中。
有两种解决方案:
第一个, 如果一个函数没有使用组件内的任何值,你应该把它提到组件外面去定义,然后就可以自由地在effects中使用:
// ✅ Not affected by the data flow
function getFetchUrl(query) {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
function SearchResults() {
useEffect(() => {
const url = getFetchUrl('react');
// ... Fetch data and do something ...
}, []); // ✅ Deps are OK
useEffect(() => {
const url = getFetchUrl('redux');
// ... Fetch data and do something ...
}, []); // ✅ Deps are OK
// ...
}
第二个, 将函数包装成useCallback
function SearchResults() {
const [query, setQuery] = useState('react');
// ✅ Preserves identity until query changes
const getFetchUrl = useCallback(() => {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}, [query]); // ✅ Callback deps are OK
useEffect(() => {
const url = getFetchUrl();
// ... Fetch data and do something ...
}, [getFetchUrl]); // ✅ Effect deps are OK
// ...
}
我们要感谢useCallback,因为如果query 保持不变,getFetchUrl也会保持不变,我们的effect也不会重新运行。但是如果query修改了,getFetchUrl也会随之改变,因此会重新请求数据。这就像你在Excel里修改了一个单元格的值,另一个使用它的单元格会自动重新计算一样。
通过useCallback还可以进行父子组件之间的传递函数
function Parent() {
const [query, setQuery] = useState("react");
// ✅ Preserves identity until query changes
const fetchData = useCallback(() => {
const url = "https://hn.algolia.com/api/v1/search?query=" + query;
// ... Fetch data and return it ...
}, [query]); // ✅ Callback deps are OK
return <Child fetchData={fetchData} />;
}
function Child({ fetchData }) {
let [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, [fetchData]); // ✅ Effect deps are OK
// ...
}
但是这种模式在class中是行不通的
在 Class Component 的代码里,如果希望参数变化就重新取数,你不能直接比对取数函数的 Diff:
componentDidUpdate(prevProps) {
// 🔴 This condition will never be true
if (this.props.fetchData !== prevProps.fetchData) {
this.props.fetchData();
}
}
要对比的是取数参数是否发生变化
componentDidUpdate(prevProps) {
if (this.props.query !== prevProps.query) {
this.props.fetchData();
}
}
- 在组件内部,那些会成为其他useEffect依赖项的方法,建议用
useCallback
包裹,或者直接编写在引用它的useEffect中。 - 己所不欲勿施于人,如果你的function会作为props传递给子组件,请一定要使用
useCallback
包裹,对于子组件来说,如果每次render都会导致你传递的函数发生变化,可能会对它造成非常大的困扰。同时也不利于react做渲染优化。