- 完全可选的。 你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。
- 100% 向后兼容的。 Hook 不包含任何破坏性改动。
- 现在可用。 Hooks 已发布于 v16.8.0。
初识动机
React Hooks 是 React 团队在开发实践中,逐渐认识到的改进点。它背后的思考涉及到类组件和函数组件的对比。所以,我们首先需要知道,什么是类组件、函数组件以及两者的比较。
类组件(Class Component)
基于 ES6 Class ,通过继承 React.Component
定义组件。
示例:
// 代码源自 “深入浅出搞定 React -- 修言”
class DemoClass extends React.Component {
// 初始化类组件的 state
state = {
text: ''
}
// 编写生命周期方法 didMount
componentDidMount() {
// 省略业务逻辑
}
// 编写自定义的实例方法
changeText = (newText) => {
// 更新 state
this.setState({
text: newText
})
}
// 编写生命周期方法 render
render() {
return (
<div className="demoClass">
<p>{this.state.text}</p>
<button onClick={this.changeText}>点我修改</button>
</div>
)
}
}
函数组件/无状态组件(Function Component/Stateless Component)
以函数形式定义的组件。在早期没有 React-Hooks 的时候,函数组件内部无法定义和维护 state
,所以它也叫无状态组件。
示例:
// 代码源自 “深入浅出搞定 React -- 修言”
function DemoFunction(props) {
const { text } = props
return (
<div className="demoFunction">
<p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p>
</div>
)
}
类组件与函数组件的对比
通过上面两个示例,除了在写法上的区分。还有一些用法上的区别包括但不限于:
- 类组件需要继承;函数组件不需要
- 类组件可以访问生命周期方法;函数组件不可以
- 类组件可以获取实例化后的
this
,并可以基于this
做各种操作;函数组件不可以 - 类组件可以定义并维护
state
;函数组件不可以 - ...
我们很容易看出,类组件的能力明显强于函数组件(在 Hook 出现之前),但这并不说明“类组件强于函数组件”。同理,也不应鼓吹函数组件的轻量优雅,会在将来干掉类组件。这两种形式并没有优劣之别,我们更应该关注两者的不同,进而在不同的场景使用相应的组件,这样才能获得一个全面、辩证的认知。
深入理解类组件
类组件是典型的面向对象编程。对于面向对象这个概念,我们总会想到 “封装” 和 “继承”
再次思考下类组件示例:
// 代码源自 “深入浅出搞定 React -- 修言”
class DemoClass extends React.Component {
// 初始化类组件的 state
state = {
text: ''
}
// 编写生命周期方法 didMount
componentDidMount() {
// 省略业务逻辑
}
// 编写自定义的实例方法
changeText = (newText) => {
// 更新 state
this.setState({
text: newText
})
}
// 编写生命周期方法 render
render() {
return (
<div className="demoClass">
<p>{this.state.text}</p>
<button onClick={this.changeText}>点我修改</button>
</div>
)
}
}
从中可以看出,React 类组件内置了很多属性/方法;你只需继承 React.Component
就可以获得它们。
类组件的强大毋庸置疑,但“多”就是“好”吗?其实未必。
React 类组件提供了多少东西,你就需要学习多少东西。假如对属性/方法的使用理解不够透彻甚至错误,会出现很大的问题。大而全的背后,是巨大的学习成本。
类组件还有一个问题,它太重了,对于解决很多简单的问题,编写一个类组件就显得过于复杂。复杂度高就带来了高昂的理解成本。
更为致命的是,类组件的内部逻辑难以复用。如果一定要这么做需要学习更复杂的实现(比如高阶组件、Render Props 等),这是一个更高的学习成本
深入理解函数组件
我们再来看下函数组件示例:
// 代码源自 “深入浅出搞定 React -- 修言”
function DemoFunction(props) {
const { text } = props
return (
<div className="demoFunction">
<p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p>
</div>
)
}
函数组件不止可以做渲染,也可以实现复杂的交互逻辑:
// 代码源自 “深入浅出搞定 React -- 修言”
function DemoFunction(props) {
const { text } = props
const showAlert = ()=> {
alert(`我接收到的文本是${text}`)
}
return (
<div className="demoFunction">
<p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p>
<button onClick={showAlert}>点击弹窗</button>
</div>
)
}
相较于类组件,函数组件轻量、灵活、易于组织和维护、较低的学习成本。
更重要的是,React 作者 Dan Abramov 写过一篇关于 函数式组件与类组件有何不同 的文章。整篇文章论证了一件事:
React的函数式组件和类组件之间是否有任何根本上的区别?当然有 —— 在心智模型上。是面向对象和函数式编程的两套不同的设计思想之间的差异。
函数组件更加契合 React 框架的设计理念
UI = f(data)
React 组件本身的定位就是函数,能够把数据变成视图的函数。
作为开发者,我们所编写的是声明式的代码,而 React 框架的主要工作就是把声明式的代码转换为命令式的 DOM 操作,把数据映射到用户可见的视图。所以,React 的数据应该和渲染绑在一起,而类组件做不到这一点。
为什么类组件做不到?我们来看一下上面文章的示例代码:
- 类组件:
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;
- 函数组件:
import React from 'react';
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
export default ProfilePage;
尝试按照以下顺序来分别使用这两个按钮:
- 点击 其中某一个 Follow 按钮。
- 在3秒内 切换 选中的账号。
- 查看 弹出的文本。
你将看到一个奇特的区别:
-
当使用 函数式组件 实现的
ProfilePage
, 当前账号是 Dan 时点击 Follow 按钮,然后立马切换当前账号到 Sophie,弹出的文本将依旧是'Followed Dan'
。 -
当使用 类组件 实现的
ProfilePage
, 弹出的文本将是'Followed Sophie'
: -
类组件的结果:
类组件的结果让人感到困惑:user
的内容是通过 props
传递的,而 props
是不可变的,为什么弹出的文字变了(从 Dan
变成 Sophie
)?
this.props.user
读取数据。在 React 中 props
是不可变的(immutable),但是 this
是永远可变的(mutable)。事实上,这就是类组件 this
存在的意义。React 本身会随着时间的推移而改变,以便可以在 render
方法及生命周期方法中获得最新的实例。
如果我们说UI在概念上是当前应用状态的一个函数,那么事件处理程序则是渲染结果的一部分 —— 就像视觉输出一样。我们的事件处理程序“属于”一个拥有特定 props 和 state 的特定渲染。
然而,调用一个回调函数读取 this.props
的 timeout 会打断这种关联。我们的 showMessage
回调并没有与任何一个特定的渲染“绑定”在一起,所以它“失去”了正确的 props。从 this 中读取数据的这种行为,切断了这种联系。
- 函数组件的结果:
props
会在 ProfilePage
函数执行的时候就被捕获,而 props
本身是不可变的,所以我们可以充分保证从函数执行开始,到之后的任何时机读取的 props
都是最初捕获的 props
当父组件传入新的 props
重新渲染,本质上是基于新的 props
发起一次全新的函数调用,并不会影响上一次调用的 props
也就是说:函数式组件捕获了渲染所用的值。
经过编程实践,React 团队认识到了,函数组件更加符合设计理念、也更有利于逻辑拆分和复用。接下来就是“用脚投票”,用实际行动支持开发者编写函数组件。于是,React-Hooks 便应运而生。
Hooks 的本质
一套能够使得函数组件更强大、更灵活的“钩子”。让函数组件拥有类组件的能力,并且保留轻量、优雅的特性。
Hooks 是使用你已经知道的 React 特性的一种更直接的方式 —— 比如 state
,生命周期,context
,以及 refs
。它们并没有从根本上改变 React 的工作方式,你对组件,props
, 以及自顶向下的数据流的知识并没有改变。
Hooks 核心 API
useState()
为函数组件引入状态(state
)
有了 useState
之后,我们就可以直接在函数组件中引入 state
示例对比:
import React, { Component } from "react";
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
changeCount = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={this.changeCount}>
Click me
</button>
</div>
);
}
}
import React, { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
同样逻辑的函数组件相比类组件而言,复杂度要低得多得多。
useState 做了什么
useState
是一种新方法,它与 class 里面的 this.state
提供的功能完全相同。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
useState 参数
useState()
方法里面唯一的参数就是初始 state。不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。
useState 返回值
返回值为:当前 state 以及更新 state 的函数。
this.state = {
text: 'abc',
num: 3,
arr: [1, 2, 3]
}
使用 useState
const [text, setText] = useState('abc')
const [num, setNum] = useState(3)
const [arr, setArr] = useState([1, 2, 3])
你还可以定义布尔值、对象等,它就像类组件中 state
对象的某一个属性一样,对应着一个单独的状态,允许存储任意类型的值。
useEffect()
useEffect
在一定程度上弥补了函数组件的生命周期的缺失。我们可以将在 componentDidMount
、componentDidUpdate
和 componentWillUnmount
三个生命周期里来做的事情,放到 useEffect
里来做。比如操作 DOM、绑定/解绑事件、网络请求 等。
示例对比:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect 和生命周期的关系
useEffect
和生命周期方法并不完全相等。它们在心智模型上是不同的,useEffect
更接近于实现状态同步,而不是响应生命周期事件。
当我不再透过熟悉的class生命周期方法去窥视useEffect
这个Hook的时候,我才得以融会贯通。
effect 分类
副作用分为两类:需要清除和不需要清除
useEffect 参数
两个参数,回调函数和依赖数组
useEffect(callback, [])
第一个参数,要么返回一个能清除副作用的函数,要么无返回值
useEffect(() => {
// 副作用逻辑
return () => {
// 清除副作用
}
}, [])
清除副作用会在卸载阶段执行,且与依赖数组无关。
第二个参数:依赖数组
- 如果不传入依赖数组,每一次渲染后都执行副作用
useEffect(callback)
- 依赖数组为空数组,仅在挂载阶段执行一次副作用
useEffect(callback, [])
- 依赖数组为非空数组,React 会在新的一次渲染后去对比前后两次的渲染,查看依赖数组内的变量是否发生更新,只要有一个发生更新就会执行副作用
useEffect(callback, [a, b, c])
扩展阅读
由 React 作者的文章: useEffect 完整指南
Why Hooks
- 类(Class)组件难以理解
- 类组件业务逻辑难以拆分
- 类组件的状态逻辑难以复用
- 函数组件从设计思想上更契合 React
难以理解的类组件
类组件难以理解的背后是 this
和生命周期的理解
this
问题
?:
class Example extends React.Component {
state = {
count: 0
}
changeCount() {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={this.changeCount}>
Click me
</button>
</div>
);
}
}
当点击 button
按钮时,我们希望 count + 1
,但实际是程序会报错。原因很简单,changeCount
方法里面拿不到组件实例 this
。为了解决这个问题,我们使用 bind
或箭头函数。但不管哪种方法,本质上都是在实践层面的约束来解决设计层面的问题
- 生命周期的问题
- 学习成本
- 不合理的逻辑规划
Hooks 实现更好的逻辑拆分
类组件中业务逻辑与生命周期耦合在一起
componentDidMount() {
// 1. 这里发起异步调用
// 2. 这里从 props 里获取某个数据,根据这个数据更新 DOM
// 3. 这里设置一个订阅
// 4. 这里随便干点别的什么
// ...
}
componentWillUnMount() {
// 在这里卸载订阅
}
componentDidUpdate() {
// 1. 在这里根据 DidMount 获取到的异步数据更新 DOM
// 2. 这里从 props 里获取某个数据,根据这个数据更新 DOM(和 DidMount 的第2步一样)
}
将没有关联的逻辑塞进同一个生命周期里,将强关联的逻辑分散到不同的生命周期里;导致生命周期复杂,逻辑混乱。
Hooks 可以实现关注点分离,将组件中相互关联的部分拆分成更小的函数。帮助我们实现业务逻辑的聚合,避免复杂的组件和冗余的代码。
状态复用
类组件的逻辑复用需要使用高阶组件(HOC) 和 Render Props 组件设计模式。在实现逻辑复用的同时,也破坏了组件的结构;其中最常见的就是 “嵌套地狱” 现象
Hooks 可以通过自定义 Hook 实现逻辑复用,这么做既不破坏组件结构,也能实现逻辑复用的目的。
Hooks 不是万能的
- Hooks 还没有完全为函数组件补齐类组件的能力:比如
getSnapshotBeforeUpdate
、componentDidCatch
这些生命周期 - 函数组件给了我们一定程度的自由,但也对开发者的水平提出更高的要求
- Hooks 在使用上有着严格的规则约束
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!