作者:Dave Ceddia 译者:前端小智
阿里云最近在做活动,低至2折,有兴趣可以看看:
https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=pxuujn3r
为了保证的可读性,本文采用意译而非直译。
我们大部分 React 类组件可以保存状态,而函数组件不能?并且类组件具有生命周期,而函数组件却不能?
React 早期版本,类组件可以通过继承PureComponent
来优化一些不必要的渲染,相对于函数组件,React 官网没有提供对应的方法来缓存函数组件以减少一些不必要的渲染,直接 16.6 出来的 React.memo
函数。
React 16.8 新出来的Hook
可以让React 函数组件具有状态,并提供类似 componentDidMount
和componentDidUpdate
等生命周期方法。
Hooks
不会替换类,它们只是一个你可以使用的新工具。React 团队表示他们没有计划在React中弃用类,所以如果你想继续使用它们,可以继续用。
我能体会那种总有新东西要学的感觉有多痛苦,不会就感觉咱们总是落后一样。Hooks
可以当作一个很好的新特性来使用。当然没有必要用 Hook 来重构原来的代码, React团队也建议不要这样做。
来看看Hooks的例子,咱们先从最熟悉的开始:函数组件。
以下 OneTimeButton 是函数组件,所做的事情就是当我们点击的时候调用 sayHi
方法。
import React from 'react';
import { render } from 'react-dom';
function OneTimeButton(props) {
return (
<button onClick={props.onClick}>
点我点我
</button>
)
}
function sayHi() {
console.log('yo')
}
render(
<OneTimeButton onClick={sayHi}/>,
document.querySelector('#root')
)
我们想让这个组件做的是,跟踪它是否被点击,如果被点击了,禁用按钮,就像一次性开关一样。
但它需要一个state,因为是一个函数,它不可能有状态(React 16.8之前),所以需要重构成类。
函数组件转换为类组件的过程中大概有5个阶段:
*否认:也许它不需要是一个类,我们可以把 state 放到其它地方。
实现:废话,必须把它变成一个class
,不是吗?
接受:好吧,我会改的。
努力加班重写:首先 写 class Thing extends React.Component
,然后 实现 render
等等 。
最后:添加state。
class OneTimeButton extends React.Component {
state = {
clicked: false
}
handleClick = () => {
this.props.onClick();
// Ok, no more clicking.
this.setState({ clicked: true });
}
render() {
return (
<button
onClick={this.handleClick}
disabled={this.state.clicked}
>
You Can Only Click Me Once
</button>
);
}
}
这是相当多的代码,组件的结构也发生了很大的变化, 我们需要多个小的功能,就需要改写很多。
接下来,使用新的 useState
hook向普通函数组件添加状态:
import React, { useState } from 'react'
function OneTimeButton(props) {
const [clicked, setClicked] = useState(false)
function doClick() {
props.onClick();
setClicked(true)
}
return (
<button
onClick={clicked ? undefined : doClick}
disabled={clicked}
>
点我点我
</button>
)
}
这段代码的大部分看起来像我们一分钟前写的普通函数组件,除了useState
。
useState
是一个hook。它的名字以“use”开头(这是Hooks的规则之一 - 它们的名字必须以“use”开头)。
useState
hook 的参数是 state 的初始值,返回一个包含两个元素的数组:当前state和一个用于更改state 的函数。
类组件有一个大的state对象,一个函数this.setState
一次改变整个state对象。
函数组件根本没有状态,但useState
hook允许我们在需要时添加很小的状态块。因此,如果只需要一个布尔值,我们就可以创建一些状态来保存它。
由于Hook
以某种特殊方式创建这些状态,并且在函数组件内也没有像setState
函数来更改状态,因此 Hook 需要一个函数来更新每个状态。所以 useState
返回是一对对应关系:一个值,一个更新该值函数。当然,值可以是任何东西 - 任何JS类型 - 数字,布尔值,对象,数组等。
现在,你应该有很多疑问,如:
当组件重新渲染时,每次都不会重新创建新的状态吗?React如何知道旧状态是什么?
为什么hook 名称必须以“use”开头?这看起来很可疑。
如果这是一个命名规则,那是否意味着我可以自定义 Hook。
如何存储更复杂的状态,很多场景不单单只有一个状态值这么简单。
将有状态信息存储在看似无状态的函数组件中,这是一个奇怪的悖论。这是第一个关于钩子的问题,咱们必须弄清楚它们是如何工作的。
原作者得的第一个猜测是某种编译器的在背后操众。搜索代码useWhatever
并以某种方式用有状态逻辑替换它。
然后再听说了调用顺序规则(它们每次必须以相同的顺序调用),这让我更加困惑。这就是它的工作原理。
React第一次渲染函数组件时,它同时会创建一个对象与之共存,该对象是该组件实例的定制对象,而不是全局对象。只要组件存在于DOM中,这个组件的对象就会一直存在。
使用该对象,React
可以跟踪属于组件的各种元数据位。
请记住,React组件甚至函数组件都从未进行过自渲染。它们不直接返回HTML
。组件依赖于React
在适当的时候调用它们,它们返回的对象结构React
可以转换为DOM
节点。
React有能力在调用每个组件之前做一些设置,这就是它设置这个状态的时候。
其中做的一件事设置 Hooks 数组。它开始是空的, 每次调用一个hook
时,React 都会向该数组添加该 hook
。
假设咱们有以下这个组件:
function AudioPlayer() {
const [volume, setVolume] = useState(80);
const [position, setPosition] = useState(0);
const [isPlaying, setPlaying] = useState(false);
.....
}
因为它调用useState
3次,React 会在第一次渲染时将这三个 hook 放入 Hooks 数组中。
下次渲染时,同样的3
个hooks
以相同的顺序被调用,所以React
可以查看它的数组,并发现已经在位置0
有一个useState
hook ,所以React
不会创建一个新状态,而是返回现有状态。
这就是React能够在多个函数调用中创建和维护状态的方式,即使变量本身每次都超出作用域。
让咱们更详细地看看这是如何实现的,第一次渲染:
React 创建组件时,它还没有调用函数。React 创建元数据对象和Hooks的空数组。假设这个对象有一个名为nextHook
的属性,它被放到索引为0
的位置上,运行的第一个hook将占用位置0
。
React 调用你的组件(这意味着它知道存储hooks
的元数据对象)。
调用useState
,React创建一个新的状态,将它放在hooks
数组的第0
位,并返回[volume,setVolume]
对,并将volume
设置为其初始值80
,它还将nextHook
索引递增1。
再次调用useState
,React查看数组的第1
位,看到它是空的,并创建一个新的状态。然后它将nextHook
索引递增为2
,并返回[position,setPosition]
。
第三次调用useState
。React看到位置2
为空,同样创建新状态,将nextHook
递增到3
,并返回[isPlaying,setPlaying]
。
现在,hooks
数组中有3
个hook,渲染完成。下一次渲染会发生什么?
React
需要重新渲染组件, 由于 React 之前已经看过这个组件,它已经有了元数据关联。
React
将nextHook
索引重置为0
,并调用组件。
调用useState
,React查看索引0
处的hooks数组,并发现它已经在该槽中有一个hook。,所以无需重新创建一个,它将nextHook
推进到索引1
并返回[volume,setVolume]
,其中volume
仍设置为80
。
再次调用useState
。这次,nextHook
为1
,所以React
检查数组的索引1
。同样,hook 已经存在,所以它递增nextHook
并返回[position,setPosition]
。
第三次调用useState
,我想你知道现在发生了什么。
就是这样了,知道了原理,看起来也就不那么神奇了, 但它确实依赖于一些规则,所以才有使用 Hooks 规则。
自定义 hooks 函数只需要遵守规则 3 :它们的名称必须以“use”为前缀。
例如,我们可以从AudioPlayer
组件中将3个状态提取到自己的自定义钩子中:
function AudioPlayer() {
// Extract these 3 pieces of state:
const [volume, setVolume] = useState(80);
const [position, setPosition] = useState(0);
const [isPlaying, setPlaying] = useState(false);
// < beautiful audio player goes here >
}
因此,咱们可以创建一个专门处理这些状态的新函数,并使用一些额外的方法返回一个对象,以便更容易启动和停止播放,例如:
function usePlayerState(lengthOfClip) {
const [volume, setVolume] = useState(80);
const [position, setPosition] = useState(0);
const [isPlaying, setPlaying] = useState(false);
const stop = () => {
setPlaying(false);
setPosition(0);
}
const start = () => {
setPlaying(true);
}
return {
volume,
position,
isPlaying,
setVolume,
setPosition,
start,
stop
};
}
像这样提取状态的一个好处是可以将相关的逻辑和行为组合在一起。可以提取一组状态和相关事件处理程序以及其他更新逻辑,这不仅可以清理组件代码,还可以使这些逻辑和行为可重用。
另外,通过在自定义hooks
中调用自定义hooks
,可以将hooks
组合在一起。hooks只是函数,当然,函数可以调用其他函数。
Hooks 提供了一种新的方式来处理React中的问题,其中的思想是很有意思且新奇的。
React团队整合了一组很棒的文档和一个常见问题解答,从是否需要重写所有的类组件到钩Hooks是否因为在渲染中创建函数而变慢? 以及两者之间的所有东西,所以一定要看看。
代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。
原文:https://daveceddia.com/archives/
干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。
https://github.com/qq449245884/xiaozhi
我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复福利,即可看到福利,你懂的。