Notes: How are function components different from classes?

overreated是React作者dan_abramov的博客,有很多详解React的干货。

How are function components different from classes是一篇详述function components和classes区别的文章。

在有Hooks之前,大家对function components与classes的区别的认知是,classes拥有更多的功能,比如可以有state等。但是在有了hooks之后,这似乎不再是一个问题了,那么它们之间的区别是什么呢?

这里作者举了一个简单的例子来说明它们之间的不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// function
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user)
}

const handleClick = () => {
setTimeout(showMessage, 3000)
}

return (
<button onClick={handleClick}>Follow</button>
)
}

// class
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user)
}

handleClick = () => {
setTimeout(this.showMessage, 3000)
}

render() {
return <button onClick={this.handleClick}>Follow</button>
}
}

上面两种写法是我们常用的function和class的写法,乍看之下,它们完成的任务是一样的,但是这里作者玩了一个小小的trick,它给页面提供了不同的profile,让user可以被切换(见live demo),于是我们看到,function实现的页面里,我们将userdan切换到sophoie之后,3秒后提示框里显示的仍是dan,但是对于class来说就不是这样了,3秒之后显示的用户变成了sophine,这是为什么呢?

This class method reads from this.props.user. Props are immutable in React so they can never change. However, this is, and has always been, mutable.

事实上,class在设计的时候就是希望this能够获取到最新的数据,这样在生命周期的方法里以及在render里都能够显示最新的数据,但这样也会有一个问题,就是虽然props是不会变的,但是this是会变的,这样就导致showMessage方法里读取到的user数据可能“太新”了,从而导致它并不是当前render对应的数据。简单来说,就是我们触发事件的时候,user还是第一次传入的props的值,可是在alert执行的时候,user值已经是第二次传入的props的值了。

假设现在没有function component,只能通过class来写组件,那么我们要如何解决上面的问题呢?

有一个方法是我们可以在事件之前先读取需要的数据,这样即使this在事件之后改变,也不会影响到数据的呈现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ProfilePage extends React.Component {
showMessage = user => {
alert('Followed ' + user)
}

handleClick = () => {
const { user } = this.props
setTimeout(() => this.showMessage(user), 3000)
}

render() {
return <button onClick={this.handleClick}>Follow</button>
}
}

这样的确可以解决上面提到的问题,但是很显然,这个方法也会导致代码变冗余,尤其是随着时间的增长,这样的写法会更容易出错,因为我们需要的每个propsstate中的值都需要事先从对象中取出来,有没有更好的解决方案呢?

另一个解决方法是通过闭包来实现数据的不变。也就是说,当你将propsstate数据都放入render中,使其成为闭包,可以保证数据不会发生变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ProfilePage extends React.Component {
render() {
const props = this.props

const showMessage = () => {
alert('Followed ' + props.user)
}

const handleClick = () => {
setTimeout(showMessage, 3000)
}

return <button onClick={handleClick}>Follow</button>
}
}

You’ve “captured” props at the time of render”

上面的方法的确可以保证数据不会发生变化,且我们可以加入更多的方法在render中,但是这种写法非常奇怪,如果我们需要在render方法里来定义函数,那么为什么要使用class呢?这样完全可以通过function来实现了:

1
2
3
4
5
6
7
8
9
10
11
12
13
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user)
}

const handleClick = () => {
setTimeout(showMessage, 3000)
}

return (
<button onClick={handleClick}>Follow</button>
)
}

不同于this, props是不会发生变化的。当ProfilePage的父组件需要传给ProfilePage不同的props时,它会重新调用这个function来重新渲染。而我们点击按钮时的props是前一次渲染中的props,所以showMessage还是会读取到前一次渲染中的user,这样就不会出现点击的callback数据和实际页面对不上的情况了。

这样我们知道了function捕获的是每一次渲染的值,hooks在实现上也是这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function MessageThread() {
const [message, setMessage] = useState('')

const showMessage = () => {
alert('You said: ' + message)
}

cosnt handleSendClick = () => {
setTimeout(showMessage, 3000)
}

const handleMessageChange = e => {
setMessage(e.target.value)
}

return (
<>
<input value={message} onChange={handleMessageChange} />
<button onClick={handleSendClick}>Send</button>
</>
)
}

那么附加一个问题,如果我们就是想要读取到“最新的数据”,即不是当前渲染的数据,而是“未来的数据”呢?

一个方法就是我们通过classes来读取this.propsthis.state来实现。还有另一个思路是我们可以通过ref来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function MessageThread() {
const [message, setMessage] = useState('')
const latestMessage = useRef('')

const showMessage = () => {
alert('You said: ' + latestMessage.current)
}

const handleSendClick = () => {
setTimeout(showMessage, 3000)
}

const handleMessageChange = e => {
setMessage(e.target.value)
latestMessage.current = e.target.value
}
}

通过重新给ref赋值,我们能够拿到最新的props值。当然这样手动修改ref值是一件麻烦的事情,有一个更方便的方法是通过useEffect来自动修改:

1
2
3
4
5
6
7
8
9
10
11
12
function MessageThread() {
const [message, setMessage] = useState('')

const latestMessage = useRef('')
useEffect(() => {
latestMessage.current = message
})

const showMessage = () => {
alert('You said: ' + latestMessage.current)
}
}