React进阶:HOOK
Posted No Silver Bullet
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React进阶:HOOK相关的知识,希望对你有一定的参考价值。
文章目录
一、前言
Hook
是 React 16.8.0
的新增特性,React Native 0.59
及以上版本支持 Hook
。它可以让你在不编写 class
的情况下使用 state
以及其他的 React
特性。
请注意,要启用 Hook
,所有 React
相关的 package
都必须升级到 16.8.0
或更高版本。如果你忘记更新诸如 React DOM
之类的 package
,Hook
将无法运行。
Hook
是一些可以让你在函数组件里“钩入” React state
及生命周期等特性的函数。Hook
不能在 class
组件中使用 —— 这使得你不使用 class
也能使用 React
。
HOOK
可以让我们在函数组件中使用 state
、生命周期以及其他 react 特性,而不仅限于 class
组件中使用。react hooks
的出现,标示着 react
中不会在存在无状态组件,而是包含类组件和函数组件。react hooks
即是应用在函数组件中。
如果你在编写函数组件并意识到需要向其添加一些 state
,以前的做法是必须将其它转化为 class
。现在你可以在现有的函数组件中使用 Hook
。
二、HOOK函数介绍
2.1 State hook
hooks
使我们在函数组件中拥有使用state
的能力, 就是通过 useState
来实现的,首先来看一个简单的例子,这个例子用来显示一个计数器。当你点击按钮,计数器的值就会增加:
function App () {
// 声明一个叫 “count” 的 state 变量。
const [ count, setCount ] = useState(0)
return (
<div>
点击次数: { count }
<button onClick={() => { setCount(count + 1)}}>点我</button>
</div>
)
}
其中,useState
就是一个 Hook
。从代码中可以看到,useState
的使用非常简单,我们从 React
中拿到 useState
后,只需要在使用的地方直接调用 useState
函数就可以。 通过在函数组件里调用它来给组件添加一些内部 state
。React
会在重复渲染时保留这个 state
。useState
会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class
组件的 this.setState
,但是它不会把新的 state
和旧的 state
进行合并。
那么这里为什么叫 count
和 setCount
?可以使用别的名字吗,这里使用了 es6
的解构赋值,所以你可以给它起任何名字,updateCount
, doCount
、anything
,当然,为了编码规范,所以建议统一使用一种命名规范,尤其是第二个值。
useState
唯一的参数就是初始 state
。在上面的例子中,计数器是从零开始的,所以初始 state
就是 0。值得注意的是,不同于 this.state
,这里的 state
不一定要是一个对象 —— 如果你有需要,它也可以是。这个初始 state
参数只有在第一次渲染时会被用到。
当我们在使用 useState
时,修改值时传入同样的值,我们的组件会重新渲染吗,例如这样
function App () {
const [ count, setCount ] = useState(0)
console.log('component render count')
return (
<div>
点击次数: { count }
<button onClick={() => { setCount(count)}}>点我</button>
</div>
)
}
结果是不会,提升了组件的渲染性能。
useState 默认值
useState
支持我们在调用的时候直接传入一个值,来指定 state
的默认值,比如这样 useState(0), useState({ a: 1 }), useState([ 1, 2 ])
,还支持我们传入一个函数,来通过逻辑计算出默认值,比如这样:
function App (props) {
const [ count, setCount ] = useState(() => {
return props.count || 0
})
return (
<div>
点击次数: { count }
<button onClick={() => { setCount(count + 1)}}>点我</button>
</div>
)
}
这个时候,就有小伙伴问了,那我组件每渲染一次,useState
中的函数就会执行一边吗,浪费性能,其实不会,useState
中的函数只会执行一次,我们可以做个测试:
function App (props) {
const [ count, setCount ] = useState(() => {
console.log('useState default value function is call')
return props.count || 0
})
return (
<div>
点击次数: { count }
<button onClick={() => { setCount(count + 1)}}>点我</button>
</div>
)
}
结果如下:
声明多个 state 变量
当我们使用多个 useState
的时候,React
怎么知道哪个 state
对应哪个 useState
?答案是 React
靠的是 Hook
调用顺序。Hook
的调用顺序在每次渲染中都是相同的,所以它能够正常工作:
function App (props) {
let count, setCount
let sum, setSum
if (count > 2) {
[ count, setCount ] = useState(0)
[ sum, setSum ] = useState(10)
} else {
[ sum, setSum ] = useState(10)
[ count, setCount ] = useState(0)
}
return (
<div>
点击次数: { count }
总计:{ sum }
<button onClick={() => { setCount(count + 1); setSum(sum - 1)}}>点我</button>
</div>
)
}
当我们在运行时改变 useState
的顺序,数据会混乱,增加 useState
, 程序会报错。
2.2 Effect hook
Effect Hook
可以让你在函数组件中执行副作用操作,什么是副作用呢,就是除了状态相关的逻辑,比如网络请求,监听事件,查找 dom等动作均视为副作用。
在 React
组件中有两种常见副作用操作:需要清除的和不需要清除的。我们来更仔细地看一下他们之间的区别。
无需清除的 effect
有时候,我们只想在 React
更新 DOM
之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。让我们对比一下使用 class 和 Hook 都是怎么实现这些副作用的。
使用 class 的示例
在 React
的 class
组件中,render 函数
是不应该有任何副作用的。一般来说,在这里执行操作太早了,我们基本上都希望在 React
更新 DOM
之后才执行我们的操作。
这就是为什么在 React class
中,我们把副作用操作放到 componentDidMount
和 componentDidUpdate
函数中。以 React
实现计数器的 class 组件
为例。它在 React
对 DOM
进行操作之后,立即更新了 document
的 title
属性:
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>
);
}
}
在上面的代码中,需要在 class
组件中在两个生命周期函数中编写重复的代码逻辑。
这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React
的 class
组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。
现在让我们来看看如何使用 useEffect
执行相同的操作。
使用 Hook 的示例
useEffect
解决了 class 组件
存在的生命周期臃肿问题。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect
做了什么? 通过使用这个 Hook
,你可以告诉 React
组件需要在渲染后执行某些操作。React
会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM
更新之后调用它。在这个 effect
中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。
为什么在组件内部调用 useEffect
? 将 useEffect
放在组件内部让我们可以在 effect
中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook
使用了 javascript 的闭包机制,而不用在 JavaScript
已经提供了解决方案的情况下,还引入特定的 React API
。
useEffect
会在每次渲染后都执行吗? 是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。你可能会更容易接受 effect
发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React
保证了每次运行 effect
的同时,DOM
都已经更新完毕。
需要清除的 effect
之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在让我们来比较一下如何用 Class 和 Hook 来实现。
使用 Class 示例
在 React class
中,你通常会在 componentDidMount
中设置订阅,并在 componentWillUnmount
中清除它。例如,假设我们有一个 ChatAPI 模块,它允许我们订阅好友的在线状态。以下是我们如何使用 class 订阅和显示该状态:
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
你会注意到 componentDidMount
和 componentWillUnmount
之间的代码逻辑相互对应。使用生命周期函数迫使我们拆分这些逻辑代码,即使这两部分代码都作用于相同的副作用。
使用 Hook 示例
你可能认为需要单独的 effect
来执行清除操作。但由于添加和删除订阅的代码的紧密性,所以 useEffect
的设计原则是相关的业务逻辑需要在同一个地方执行。如果 effect
返回一个函数,React
将会在执行清除操作时调用它:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
为什么要在 effect
中返回一个函数? 这是 effect
可选的清除机制。每个 effect
都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect
的一部分。
React
何时清除 effect
? React
会在组件卸载的时候执行清除操作。正如之前学到的,effect
在每次渲染的时候都会执行。这就是为什么 React
会在执行当前 effect
之前对上一个 effect
进行清除。
注意
并不是必须为 effect 中返回的函数命名。这里我们将其命名为 cleanup
是为了表明此函数的目的,但其实也可以返回一个箭头函数或者给起一个别的名字。
useEffect 生命周期
如果你熟悉 React class
的生命周期函数,你可以把 useEffect Hook
看做 componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的组合。
以往我们在绑定事件、解绑事件、设定定时器、查找 dom 的时候,都是通过 componentDidMount
、componentDidUpdate
、componentWillUnmount
生命周期来实现的,而 useEffect
会在组件每次 render
之后调用,就相当于这三个生命周期函数,只不过可以通过传参来决定是否调用。
需要注意的是,useEffect
会返回一个回调函数,作用于清除上一次副作用遗留下来的状态,如果useEffect
只调用一次,该回调函数相当于 componentWillUnmount
生命周期。
具体看下面例子:
function App () {
const [ count, setCount ] = useState(0)
const [ width, setWidth ] = useState(document.body.clientWidth)
const onChange = () => {
setWidth(document.body.clientWidth)
}
useEffect(() => {
window.addEventListener('resize', onChange, false)
return () => {
window.removeEventListener('resize', onChange, false)
}
})
useEffect(() => {
document.title = count
})
return (
<div>
页面名称: { count }
页面宽度: { width }
<button onClick={() => { setCount(count + 1)}}>点我</button>
</div>
)
}
上面例子要处理两种副作用逻辑,这里我们既要处理 title,还要监听屏幕宽度改变,按照 class
的写法,我们要在生命周期中处理这两种逻辑,但在 hooks 中,我们只需要两个 useEffect
就能解决这些问题,我们之前提到,useEffect
能够返回一个函数,用来清除上一次副作用留下的状态,这个地方我们可以用来解绑事件监听,这个地方存在一个问题,就是 useEffect
是每次 render
之后就会调用,比如 title 的改变,相当于 componentDidUpdate
,但我们的事件监听不应该每次 render
之后,进行一次绑定和解绑,就是我们需要 useEffect
变成 componentDidMount
, 它的返回函数变成 componentWillUnmount
,这里就需要用到 useEffect 函数的第二个参数。
useEffect 的第二个参数
useEffect
的第二个参数,分三种情况:
- 什么都不传,组件每次
render
之后useEffect
都会调用,相当于componentDidMount
和componentDidUpdate
; - 传入一个空数组 [], 只会调用一次,相当于
componentDidMount
和componentWillUnmount
; - 传入一个数组,其中包括变量,只有这些变量变动时,
useEffect
才会执行;
具体看下面例子:
function App () {
const [ count, setCount ] = useState(0)
const [ width, setWidth ] = useState(document.body.clientWidth)
const onChange = () => {
setWidth(document.body.clientWidth)
}
useEffect(() => {
// 相当于 componentDidMount
console.log('add resize event')
window.addEventListener('resize', onChange, false)
return () => {
// 相当于 componentWillUnmount
window.removeEventListener('resize', onChange, false)
}
}, [])
useEffect(() => {
// 相当于 componentDidUpdate
document.title = count
})
useEffect(() => {
console.log(`count change: count is ${count}`)
}, [ count ])
return (
<div>
页面名称: { count }
页面宽度: { width }
<button onClick={() => { setCount(count + 1)}}>点我</button>
</div>
)
}
根据上面例子的运行结果,第一个 useEffect
中的 ‘add resize event’ 只会在第一次运行时输出一次,无论组件怎么 render
,都不会在输出;第二个 useEffect
会在每次组件 render
之后都执行,title 每次点击都会改变; 第三个 useEffect
, 只有在第一次运行和 count
改变时,才会执行,屏幕发生改变引起的 render
并不会影响第三个 useEffect
。
2.3 Context hook
context
中的 Provider
和 Consumer
,在类组件和函数组件中都能使用,contextType
只能在类组件中使用,因为它是类的静态属性,具体如何使用 useContext
呢?
const value = useContext(MyContext);
接收一个 context
对象(React.createContext
的返回值)并返回该 context
的当前值。当前的 context
值由上层组件中距离当前组件最近的 <MyContext.Provider>
的 value prop
决定。
当组件上层最近的 <MyContext.Provider>
更新时,该 Hook
会触发重渲染,并使用最新传递给 MyContext provider
的 context value
值。即使祖先使用 React.memo
或 shouldComponentUpdate
,也会在组件本身使用 useContext
时重新渲染。
useContext
的参数必须是 context
对象本身:
- 正确:
useContext(MyContext)
- 错误:
useContext(MyContext.Consumer)
- 错误:
useContext(MyContext.Provider)
调用了 useContext
的组件总会在 context
值变化时重新渲染。如果重渲染组件的开销较大,你可以通过使用 memoization
来优化。
提示
如果你在接触 Hook
前已经对 context API
比较熟悉,那应该可以理解,useContext(MyContext)
相当于 class
组件中的 static contextType = MyContext
或者 <MyContext.Consumer>
。
useContext(MyContext)
只是让你能够读取 context
的值以及订阅 context
的变化。你仍然需要在上层组件树中使用 <MyContext.Provider>
来为下层组件提供 context
。
把如下代码与 Context.Provider
放在一起
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
2.4 Memo hook
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
把“创建”函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算 memoized
值。这种优化有助于避免在每次渲染时都进行高开销的计算。
useMemo
是什么呢?它跟 memo
有关系吗?memo
就是函数组件的 PureComponent
,用来做性能优化的手段,useMemo
也是,useMemo
和 Vue
的computed
计算属性类似,都是根据依赖的值计算出结果,当依赖的值未发生改变的时候,不触发状态改变,useMemo
具体如何使用呢,看下面例子:
function App () {
const 以上是关于React进阶:HOOK的主要内容,如果未能解决你的问题,请参考以下文章