引出问题
试想有这么一个组件,它渲染了一个利用 setTimeout 来模拟网络请求,然后显示一个确认警告的按钮。 函数式:
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>;
}
}
尝试按照以下顺序来分别使用这两个按钮:
- 点击 其中某一个 Follow 按钮。
- 在 3 秒内 切换 选中的账号。
- 查看 弹出的文本。
你将看到一个奇特的区别:
- 当使用 函数式组件 实现的
ProfilePage
, 当前账号是 Dan 时点击 Follow 按钮,然后立马切换当前账号到 Sophie,弹出的文本将依旧是'Followed Dan'
。 - 当使用 类组件 实现的
ProfilePage
, 弹出的文本将是'Followed Sophie'
在这个例子中,第一个行为是正确的。如果我关注一个人,然后导航到了另一个人的账号,我的组件不应该混淆我关注了谁。 在这里,类组件的实现很明显是错误的。
原因
让我们来仔细看看我们类组件中的 showMessage 方法:
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
这个类方法从 this.props.user
中读取数据。在 React 中 Props 是不可变(immutable)的,所以他们永远不会改变。然而,this
是,而且永远是,可变(mutable)的。
事实上,这就是类组件 this
存在的意义。React 本身会随着时间的推移而改变,以便你可以在渲染方法以及生命周期方法中得到最新的实例。
因为 this 是挂载到组件实例上的,在 React 的整个生命周期里伴随着组件的创建、更新、销毁,所以一旦 React 组件重新渲染,就会生成一个新的实例,从而 this 也指向了这个新的实例,导致组件内的 state 和 props 始终都是最新的值。
所以如果在请求已经发出的情况下我们的组件进行了重新渲染,this.props
将会改变。showMessage
方法从一个“过于新”的props
中得到了user
。
这暴露了一个关于用户界面性质的一个有趣观察。如果我们说 UI 在概念上是当前应用状态的一个函数,那么事件处理程序则是渲染结果的一部分 —— 就像视觉输出一样。我们的事件处理程序“属于”一个拥有特定 props 和 state 的特定渲染。
然而,调用一个回调函数读取 this.props
的 timeout 会打断这种关联。我们的 showMessage
回调并没有与任何一个特定的渲染“绑定”在一起,所以它“失去”了正确的 props。从 this 中读取数据的这种行为,切断了这种联系。
假设函数式组件不存在。我们将如何解决这个问题?
一种方法是在调用事件之前读取this.props
,然后将他们显式地传递到 timeout 回调函数中去:
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
调用的每个方法中一路传递下去。
如果我们能利用 JavaScript闭包的话问题将迎刃而解。
常来说我们会避免使用闭包,但是在 React 中,props 和 state 是不可变得!这就消除了闭包的一个主要缺陷,这就意味着如果你在一次特定的渲染中捕获那一次渲染所用的 props 或者 state,你会发现他们总是会保持一致,就如同你的预期那样:
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>;
}
}
结论
函数组件捕获了渲染所使用的值,保证了在一段时间内,相同的状态渲染相同的结果。
Hooks 的优势
-
函数组件的写法更轻量,更灵活: 在函数组件中,我们不需要去继承一个 class 对象,不需要去记忆那些生命周期,不需要固定的把数据定义在 state 中。函数作为 js 中的一等公民,函数式编程方式可以让我们更灵活的去组织代码。
-
类组件存在自身缺陷: 最常见的就是,在 React 中,如果我们定义一个方法,我们必须使用 bind 或者箭头函数去约束这个方法的 this 作用域。但是在函数组件中,可以通过闭包的方式,在一次渲染中,组件的 props 和 state 是保持不变的,而且传递的方法本身就是已经被约束的了。
-
逻辑是分散的,难以复用: 在 React 里,数据是定义在 state 中的,然后需要编写相关的事件方法,再在生命周期里进行逻辑的初始化,组件更新的时候处理,最后在模板里写 JSX,如果我们去维护这些代码,为了去查看这些逻辑,要上下翻,找出各自的数据、方法、生命周期和模板。
-
Hooks 更贴合 React 的基本理念: React 的核心理念之一,相同的参数输入应该产生相同的输出。简单说,它应当是一个简单的纯函数。