Hooks
# 对 React Hook
的理解,它的实现原理是什么
类组件: 所谓类组件,就是基于 ES6 Class
这种写法,通过继承 React.Component
得来的 React
组件。以下是一个类组件:
class DemoClass extends React.Component {
state = {
text: ""
};
componentDidMount() {
//...
}
changeText = (newText) => {
this.setState({
text: newText
});
};
render() {
return (
<div className="demoClass">
<p>{this.state.text}</p>
<button onClick={this.changeText}>修改</button>
</div>
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
可以看出,React
类组件内部预置了相当多的“现成的东西”等着我们去调度/定制,state
和生命周期就是这些“现成东西”中的典型。要想得到这些东西,难度也不大,只需要继承一个 React.Component
即可。
函数组件:函数组件就是以函数的形态存在的 React
组件。早期并没有 React-Hooks
,函数组件内部无法定义和维护 state
,因此它还有一个别名叫“无状态组件”。以下是一个函数组件:
function DemoFunction(props) {
const { text } = props
return (
<div className="demoFunction">
<p>{`函数组件接收的内容:[${text}]`}</p>
</div>
);
}
2
3
4
5
6
7
8
相比于类组件,函数组件肉眼可见的特质自然包括轻量、灵活、易于组织和维护、较低的学习成本等。
通过对比,从形态上可以对两种组件做区分,它们之间的区别如下:
- 类组件需要继承
class
,函数组件不需要; - 类组件可以访问生命周期方法,函数组件不能;
- 类组件中可以获取到实例化后的
this
,并基于这个 this 做各种各样的事情,而函数组件不可以; - 类组件中可以定义并维护
state
(状态),而函数组件不可以;
除此之外,还有一些其他的不同。通过上面的区别,我们不能说谁好谁坏,它们各有自己的优势。在 React-Hooks
出现之前,类组件的能力边界明显强于函数组件。
实际上,类组件和函数组件之间,是面向对象和函数式编程这两套不同的设计思想之间的差异。而函数组件更加契合 React
框架的设计理念:
React
组件本身的定位就是函数,一个输入数据、输出 UI
的函数。作为开发者,我们编写的是声明式的代码,而 React
框架的主要工作,就是及时地把声明式的代码转换为命令式的 DOM
操作,把数据层面的描述映射到用户可见的 UI
变化中去。这就意味着从原则上来讲,React
的数据应该总是紧紧地和渲染绑定在一起的,而类组件做不到这一点。函数组件就真正地将数据和渲染绑定到了一起。函数组件是一个更加匹配其设计理念、也更有利于逻辑拆分与重用的组件表达形式。
为了能让开发者更好的的去编写函数式组件。于是,React-Hooks
便应运而生。
React-Hooks
是一套能够使函数组件更强大、更灵活的“钩子”。
函数组件比起类组件少了很多东西,比如生命周期、对 state
的管理等。这就给函数组件的使用带来了非常多的局限性,导致我们并不能使用函数这种形式,写出一个真正的全功能的组件。而 React-Hooks
的出现,就是为了帮助函数组件补齐这些(相对于类组件来说)缺失的能力。
如果说函数组件是一台轻巧的快艇,那么 React-Hooks
就是一个内容丰富的零部件箱。“重装战舰”所预置的那些设备,这个箱子里基本全都有,同时它还不强制你全都要,而是允许你自由地选择和使用你需要的那些能力,然后将这些能力以 Hook
(钩子)的形式“钩”进你的组件里,从而定制出一个最适合你的“专属战舰”。
# 为什么 useState
要使用数组而不是对象
useState
的用法:
const [count, setCount] = useState(0)
可以看到 useState
返回的是一个数组,那么为什么是返回数组而不是返回对象呢?
这里用到了解构赋值,所以先来看一下 ES6
的解构赋值:
数组的解构赋值
const foo = [1, 2, 3];
const [one, two, three] = foo;
console.log(one); // 1
console.log(two); // 2
console.log(three); // 3
2
3
4
5
对象的解构赋值
const user = {
id: 888,
name: "xiaoxin"
};
const { id, name } = user;
console.log(id); // 888
console.log(name); // "xiaoxin"
2
3
4
5
6
7
看完这两个例子,答案应该就出来了:
- 如果
useState
返回的是数组,那么使用者可以对数组中的元素命名,代码看起来也比较干净 - 如果
useState
返回的是对象,在解构对象的时候必须要和useState
内部实现返回的对象同名,想要使用多次的话,必须得设置别名才能使用返回值
下面来看看如果 useState
返回对象的情况:
// 第一次使用
const { state, setState } = useState(false);
// 第二次使用
const { state: counter, setState: setCounter } = useState(0)
2
3
4
这里可以看到,返回对象的使用方式还是挺麻烦的,更何况实际项目中会使用的更频繁。
总结: useState
返回的是 array
而不是 object
的原因就是为了降低使用的复杂度,返回数组的话可以直接根据顺序解构,而返回对象的话要想使用多次就需要定义别名了。
# React Hooks
解决了哪些问题?
在组件之间复用状态逻辑很难
React
没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store
)解决此类问题可以使用 render props
和 高阶组件。但是这类方案需要重新组织组件结构,这可能会很麻烦,并且会使代码难以理解。由 providers
,consumers
,高阶组件,render props
等其他抽象层组成的组件会形成“嵌套地狱”。尽管可以在 DevTools
过滤掉它们,但这说明了一个更深层次的问题:React
需要为共享状态逻辑提供更好的原生途径。
可以使用 Hook
从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook
使我们在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook
变得更便捷。
复杂组件变得难以理解
在组件中,每个生命周期常常包含一些不相关的逻辑。例如,组件常常在 componentDidMount
和 componentDidUpdate
中获取数据。但是,同一个 componentDidMount
中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount
中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。
在多数情况下,不可能将组件拆分为更小的粒度,因为状态逻辑无处不在。这也给测试带来了一定挑战。同时,这也是很多人将 React
与状态管理库结合使用的原因之一。但是,这往往会引入了很多抽象概念,需要你在不同的文件之间来回切换,使得复用变得更加困难。
为了解决这个问题,Hook
将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer
来管理组件的内部状态,使其更加可预测。
难以理解的 class
除了代码复用和代码管理会遇到困难外,class
是学习 React
的一大屏障。我们必须去理解 JavaScript
中 this
的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。没有稳定的语法提案,这些代码非常冗余。大家可以很好地理解 props
,state
和自顶向下的数据流,但对 class
却一筹莫展。即便在有经验的 React
开发者之间,对于函数组件与 class
组件的差异也存在分歧,甚至还要区分两种组件的使用场景。
为了解决这些问题,Hook
使你在非 class
的情况下可以使用更多的 React
特性。 从概念上讲,React
组件一直更像是函数。而 Hook
则拥抱了函数,同时也没有牺牲 React
的精神原则。Hook
提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术
# React Hook
的使用限制有哪些?
React Hooks
的限制主要有两条:
- 不要在循环、条件或嵌套函数中调用
Hook
; - 在
React
的函数组件中调用Hook
。
那为什么会有这样的限制呢?Hooks
的设计初衷是为了改进 React
组件的开发模式。在旧有的开发模式下遇到了三个问题。
- 组件之间难以复用状态逻辑。过去常见的解决方案是高阶组件、
render props
及状态管理框架。 - 复杂的组件变得难以理解。生命周期函数与业务逻辑耦合太深,导致关联部分难以拆分。
- 人和机器都很容易混淆类。常见的有
this
的问题,但在React
团队中还有类难以优化的问题,希望在编译优化层面做出一些改进。
这三个问题在一定程度上阻碍了 React
的后续发展,所以为了解决这三个问题,Hooks
基于函数组件开始设计。然而第三个问题决定了 Hooks
只支持函数组件。
那为什么不要在循环、条件或嵌套函数中调用 Hook
呢?因为 Hooks
的设计是基于数组实现。在调用时按顺序加入数组中,如果使用循环、条件或嵌套函数很有可能导致数组取值错位,执行错误的 Hook
。当然,实质上 React
的源码里不是数组,是链表。
# useEffect
与 useLayoutEffect
的区别
共同点
- 运用效果:
useEffect
与useLayoutEffect
两者都是用于处理副作用,这些副作用包括改变DOM
、设置订阅、操作定时器等。在函数组件内部操作副作用是不被允许的,所以需要使用这两个函数去处理。 - 使用方式:
useEffect
与useLayoutEffect
两者底层的函数签名是完全一致的,都是调用的mountEffectImpl
方法,在使用上也没什么差异,基本可以直接替换。
不同点
- 使用场景:
useEffect
在React
的渲染过程中是被异步调用的,用于绝大多数场景;而useLayoutEffect
会在所有的DOM
变更之后同步调用,主要用于处理DOM
操作、调整样式、避免页面闪烁等问题。也正因为是同步处理,所以需要避免在useLayoutEffect
做计算量较大的耗时任务从而造成阻塞。 - 使用效果:
useEffect
是按照顺序执行代码的,改变屏幕像素之后执行(先渲染,后改变DOM
),当改变屏幕内容时可能会产生闪烁;useLayoutEffect
是改变屏幕像素之前就执行了(会推迟页面显示的事件,先改变DOM
后渲染),不会产生闪烁。useLayoutEffect
总是比useEffect
先执行。
在未来的趋势上,两个 API
是会长期共存的,暂时没有删减合并的计划,需要开发者根据场景去自行选择。React
团队的建议非常实用,如果实在分不清,先用 useEffect
,一般问题不大;如果页面有异常,再直接替换为 useLayoutEffect
即可。
# React Hooks
在平时开发中需要注意的问题和原因
- 不要在循环,条件或嵌套函数中调用
Hook
,必须始终在React
函数的顶层使用Hook
这是因为React
需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数。一旦在循环或条件分支语句中调用Hook
,就容易导致调用顺序的不一致性,从而产生难以预料到的后果。
- 使用
useState
时候,使用push
,pop
,splice
等直接更改数组对象的坑
使用push
直接更改数组无法获取到新值,应该采用析构方式,但是在class
里面不会有这个问题。代码示例:
function Indicatorfilter() {
let [num,setNums] = useState([0,1,2,3])
const test = () => {
// 这里坑是直接采用push去更新num
// setNums(num)是无法更新num的
// 必须使用num = [...num ,1]
num.push(1)
// num = [...num ,1]
setNums(num)
}
return (
<div className='filter'>
<div onClick={test}>测试</div>
<div>
{num.map((item,index) => (
<div key={index}>{item}</div>
))}
</div>
</div>
)
}
class Indicatorfilter extends React.Component<any,any>{
constructor(props:any){
super(props)
this.state = {
nums:[1,2,3]
}
this.test = this.test.bind(this)
}
test(){
// class采用同样的方式是没有问题的
this.state.nums.push(1)
this.setState({
nums: this.state.nums
})
}
render(){
let {nums} = this.state
return(
<div>
<div onClick={this.test}>测试</div>
<div>
{nums.map((item:any,index:number) => (
<div key={index}>{item}</div>
))}
</div>
</div>
)
}
}
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
45
46
47
48
49
50
51
52
53
54
useState
设置状态的时候,只有第一次生效,后期需要更新状态,必须通过useEffect
TableDeail
是一个公共组件,在调用它的父组件里面,我们通过set
改变columns
的值,以为传递给 TableDeail
的 columns
是最新的值,所以tabColumn
每次也是最新的值,但是实际tabColumn
是最开始的值,不会随着columns
的更新而更新:
const TableDeail = ({
columns,
}:TableData) => {
const [tabColumn, setTabColumn] = useState(columns)
}
// 正确的做法是通过useEffect改变这个值
const TableDeail = ({
columns,
}:TableData) => {
const [tabColumn, setTabColumn] = useState(columns)
useEffect(() =>{setTabColumn(columns)},[columns])
}
2
3
4
5
6
7
8
9
10
11
12
13
- 善用
useCallback
父组件传递给子组件事件句柄时,如果我们没有任何参数变动可能会选用useMemo
。但是每一次父组件渲染子组件即使没变化也会跟着渲染一次。
- 不要滥用
useContext
可以使用基于 useContext
封装的状态管理工具。
# React Hooks
和生命周期的关系?
函数组件 的本质是函数,没有 state
的概念的,因此不存在生命周期一说,仅仅是一个 render
函数而已。 但是引入 Hooks
之后就变得不同了,它能让组件在不使用 class
的情况下拥有 state
,所以就有了生命周期的概念,所谓的生命周期其实就是 useState
、 useEffect()
和 useLayoutEffect()
。
即:Hooks
组件(使用了Hooks
的函数组件)有生命周期,而函数组件(未使用Hooks
的函数组件)是没有生命周期的。
下面是具体的 class
与 Hooks
的生命周期对应关系:
constructor
:函数组件不需要构造函数,可以通过调用useState
来初始化state
。如果计算的代价比较昂贵,也可以传一个函数给useState
。
const [num, UpdateNum] = useState(0)
getDerivedStateFromProps
:一般情况下,我们不需要使用它,可以在渲染过程中更新state
,以达到实现getDerivedStateFromProps
的目的。
function ScrollView({row}) {
let [isScrollingDown, setIsScrollingDown] = useState(false);
let [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
// Row 自上次渲染以来发生过改变。更新 isScrollingDown。
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
}
2
3
4
5
6
7
8
9
10
React
会立即退出第一次渲染并用更新后的 state
重新运行组件以避免耗费太多性能。
shouldComponentUpdate
:可以用React.memo
包裹一个组件来对它的props
进行浅比较
const Button = React.memo((props) => { // 具体的组件});
复制代码
2
注意:React.memo
等效于 PureComponent
,它只浅比较 props
。这里也可以使用 useMemo
优化每一个节点。
render
:这是函数组件体本身。componentDidMount
,componentDidUpdate
:useLayoutEffect
与它们两的调用阶段是一样的。但是,我们推荐你一开始先用useEffect
,只有当它出问题的时候再尝试使用useLayoutEffect
。useEffect
可以表达所有这些的组合。
// componentDidMount
useEffect(()=>{
// 需要在 componentDidMount 执行的内容
}, [])
useEffect(() => {
// 在 componentDidMount,以及 count 更改时 componentDidUpdate 执行的内容
document.title = `You clicked ${count} times`;
return () => {
// 需要在 count 更改时 componentDidUpdate(先于 document.title = ... 执行,遵守先清理后更新)
// 以及 componentWillUnmount 执行的内容
} // 当函数中 Cleanup 函数会按照在代码中定义的顺序先后执行,与函数本身的特性无关
}, [count]); // 仅在 count 更改时更新
2
3
4
5
6
7
8
9
10
11
12
请记得 React
会等待浏览器完成画面渲染之后才会延迟调用 ,因此会使得额外操作很方便
componentWillUnmount
:相当于useEffect
里面返回的cleanup
函数
// componentDidMount/componentWillUnmount
useEffect(()=>{
// 需要在 componentDidMount 执行的内容
return function cleanup() {
// 需要在 componentWillUnmount 执行的内容
}
}, [])
2
3
4
5
6
7
componentDidCatch
、getDerivedStateFromError
:目前还没有这些方法的Hook
等价写法,但很快会加上。
class 组件 | Hooks 组件 |
---|---|
constructor | useState |
getDerivedStateFromProps | useState 里面 update 函数 |
shouldComponentUpdate | useMemo |
render | 函数本身 |
componentDidMount | useEffect |
componentDidUpdate | useEffect |
componentWillUnmount | useEffect 里面返回的函数 |
componentDidCatch | 无 |
getDerivedStateFromError | 无 |