React Hooks 起源
- React 一直都提倡使用函数式组件。更轻便,更优雅,性能更佳。函数式组件又称无状态组件(FSC)。
- 以前,需要使用
state
,生命周期等React 特性,必须重构为 class 组件。 Hooks
是 React 16.8 新增的特性,它可以让你在不编写class
的情况下使用state
以及其他的 React 特性。- 现在,你可以直接在现有的函数式组件中使用
Hooks
,而无须重构为class
组件。 - 全新的思维方式。no magic, just javascript and some rules。
类组件被诟病
- 类(累):自js开天辟地,就是面向函数式编程(
FP
), 面向对象编程(OOP
)为何物。烦人的构造函数。super
是什么?…… - this绑定:类方法不会自动绑定 this 到实例上。现有四种
bind
方式。不优雅,易出错,bind
还影响性能(使用箭头函数后有所改善)。 setState()
: 异步更新机制,state
浅合并机制。不理解这些概念,很容易踩坑。- 生命周期耦合:每个生命周期方法通常包含一堆不相关的逻辑;不同生命周期中的逻辑又有关联。
下面组件来自实际项目,经过简化和微调(方便演示和直观感受),基本上暴露出了上面所有问题。业务逻辑严谨性不用推敲:
class NumberInput extends React.Component {
constructor(props) {
// 为什么必须super,不传props会怎样
super(props)
this.state = {
focus: false
}
this.tradingpwd = ''
// 第一种bind,官方推荐
;['onBlur'].forEach(method => {
this[method] = this[method].bind(this)
})
}
// 下面两个生命周期得相互配合,实现某些功能
componentDidMount() {
this.tradingPwdHideInput.focus()
// 处理某类兼容问题
let bodyTop = document.body.getBoundingClientRect().top
const styleText = 'position: fixed; width: 100%; top: ' + bodyTop + 'px'
document.body.style.cssText = styleText
}
componentWillUnmount() {
this.tradingPwdHideInput.blur()
document.body.style.position = 'static'
}
tradingPwdChange(e) {
// ...
this.tradingpwd = e.target.value
this.props.inputChangeCallback(e.target.value)
// ...
}
// 第二种bind
onFocus = () => {
this.setState({
focus: true
})
}
onBlur() {
this.setState({
focus: false
})
}
render() {
return (
<div className={classNames('NumberInput')}>
<input
type='tel'
ref={ref => {
this.tradingPwdHideInput = ref
}}
id='tradingPwdHideInput'
/* 第三种bind */
onClick={() => {
this.tradingPwdHideInput.focus()
}}
onBlur={ this.onBlur }
onFocus={ this.onFocus }
/* 第四种bind,不推荐,在每次 render() 方法执行时绑定类方法,消耗性能*/
onChange={ this.tradingPwdChange.bind(this) }
/>
</div>
)
}
}
proposal-class-fields 新提案会改善上述情况,目前处于第三阶段。
随着类组件趋于复杂,还有其他诟病:
- 难拆分,本地
state
逻辑到处都是,当组件越来越复杂,想拆分比较难。 - 状态逻辑难复用:需要引入高阶特性进行代码重构,需要调整组件结构,成本高。
- 抽象地狱:大型
React
往往使用render props
,HOC
,Context
等高阶特性,形成大量包装组件(wrapping components)。层级冗余,逻辑难追踪。
Hooks 优越性
Hooks
引入的一个重要的原因,就是类组件存在着种种诟病。那他必然存在一些优越性。
在说明这些优越性之前,先了解一个概念:
副作用:React 中主要指那些没有发生在数据向视图(M-V)转换过程中的逻辑,如
Ajax
请求、访问原生DOM
元素、本地持久化缓存、绑定/解绑事件、添加/取消订阅、设置定时器、记录日志等。
Hooks 的优越性:
- 函数式编程:No
class
, Nosuper
, Nothis
。对于不了解OOP
的React
初学者更友好。 - 有状态逻辑易复用:可以通过
Custom Hook
(后面讲解)重构,而不用修改组件结构。 - 易拆分:状态管理和副作用管理松耦合,原子性强。很容易将一些相关联的逻辑拆分成更小的函数。
- 可逐步引入:
Hooks
向后兼容,与现有代码可并行工作,因此我们可以逐步采用它们。 - 副作用分组:很多副作用逻辑分散在类组件生命周期函数中。而
Hooks
可以将每个副作用的设置和清理封装在一个函数中。 - 副作用分离:副作用操作都在页面渲染之后。
抛弃类组件?
既然 Hooks
存在这么多优越性。那是不是就到了抛弃 class
组件的时候了。
对此,官方说:
- 新版本依然支持
class
相关API,在相当一段时期内,class
组件 和Hooks
组件并存。 - 向后兼容,是加法。注意,是函数组件的加法,即
Hooks
只能用在函数组件中。 - 推荐使用 函数组件 +
Hooks
。
个人看法:
不抛弃,不放弃。class
组件将我们带到了 OOP
的世界,OOP
在编程界举足轻重,其思想是值得学习的,还会长期存在。即便 class
组件已然成为一种历史产物,但他的存量巨大,依然需要去维护,去慢慢消化。
所以:
- 对于
React
老司机:拥抱Hooks
,是拥抱变化。这个变化,是加法,是学习新的API
,新的技能,新的思想。 - 对于
React
新手:拥抱Hooks
,降低了学习门槛,可以更快入门。但是类组件也非常有必要去了解,理解。知己知彼,重构不殆。
说了这么多,我们来和这些 React Hooks
的 API
见个面:
基础 Hook
useState
简单状态管理useEffect
副作用管理
常用 Hook
useContext
全局状态管理useReducer
复杂状态管理useRef
访问Dom
元素
其他 Hook
useMemo
useCallback
useImperativeHandle
useLayoutEffect
useDebugValue
- … 还会增加
useState hook
- 功能:在函数组件中用来进行简单状态管理,创建一些本地
state
。 - API:
const [currentState, setFunction] = useState(initialState);
。传一个参数,返回一个数组(包含两个值)- 三要素。
import React, { useState } from 'react';
function Form() {
// ES6 解构
const [name, setName] = useState('Mary'); // State 变量 1
const [surname, setSurname] = useState('Poppins'); // State 变量 2
const [width, setWidth] = useState(window.innerWidth); // State 变量 3
function handleNameChange(e) {
setName(e.target.value);
}
function handleSurnameChange(e) {
setSurname(e.target.value);
}
return (
<>
<input value={name} onChange={handleNameChange} />
<input value={surname} onChange={handleSurnameChange} />
<p>Hello, {name} {surname}</p>
<p>Window width: {width}</p>
</>
);
}
export default Form;
useState「粒度」问题
看到这里,对于写过class
组件的我们,很容易产生一个疑问。 实际工作中,一个类组件的 this.state
中往往有十几项,用 Hooks
改写的话难道要写十几个 useState
么?
根据官方文档,总结下来,有几点:
- 建议将
state
分割为多个useState
。粒度更细,更易于管理,更好复用。 - 可能一起改变的
state
可合并成一个useState
( 比如Dom
元素的top
left
)。 - 当
state
逻辑趋于复杂,建议使用useReducer
或Custom Hook
管理(后面介绍)。
当组件的 state
很多的时候,为了提高代码的可读性,也可以把逻辑相关的一些 state
合并为一个 useState
( 比如分页参数 )。但这些 state
并不是一起改变的,所以当其中一个 state
改变,调用对应的 setFunction
的时候。你需要做对象合并(不合并就丢了):
const [ pageData, setPageDate ] = useState({ pageSize: 20, current: 1, total:0, })
const onPageChange = current => {
// 常规操作
setPageDate( Object.assign( {}, pageData, { current } ) )
// 官方建议
setPageDate(currentPageData => ({ ...currentPageData, current}));
}
知识点:调用
useState
的更新函数时,可以传一个箭头函数,这个函数的参数是当前最新的state
, 返回值是要设置的state
。
useEffect hook
API 可抽象为: useEffect(arrowFunction, [depsArr])
arrowFunction
: 必须。执行函数,执行副作用操作。它决定了做什么。depsArr
: 非必须。一个依赖项数组。它决定了什么时候做(下面示例中介绍)。
根据实际情况,可细分为三种:
// 第一种
// 最基础的,只有箭头函数。没有依赖项,所以组件每次渲染都会执行。
// 相当于 componentDidMount + componentDidUpdate
useEffect(() => {
//side-effect
})
// 第二种
// 有依赖项,是一个空数组,因为它永远不会变,所以只会首次执行。
// 相当于 componentDidMount
useEffect(() => {
//side-effect
}, [])
// 第三种
// 有第二个参数,且非空数组。首次渲染会执行。重新渲染时,只有当依赖项的值改变了才会执行。
useEffect(() => {
//side-effect
}, [...state])
总结下来:
- 功能:管理
React
函数组件的副作用,赋予生命周期能力。 - 怎么管:组件每次渲染到屏幕之后,根据依赖项的情况判断是否调用执行函数。
- 二要素:执行函数,依赖项。
- 清理机制:你可以在执行函数中返回另一个函数-清理函数,清理函数会在组件卸载的时候,会在组件重新渲染,且useEffect的依赖项值改变的时候调用。起到了 class 组件中
componentWillUnmount
的作用, 后续会在场景实例中介绍。 - 使用上:和
useState
一样,可使用多个。建议一个副作用对应一个useEffect
。
以定时器为例,让我们来实现一个秒表组件。这是一个学习和理解 useEffect
非常有意思的例子:
import React, { useState, useEffect } from 'react';
function App() {
// 秒表开关
const [isOn, setIsOn] = useState(false);
// 计数
const [timer, setTimer] = useState(0);
useEffect(() => {
let interval;
//开关打开的时候才执行
if (isOn) {
// 通过定时器增加计数
interval = setInterval(
() => setTimer(val => val + 1),
1000,
);
}
// 需要清除定时器
return () => clearInterval(interval);
},[isOn]);
return (
<>
<p>{timer}</p>
{!isOn && (
<button type="button" onClick={() => setIsOn(true)}>
Start
</button>
)}
{isOn && (
<button type="button" onClick={() => setIsOn(false)}>
Stop
</button>
)}
</>
);
}
export default App;
规则:
useEffect
不能接收async
作为执行函数。useEffect
接收的函数,要么返回一个能清除副作用的函数,要么就不返回任何内容。而async
返回的是promise
。useEffect
调用的函数如果依赖state
或者props
。最好在执行函数中定义。这样依赖容易追踪。
useEffect
的使用,看起来很简单。但是要做到不滥用,正确使用也不是那么容易。主要在使用之前要多一些思考。
Custom Hooks
它并不是 React hooks
的 API
,而是自定义 hook。顾名思义,React
允许你构建自己的 hooks
。在学习完前面两个最受欢迎的 hooks
以后,你完全具备了实现自定义 hooks
的能力。
官网定义: 自定义
Hook
是一个JavaScript
函数,其名称以”use”
开头,可以调用其他Hook
。
为什么需要Custom Hooks?
useState
解决了函数组件无状态的问题。useEffect
实现了副作用管理,生命周期的功能。Custom Hooks
将解决有状态(stateful
)逻辑共享的问题(相当于类组件中Hoc
的功能)。👇
我们来到一个实际场景。如今 HTML5
移动应用或 Web app
中越来越普遍的使用了离线浏览技术,所以用 JS
检测浏览器在线/离线状态非常常见。首先,我们用 React Hooks
来实现这个功能:
import React, { useState, useEffect } from 'react';
function App() {
const [isOffline, setIsOffline] = useState(window.navigator.onLine);
// 离线事件处理方法
function onOffline() {
setIsOffline(true);
}
// 在线事件处理方法
function onOnline() {
setIsOffline(false);
}
useEffect(() => {
// 事件监听
window.addEventListener('offline', onOffline);
window.addEventListener('online', onOnline);
// 清理函数
return () => {
window.removeEventListener('offline', onOffline);
window.removeEventListener('online', onOnline);
};
}, []); // 只需要首次执行
return (
<>
{
isOffline
? <div>网断已断开 ...</div>
: <div>网络已连接 ...</div>
}
</>
)
}
export default App;
无论浏览器是否在线,
navigator.onLine
属性都会提供一个布尔值。 如果浏览器在线,则设置为true
,否则设置为false
。
OK,我们实现了一个很不错的功能。很明显,这个功能是可复用的,应该共享的。
我们把功能逻辑提取出来,把它封装成一个 Custom hook
就可以了:
import React, { useState, useEffect } from 'react';
// 自定义 hook
function useOffline() {
const [isOffline, setIsOffline] = useState(window.navigator.onLine);
function onOffline() {
setIsOffline(true);
}
function onOnline() {
setIsOffline(false);
}
useEffect(() => {
window.addEventListener('offline', onOffline);
window.addEventListener('online', onOnline);
return () => {
window.removeEventListener('offline', onOffline);
window.removeEventListener('online', onOnline);
};
}, []);
return isOffline; // 只暴露一个 state
}
// 函数组件
function App() {
const isOffline = useOffline();
return (
<>
{
isOffline
? <div>网断已断开 ...</div>
: <div>网络已连接 ...</div>
}
</>
)
}
export default App;
从重构层面来说,就是把组件中的一些 hooks
抽离到一个函数中,再使用这个函数。这个函数就是 React custom hooks
。
书写 custom hooks
需要注意些什么呢:
- 自定义
Hooks
自然地遵循Hooks
设计的约定。即遵循所有你用到的Hooks
的规则。 - 请使用
use
开头。这个习惯非常重要。如果没有它,我们就不能自动检查该Hook
是否违反了Hooks
的规则,因为我们无法判断某个函数是否包含对其内部Hooks
的调用。
useRef Hook
在典型的 React
数据流中,DOM
元素的修改都是通过 M-V
的形式重新渲染。M
主要是组件的本地 state
,以及父组件传递给子组件的 props
。
但是,在某些情况下,你需要在典型数据流之外,手动使用/操作 DOM
:
- 管理焦点,媒体播放,文本选择,触发强制动画等。
- 集成第三方
DOM
库。
import React, { useState, useEffect, useRef } from "react";
function App() {
const [value, setValue] = useState("hellow hooks");
//创建 refs 只有这里有修改
const inputRef = useRef();
// 日志A
console.log(inputRef.current)
useEffect(() => {
// 日志B
console.log(inputRef.current);
inputRef.current.focus();
});
function inputHandler(val) {
setValue(val.target.value);
}
return (
<>
<input type="text" value={value} ref={inputRef} onChange={inputHandler} />
</>
);
}
export default App;
- 类组件中:
React16.3
及以上版本建议使用React.createRef
这个API
,低版本使用回调refs
。 - 函数组件中:为性能考虑,建议都使用
useRef
这个hook
。
useContext Hook
React 16.3.0
中引入了 Context
系列 API
。用于共享那些对于一个组件树而言是 “全局” 的 state
。可用于解决 prop drilling
问题。
useContext Hook
只是新增了一种方式,让你在函数组件中更方便,更优雅的消费 context
:
//
// 目标子组件 - D
// 使用 context 对象的 Consumer 组件获取共享数据,通过更新函数更新数据
const D = () => (
const { value, setValue } = React.useContext(MyContext)
return (
<input
type="text"
value={value}
onChange={e => {
setValue(e.target.value);
}}
/>
)
);
可读性更高了,还可很方便的消费多个 context
。
UseReducer Hook
useReducer hook
用于 React
函数组件中管理复杂的 state
。它把一个reducer
方法,和初始state
作为输入,包含当前state
,和一个dispatch
方法的解构数组作为输出。
API
( 对照 useState
):
// useReducer hook API
const [current, dispatch] = useReducer(reducer, initState);
// useState hook API
const [current, setFunc] = useReducer(initState);
来看一个使用 useReducer
的简单例子:
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
上述示例中的 state
很简单,其实使用 useState
就可以。那么,我们什么时候该使用 useReducer
呢?
适用于useReducer
的复杂 state
的场景主要有:
state
逻辑较复杂且包含多个子值(大的对象,数组)。state
更新依赖于之前的state
。state
组件树深层更新。使用useReducer
可以向子组件传递dispatch
而不是回调函数,这样可以优化性能。
原理
- 集中管理:每个组件都有一个 “内存单元” 的内部列表,作为
Hooks
管理中心。 - just js:只是
JavaScript
对象 + 闭包。你可以想象它是一个数组(实际上是一个单向链表),我们可以在其中放置一些数据。 - 顺序调用:当组件调用
useState()
等Hook
时,它读取当前单元格(或在第一次呈现时初始化它),然后将指针(cursor
) 移动到下一个单元格。这就是多个useState()
调用各自获取独立本地state
的方式。
缺点
需要开发者遵从许多规则。理解并合理运用这些规则,能写出优雅的,可读性高的,性能好的代码。反之,很容易出现死循环,数据重复请求等问题。最让人担心的是性能,很多时候业务功能实现了,但是其实存在很多不必要的开销。