Hooks 是React的一次革命性升級,本文將對其優(yōu)勢和API進(jìn)行比較全面的解析
為什么要有hooks
在沒有hooks之前,除了對于一些無狀態(tài)組件可以使用函數(shù)來聲明組件以外,大家都會使用class來聲明組件。作為一個主要工作內(nèi)容為Android開發(fā)的我,早已習(xí)慣萬物皆class,而Android中的Activity(可以理解為每一個交互界面)就是class,所以也欣然接受,并且在ES6中class帶有的constructor、super以及react的生命周期函數(shù)對使用Java的Android開發(fā)者來講很容易理解接受。
但是使用class來聲明組件卻存在以下問題:
- 狀態(tài)邏輯難以復(fù)用
假如有一段邏輯代碼需要在多個組件中使用,那么在以前,可以通過以下幾個方式來實(shí)現(xiàn):
- copy代碼,顯然不符合代碼設(shè)計的原則
- 繼承,一方面,js只支持單繼承(Java中可以通過接口實(shí)現(xiàn)對多個類的實(shí)現(xiàn)),想要對復(fù)用多個組件的邏輯就無能為力;另一方面,只為了復(fù)用部- 分邏輯而濫用繼承,顯然是違背oop原則的
- HOC高階組件,使用HOC復(fù)用的原理很簡單,就是包裹封裝,比如我們想實(shí)現(xiàn)對onScroll方法的復(fù)用:
import React,{Component} from 'react'
function scrollable(Child) {
return class ScrollWrapper extends Component {
ref = React.createRef()
onScroll = (...args) => {
console.log('onscroll')
this.ref.current.onScroll(...args)
}
componentDidMount() {
document.addEventListener('scroll', this.onScroll, false);
}
componentWillUnmount() {
document.removeEventListener('scroll', this.onScroll, false);
}
render() {
return <Child ref={this.ref} />
}
}
}
class ScrollableApp extends Component {
onScroll(){
console.log('child onscroll')
}
render() {
return <div style={{color:'red',height:10000,width:800}}>
</div>
}
}
export default scrollable(ScrollableApp);
ScrollableApp通過高階組件即函數(shù)scrollable()封裝后,復(fù)用了ScrollWrapper中的onScroll()方法,并且還能再ScrollWrapper的onScroll使子組件ScrollableApp中的onScroll也能得到調(diào)用;使用渲染屬性也可以實(shí)現(xiàn)復(fù)用:
export class Scrollable extends Component {
onScroll = (...args) => {
console.log('onscroll')
console.log(this.props.children)
}
componentDidMount() {
document.addEventListener('scroll', this.onScroll, false);
}
componentWillUnmount() {
document.removeEventListener('scroll', this.onScroll, false);
}
render() {
return this.props.render()
}
}
export class ScrollableApp extends Component {
onScroll() {
console.log('child onscroll')
}
render() {
return <div style={{ color: 'red', height: 10000, width: 800 }}>
</div>
}
}
使用時:
function App() {
return (
<div>
<Scrollable render={() => <ScrollableApp></ScrollableApp>}></Scrollable>
</div>
);
}
export default App;
這個方法和高階組件方式差不多,不再贅述
使用這兩者雖然能實(shí)現(xiàn)邏輯復(fù)用,但無疑對代碼的簡潔和運(yùn)行性能都有不少的損耗
- 另外,“組合優(yōu)于繼承”,或許可以使用策略模式,抽取出不同的類來封裝這些邏輯,在使用時引入相關(guān)的類來實(shí)現(xiàn),但這無疑有點(diǎn)過度設(shè)計,也大大增加了代碼結(jié)構(gòu)的復(fù)雜度
類組件復(fù)雜,難以維護(hù),主要指生命周期函數(shù)混亂,比如上面的onScroll監(jiān)聽,在componentDidMount和componentWillUnmount分別要注冊反注冊,相關(guān)的邏輯分散在不同地方,而在componentDidMount往往還需要處理類似網(wǎng)絡(luò)請求等各種初始化的動作,也導(dǎo)致不相關(guān)的邏輯混雜在一起,使得代碼難以維護(hù)(這個在Android開發(fā)中其實(shí)也是再正常不過做法...)
this指向等問題
上面有一段代碼:
onScroll = (...args) => {
...
}
這里使用類屬性的方式定義onScroll,才能通過this.onScroll訪問到該方法,而如果聲明為類成員函數(shù),則在向下一級組件傳遞回調(diào)函數(shù)時無法正確訪問到該方法
而hooks則很好的解決了以上的問題
使用hooks
useState
- 使用
在沒有react hooks之前,組件可以分為有狀態(tài)組件和無狀態(tài)組件,如:
class Counter extends Component {
state = {
count: 0
}
render() {
return (
<div>
{this.state.count}
</div>
)
}
}
function Counter(props) {
return (
<div>
{props.count}
</div>
)
}
第一種寫法Counter中存儲了狀態(tài)state,而函數(shù)組件寫法中只能通過props來獲取狀態(tài)
使用useState:
import React, { useState } from 'react'
export default function Counter(props) {
const [count, setCount] = useState(0)
return (
<div>
<button
onClick={() => {
setCount(count=>count + 1)
}}>
</button>
{count}
</div>
)
}
這里const [count, setCount] = useState(0)中,相當(dāng)于定義了一個count變量作為該組件state的一個屬性,而useState中傳入的值為count的初始默認(rèn)值(也可以不傳入,則為undefined),setCount為改變count值的方法;以上代碼等價于:
export class Counter extends React.Component {
state = {
count: 0
}
render() {
return (
<div>
<button
onClick={() => {
this.setState(
{
count: this.state.count + 1
}
)
}}>
</button>
{this.state.count}
</div>
)
}
}
可見,使用useState使得代碼大大簡化,可以不使用class聲明組件,也不用擔(dān)心this指向的問題;并且從此我們不用再以有無狀態(tài)來區(qū)分組件了,因為函數(shù)組件也可以擁有狀態(tài)
2.原理
這里有幾個值得探討的問題:
useState()如何確定應(yīng)該返回的是哪一個component的state
這個很簡單,因為js運(yùn)行在單線程環(huán)境中,所以在運(yùn)行到某一個useState函數(shù)時,可以獲取到對應(yīng)的運(yùn)行上下文處在哪一個component中如何確定useState對應(yīng)于哪一個返回值
思考以下的偽代碼:
function Counter(props) {
if (someCondition) {
useState();
}
useState();
}
在實(shí)際執(zhí)行中會報錯,而且如果eslint配置了react-hooks/rules-of-hooks,會直接編譯報錯
實(shí)際上為了代碼盡可能簡潔,useState是通過記錄第一次運(yùn)行時的順序來確定之后的每次運(yùn)行分別返回對應(yīng)哪個state的,所以Hooks函數(shù)必須始終以相同的次序和數(shù)量被調(diào)用
- setState相同值的時候會否重新渲染
改寫之前的代碼,setCount時每次都為0,發(fā)現(xiàn)并不會執(zhí)行render函數(shù)
function Counter(props) {
const [count, setCount] = useState(0)
console.log('render')
return (
<div>
<button
onClick={() => {
setCount(0)
}}>
</button>
{count}
</div>
)
}
假如我們的state中存儲的是對象呢?
function Counter(props) {
const [countObj, setCountObj] = useState({ count: 0 })
console.log('render')
return (
<div>
<button
onClick={() => {
countObj.count = countObj.count + 1
setCountObj(countObj)
}}>
</button>
{countObj.count}
</div>
)
}
點(diǎn)擊button,發(fā)現(xiàn)也未重新渲染。因此在setState時,如果為對象,對比的地址值未改變,并不會重新render,這和PureComponent類似
useEffect
effec被翻譯過來為副作用,但是這個確很容易產(chǎn)生語義誤解;實(shí)際上副作用實(shí)際上指的是視圖組件與視圖組件之外系統(tǒng)進(jìn)行交互的行為u,比如與DOM交互,網(wǎng)絡(luò)請求,數(shù)據(jù)持久化操作等
假如我們需要在componentDidMount之后設(shè)置onScroll監(jiān)聽,使用class的寫法為:
class Scrollable extends React.Component {
onScroll = () => {
console.log('onscroll')
}
componentDidMount() {
document.addEventListener('scroll', this.onScroll, false);
}
}
通過useEffect改寫則為:
function Scrollable(props) {
useEffect(() => {
document.addEventListener('scroll', this.onScroll, false);
})
return <div></div>
}
useEffect()中的函數(shù)會在每次componentDidMount、componentDidUpdate的時候執(zhí)行,如果要在componentWillUnmount中取消監(jiān)聽也很簡單,只需在useEffect()傳入的函數(shù)中return相關(guān)處理函數(shù)即可:
function Scrollable(props) {
useEffect(() => {
document.addEventListener('scroll', this.onScroll, false);
return () => {
document.removeEventListener('scroll', this.onScroll, false);
};
})
return <div></div>
}
除此之外,useEffect還可以傳入第二個參數(shù),該參數(shù)類型為數(shù)組;這里分為三種情況:
1.不傳入數(shù)組參數(shù):在不傳入該數(shù)組的情況下(參考上面的代碼),每次componentDidMount、componentDidUpdate或componentWillUnmount時都會執(zhí)行對應(yīng)的副作用函數(shù)
2.傳入空數(shù)組:該副作用會在組件整個生命周期中只執(zhí)行一次、清理一次;這很適用于對網(wǎng)絡(luò)請求、事件監(jiān)聽等操作
3.傳入非空數(shù)組:該副作用會在數(shù)組中的各個參數(shù)發(fā)生變化時(對象比較地址值),才會在對應(yīng)的生命周期中重新執(zhí)行
通過useEffect,可以使得我們的代碼更簡潔,邏輯更清晰易維護(hù),對數(shù)組參數(shù)的控制也能幫助我們更輕松的寫出高性能的代碼
useContext
在沒有hooks之前Context就已經(jīng)存在,用以實(shí)現(xiàn)跨層級數(shù)據(jù)傳遞,一般使用Consumer和ContextType實(shí)現(xiàn)
但是Context的似乎使用得不是很多,多數(shù)還是通過redux的store存儲全局?jǐn)?shù)據(jù);useContext使得Context的使用更容易,可以在函數(shù)組件中使用Context,并且不用依賴ContextType,避免了每一個組件只能對應(yīng)一個ContextType的缺點(diǎn),當(dāng)然也不需要Consumer
使用很簡單(這里只是演示,代碼結(jié)構(gòu)可以根據(jù)實(shí)際做優(yōu)化;這里順便列出以往使用Consumer和ContextType實(shí)現(xiàn)Context數(shù)據(jù)傳遞的寫法作對比):
import React, { Component, createContext, useContext, useState } from 'react'
const CountContext = createContext(0)
function App() {
const [count, setCount] = useState(0)
return (
<div>
<button
onClick={() => {
setCount(count + 1)
}}>
</button>
<CountContext.Provider value={count}>
<CounterByConsummer></CounterByConsummer>
<Counter></Counter>
<CounterByContextType></CounterByContextType>
</CountContext.Provider>
</div>
);
}
//useContext寫法
function Counter(props) {
const count = useContext(CountContext)
return (
<div>
{count}
</div>
)
}
//Consummer寫法
class CounterByConsummer extends Component {
render() {
return (
<CountContext.Consumer>
{count => <div>{count}</div>}
</CountContext.Consumer>
)
}
}
//ContextType寫法
class CounterByContextType extends Component {
static contextType = CountContext
render() {
const count = this.context
return (
<div>
{count}
</div>
)
}
}
export default App;
需要注意的一點(diǎn)是,不要濫用context,因為會破壞組件的獨(dú)立性
useMemo&useCallback
理解memo
為了提高react的運(yùn)行效率,避免無用的重渲染,我們常使用繼承PureComponent的方式;而在函數(shù)組件則可以使用React.memo(Component)來達(dá)到同樣的效果;
使用useMemo
React.memo()針對組件,而useMemo則是針對組件的方法,思考如下代碼:
import React, { useMemo, useState } from 'react'
function App() {
const [name, setName] = useState('smartzheng')
const [age, setAge] = useState(18)
return (
<>
<button
onClick={() => {
setName(name + 'changed')
}}>
changeName
</button>
<button
onClick={() => {
setAge(age + 1)
}}>
changeAge
</button>
<Description age={age} name={name}></Description>
</>
);
}
function Description({ name, age }) {
function getAge(age) {
console.log('changeAge')
return age + '歲'
}
const newAge = useMemo(() => getAge(age), [age])
return <>
<div>
{name}
</div>
<div>
{newAge}
</div></>
}
export default App;
這段代碼主要做的是顯示兩個button,一個用來改變name,一個改變age,而子組件中對name和age進(jìn)行顯示,并且通過getAge()在age后加上“歲”字。測試發(fā)現(xiàn),點(diǎn)擊changeName和changeAge都會導(dǎo)致子組件重新執(zhí)行g(shù)etAge(),這并不是我們想要的結(jié)果;這里就可以通過useMemo來實(shí)現(xiàn)只有在age發(fā)生變化時才執(zhí)行g(shù)etAge(),使用很簡單,只需將 const newAge = useMemo(() => getAge(age), [age]) 改為const newAge = useMemo(() => getAge(age), [age])即可
在useMemo(() => getAge(age), [age])中,傳入的是一個函數(shù),可以理解為一個回調(diào),數(shù)組[age]代表該回調(diào)只有在age變化時才會執(zhí)行,而執(zhí)行的內(nèi)容為getAge(age)
使用useCallback
useCallback和useMemo很類似,不過他返回的是緩存的函數(shù):const fnA = useCallback(fnB, [a]),代表useCallback會將fnB函數(shù)返回,返回值是否改變依賴于a值是否改變。舉例如下:
import React, { useState, useCallback } from 'react';
const set = new Set();
export default function Callback() {
const [count, setCount] = useState(0);
const [val, setVal] = useState('');
const callback = useCallback(() => {
console.log(count);
}, [count]);
set.add(callback);
return <div>
<h1>{count}</h1>
<h1>{set.size}</h1>
<div>
<button onClick={() => setCount(count + 1)}>changeCount</button>
<input value={val} onChange={event => setVal(event.target.value)} />
</div>
</div>;
}
這里的callback就是對() => {console.log(count)的緩存,通過一個set來存放它,只有當(dāng)點(diǎn)擊changeCount的按鈕是set的size才會發(fā)生變化,說明只有在count變化時,才會返回新的callback方法,這對減少重復(fù)創(chuàng)建相同的方法對象很有幫助
useRef
在hooks之前,常用createRef來創(chuàng)建ref來獲取DOM元素的引用,React Hooks中則提供了useRef
- 使用useRef獲取DOM元素的引用
import React, { PureComponent, useRef } from 'react';
function App(props) {
const countRef = useRef();
return (
<>
<Counter ref={countRef}></Counter>
<button onClick={() => { console.log(countRef.current) }}></button>
</>
)
}
class Counter extends PureComponent {
render() {
return (
<div>
</div>
)
}
}
export default App
在上述代碼中,通過useRef()創(chuàng)建了countRef并在Counter組件上進(jìn)行賦值,點(diǎn)擊button,每次都會正確打印出Counter組件
值得注意的是,這里的Counter如果使用函數(shù)組件則會報錯,提示function components can not be given refs,原因是函數(shù)組件會被React底層處理,被class wrap,所以直接給該函數(shù)組件進(jìn)行ref賦值沒有意義;從這里也可以發(fā)現(xiàn)函數(shù)組件還不能完全替代類組件
- 使用useRef存儲對象
正常情況下,如果在函數(shù)組件中聲明一個變量,那么該變量會在每次渲染時重新創(chuàng)建,而使用useRef可以實(shí)現(xiàn)跨越聲明周期存儲數(shù)據(jù),思考如下代碼:
import React, { useRef,useEffect,useState } from 'react'
function App(props) {
const [count, setCount] = useState(0)
let interval;
useEffect(()=>{
interval = setInterval(()=>{
setCount(count+1)
},1000)
},[])
if(count>5){
clearInterval(interval)
}
return (
<>
<div>{count}</div>
</>
)
}
export default App
當(dāng)count大于5時,清除定時器,這樣寫肯定是無效的,因為每次都會創(chuàng)建一個新的interval,clearInterval中的interval并不是最開始的interval,通過useRef改寫即可實(shí)現(xiàn):
import React, { useRef,useEffect,useState } from 'react'
function App(props) {
const [count, setCount] = useState(0)
let interval = useRef();
useEffect(()=>{
interval.current = setInterval(()=>{
setCount(count+1)
},1000)
},[])
if(count>5){
clearInterval(interval.current)
}
return (
<>
<div>{count}</div>
</>
)
}
export default App
自定義Hooks
前面提到類組件有三個缺點(diǎn),首當(dāng)其沖的是邏輯復(fù)用問題,我們可以通過自定義Hooks來解決該問題,例如我們通過自定義useCount來復(fù)用一個定時器:
import React, { useRef, useEffect, useState } from 'react'
function App(props) {
const [count] = useCount(0)
return (
<>
<div>{count}</div>
</>
)
}
function useCount(defaultCount){
const [count, setCount] = useState(0)
let interval = useRef();
useEffect(() => {
interval.current = setInterval(() => {
setCount(count => count + 1)
}, 1000)
}, [])
useEffect(()=>{
if (count >= 5) {
clearInterval(interval.current)
}
})
return [count, setCount]
}
export default App
關(guān)于React Hooks的優(yōu)勢和常用API使用先寫到這,后面的文章再對Hooks的深層實(shí)現(xiàn)原理和自定義Hooks學(xué)習(xí)和解析