函数式组件与类组件有何不同?

性能问题可以忽略

性能主要取决于代码的作用,而不是选择函数式还是类组件。在我们的观察中,尽管优化策略各有略微不同,但性能差异可以忽略不计。

hooks慢是因为在渲染中创建了函数么

现在浏览器中闭包与类没有明显的性能差别,除非在极端的情况下,hooks在两点上更有效率

  • 避免了类组件实例化,和constructor中绑定时间处理函数的消耗

  • 常规代码使用hooks不需要过深的组件树嵌套,在封装的基础库中使用高阶组建,渲染props和context很常见

传统上,React内联函数的性能问题主要与如何在子组件中为每个渲染断点shouldComponentUpdate传递新的回调有关。

Hooks 使用三个HooksApi来解决

  • useCallback 在每次渲染中保持了相同的句柄引用,所以shouldComponentUpdate继续工作

  • useMemo 控制子组件何时渲染,不在需要prueCompnent

  • useReducer 避免了需要吧会掉函数传递过深

类组件存在的问题

下面模拟在点击一个按钮之后发送一个关注请求

index.js

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import React from "react";
import ReactDOM from "react-dom";

import ProfilePageFunction from './ProfilePageFunction';
import ProfilePageClass from './ProfilePageClass';

class App extends React.Component {
state = {
user: 'Dan',
};
render() {
return (
<>
<label>
<b>Choose profile to view: </b>
<select
value={this.state.user}
onChange={e => this.setState({ user: e.target.value })}
>
<option value="Dan">Dan</option>
<option value="Sophie">Sophie</option>
<option value="Sunil">Sunil</option>
</select>
</label>
<h1>Welcome to {this.state.user}’s profile!</h1>
<p>
<ProfilePageFunction user={this.state.user} />
<b> (function)</b>
</p>
<p>
<ProfilePageClass user={this.state.user} />
<b> (class)</b>
</p>
<p>
Can you spot the difference in the behavior?
</p>
</>
)
}
}


const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

ClassCompnent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';

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

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

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

export default ProfilePage;

再点击按钮三秒之后会弹出关注的姓名,但是如果在点击之后马上修改下啦列表的值,那么三秒之后显示的是修改之后的值,这显然史有问题的

造成这个问题的原因就是this是不断在改变的,虽然在第一次渲染结束的时候,已经为setTimeout的回调函数传入了类方法showMessage,但是当点击按钮之后迅速切换下拉框,state的改变会导致组建重新加载,这时子组件会接受到一个过于新的props, 而setTimeout中的回调函数并没有与之前的旧的props绑定,而是直接读取了,this中较新的props

也许你会想使bind,但是这并不起作用,当读取props的时候,this中的props已经被修改,bind只是绑定了类方法的执行上下文,但是并没有固定为上一次的this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ProfilePage extends React.Component {
constructor(props) {
super(props);
this.showMessage = this.showMessage.bind(this);
this.handleClick = this.handleClick.bind(this);
}

showMessage() {
alert('Followed ' + this.props.user);
}

handleClick() {
setTimeout(this.showMessage, 3000);
}

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

类组件问题解决方法

也许你会想到绑定回调函数之前先把用到的props缓存起来

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>;
}
}

这种方法使得代码明显变得更加冗长,并且随着时间推移容易出错。如果我们需要的不止是一个props怎么办?如果我们还需要访问state怎么办?如果 showMessage 调用了另一个方法,然后那个方法中读取了 this.props.something 或者 this.state.something,我们又将遇到同样的问题。然后我们不得不将this.props和this.state以函数参数的形式在被showMessage调用的每个方法中一路传递下去。

所以我们想到用闭包的方式解决props不能被保存的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ProfilePage extends React.Component {
render() {
// Capture the props!
const props = this.props;

// Note: we are *inside render*.
// These aren't class methods.
const showMessage = () => {
alert('Followed ' + props.user);
};

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

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

既然只需要在render函数中定义各种函数,所以自然想到,可以省略class,直接使用函数式组件

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

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

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

而hooks其实就是捕获了state中的值

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);
};

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

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

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

Hooks与未来值

上面我们已经能拿到一个状态的过去值,但是如何能拿到一个状态的未来值

我们在一个effect内部执行赋值操作以便让ref的值只会在DOM被更新后才会改变。这确保了我们的变量突变不会破坏依赖于可中断渲染的时间切片和 Suspense等特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function ProfilePage(props) {
const ref = useRef(null);
const showMessage = () => {
alert("Followed " + ref.current.user);
};
useEffect(() => {
ref.current ={user:props.user};
}, [props,ref]);
const handleClick = () => {
setTimeout(showMessage, 3000);
};

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

总结

所谓的“陈旧的闭包”问题的出现多是由于错误的假设了“函数不会改变”或者“props永远是一样的”。事实并非如此。

函数捕获了他们的props和state —— 因此它们的标识也同样重要。这不是一个bug,而是一个函数式组件的特性。例如,对于useEffect或者useCallback来说,函数不应该被排除在“依赖数组”之外。(正确的解决方案通常是使用上面说过的useReducer或者useRef)

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信