本文是 React Hooks 深刻系列的後續。此篇詳細介紹了 Hooks 相對 class 的優點所在, 並介紹了相關 api 的設計思想, 同時對 Hooks 如何對齊 class 的生命週期鉤子做了闡述。html
React 的 logo 是一個原子圖案, 原子組成了物質的表現。相似的, React 就像原子般構成了頁面的表現; 而 Hooks 就如夸克, 其更接近 React 本質的樣子, 可是直到 4 年後的今天才被真正設計出來。 —— Dan in React Conf(2018)react
一: 多個組件間邏輯複用
: 在 Class 中使用 React 不能將帶有 state 的邏輯給單獨抽離成 function, 其只能經過嵌套組件的方式來解決多個組件間邏輯複用的問題, 基於嵌套組件的思想存在 HOC 與 render props
兩種設計模式。可是這兩種設計模式是否存在缺陷呢?git
二: 單個組件中的邏輯複用
: Class 中的生命週期 componentDidMount
、componentDidUpdate
甚至 componentWillUnMount
中的大多數邏輯基本是相似的, 必須拆散在不一樣生命週期中維護相同的邏輯對使用者是不友好的, 這樣也形成了組件的代碼量增長。github
三: Class 的其它一些問題: 在 React 使用 Class 須要書寫大量樣板, 用戶一般會對 Class 中 Constructor 的 bind 以及 this 的使用感到困惑; 當結合 class 與 TypeScript 一塊兒使用時, 須要對 defaultValue 作額外聲明處理; 此外 React Team 表示 Class 在機器編譯優化方面也不是很理想。redux
緣由是數組的解構比對象更加方便, 能夠觀察如下兩種數據結構解構的差別。設計模式
返回數組時, 能夠直接解構成任意名字。api
[name, setName] = useState('路飛')
[age, setAge] = useState(12)
複製代碼
返回對象時, 卻須要多一層的命名。數組
{value: name, setValue: setName} = useState('路飛')
{value: old, setValue: setOld} = useState(12)
複製代碼
Hooks 是否能夠設計成在組件中經過函數傳參來使用? 好比進行以下調用?緩存
const SomeContext = require('./SomeContext)
function Example({ someProp }, hooks) {
const contextValue = hooks.useContext(SomeContext)
return <div>{someProp}{contextValue}</div>
}
複製代碼
使用傳遞的劣勢是會出現冗餘的傳遞。(能夠聯想 context 解決了什麼)安全
Hooks 中的 setState 與 Class 中最大區別在於 Hooks 不會對屢次 setState 進行合併操做。若是要執行合併操做, 可執行以下操做:
setState(prevState => {
return { ...prevState, ...updateValues }
})
複製代碼
此外能夠對 class 與 Hooks 之間 setState
是異步仍是同步的表現進行對比, 能夠先對如下 4 種情形 render 輸出的個數進行觀察分析:
class 中的 setState:
export default class App extends React.Component {
state = {
name: '路飛',
old: 12,
gender: 'boy'
}
// 情形 ①: class 中異步調用 setState
componentDidMount() {
this.setState({
name: '娜美'
})
this.setState({
old: 13
})
this.setState({
gender: 'girl'
})
}
// 情形 ②: class 中同步調用 setState
componentDidMount() {
setTimeout(() => {
this.setState({
name: '娜美'
})
this.setState({
old: 13
})
this.setState({
gender: 'girl'
})
})
}
render() {
console.log('render')
const { name, old, gender } = this.state
return (
<>{name}{old}{gender}</> ) } } 複製代碼
Hooks 中的 setState
export default function() {
const [name, setName] = useState('路飛')
const [old, setOld] = useState('12')
const [gender, setGender] = useState('boy')
// 情形③: Hooks 中異步調用 setState
useEffect(() => {
setName('娜美')
setOld('13')
setGender('girl')
}, [])
// 情形④: Hooks 中同步調用 setState
useEffect(() => {
setTimeout(() => {
setName('娜美')
setOld('13')
setGender('girl')
}, 0)
}, [])
console.log('render')
return (
<>{name}{old}{gender}</> ) } 複製代碼
情形①、情形②、情形③、情形④ 中 render 輸出的次數分別是 2, 4, 2, 4。能夠看出在 React 中使用 class 寫法和 hooks 寫法是一一對應的。此外 setState 的執行是異步仍是同步取決於其執行環境
。
在 React 16.8 版本以後, 針對不是特別複雜
的業務場景, 可使用 React 提供的 useContext
、useReducer
實現自定義簡化版的 redux, 可見 todoList 中的運用。核心代碼以下:
import React, { createContext, useContext, useReducer } from "react"
// 建立 StoreContext
const StoreContext = createContext()
// 構建 Provider 容器層
export const StoreProvider = ({reducer, initialState, children}) => {
return (
<StoreContext.Provider value={useReducer(reducer, initialState)}> {children} </StoreContext.Provider> ) } // 在子組件中調用 useStoreContext, 從而取得 Provider 中的 value export const useStoreContext = () => useContext(StoreContext) 複製代碼
可是針對特別複雜的場景目前不建議使用此模式, 由於 context 的機制會有性能問題。具體緣由可見 react-redux v7 回退到訂閱的緣由
React 官方在將來極可能會提供一個 usePrevious
的 hooks 來獲取以前的 props 以及 state。
usePrevious
的核心思想是用 ref 來存儲先前的值。
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
複製代碼
在 Hooks 中使用 useRef() 等價於在 Class 中使用 this.something。
/* in a function */
const X = useRef()
X.current // can read or write
/* in a Class */
this.X // can read or write
複製代碼
在 React 暗器百解 中提到了 getDerivedStateFromProps
是一種反模式, 可是極少數狀況仍是用獲得該鉤子, Hooks 沒有該 api, 那其如何達到 getDerivedStateFromProps 的效果呢?
function ScrollView({row}) {
const [isScrollingDown, setISScrollingDown] = setState(false)
const [prevRow, setPrevRow] = setState(null)
// 核心是建立一個 prevRow state 與父組件傳進來的 row 進行比較
if (row !== prevRow) {
setISScrollingDown(prevRow !== null && row > prevRow)
setPrevRow(row)
}
return `Scrolling down ${isScrollingDown}`
}
複製代碼
可使用 useReducer
來 hack forceUpdate
, 可是儘可能避免 forceUpdate 的使用。
const [ignored, forceUpdate] = useReduce(x => x + 1, 0)
function handleClick() {
forceUpdate()
}
複製代碼
在 Hooks 中可使用 useMemo
來做爲 shouldComponentUpdate
的替代方案, 但 useMemo
只對 props 進行淺比較。
React.useMemo((props) => {
// your component
})
複製代碼
useMemo(() => <component />) 等價於 useCallback(<component />) 複製代碼
一般來講依賴列表中移除函數是不安全的。觀察以下 demo
const { useState, useEffect } = React
function Example({ someProp }) {
function doSomething() {
console.log(someProp) // 這裏只輸出 1, 點擊按鈕的 2 並無輸出。
}
useEffect(
() => {
doSomething()
},
[] // 🔴 這是不安全的, 由於在 doSomething 函數中使用了 someProps 屬性
)
return <div>example</div>
}
export default function() {
const [value, setValue] = useState(1)
return (
<>
<Example someProp={value} />
<Button onClick={() => setValue(2)}>button</Button>
</>
)
}
複製代碼
在該 demo 中, 點擊 button 按鈕, 並無打印出 2。解決上述問題有兩種方法。
方法一: 將函數放入 useEffect
中, 同時將相關屬性放入依賴項中。由於在依賴中改變的相關屬性一目瞭然, 因此這也是首推的作法。
function Example({ someProp }) {
useEffect(
() => {
function doSomething() {
console.log(someProp)
}
doSomething()
},
[someProps] // 相關屬性改變一目瞭然
)
return <div>example</div>
}
複製代碼
方法二: 把函數加入依賴列表中
function Example({ someProp }) {
function doSomething() {
console.log(someProp)
}
useEffect(
() => {
doSomething()
},
[doSomething]
)
return <div>example</div>
}
複製代碼
方案二基本上不會單獨使用, 它通常結合 useCallback
一塊兒使用來處理某些函數計算量較大的函數。
function Example({ someProp }) {
const doSomething = useCallback(() => {
console.log(someProp)
}, [someProp])
useEffect(
doSomething(),
[doSomething]
)
return <div>example</div>
}
複製代碼
useState
的懶初始化, 用法以下const [value, setValue] = useState(() => createExpensiveObj)
複製代碼
function Image(props) {
const ref = useRef(null)
function getExpensiveObj() {
if (ref.current === null) {
ref.current = ExpensiveObj
}
return ref.current
}
// if need ExpensiveObj, call getExpensiveObj()
}
複製代碼