設計Hooks主要是解決ClassComponent的幾個問題:html
同時,也爲了讓 FunctionalComponent 也擁有 ClassComponent 的一些特性。node
使用注意:react
React 中提供的 hooks:web
const [state, setState] = useState(initialState)
複製代碼
setter
函數,可傳任意類型的變量,或者一個接收 state 舊值的函數,其返回值做爲 state 新值。
function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount) // Lazy initialization const [state, setState] = useState(() => { const initialState = someExpensiveComputation(props) return initialState }) return ( <> Count: {count} <button onClick={() => setCount(0)}>Reset</button> <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> </> ) } 複製代碼
注意: set 方法不會像類組件的 setState 同樣作 merge,因此建議:redux
{...state, value}
形勢。
const [state, dispatch] = useReducer(reducer, initialArg, init)
複製代碼
init
函數來計算初始狀態/值,而不是顯式的提供值。若是初始值可能會不同,這會很方便,最後會用計算的值來代替初始值。
注意: React 不使用 state = initialState
這一由 Redux 推廣開來的參數約定。有時候初始值依賴於 props,所以須要在調用 Hook 時指定。若是你特別喜歡上述的參數約定,能夠經過調用 useReducer(reducer, undefined, reducer)
來模擬 Redux 的行爲,但不鼓勵你這麼作。api
function init(initialCount) {
return {count: initialCount}; } function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; case 'reset': return init(action.payload); default: throw new Error(); } } function Counter({initialCount}) { const [state, dispatch] = useReducer(reducer, initialCount, init); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset </button> <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); } function render () { ReactDOM.render(<Counter initialCount={0} />, document.getElementById('root')); } 複製代碼
同時,useReucer 也是 useState 的內部實現,useState 和 useReucer 的實現原理:數組
let memoizedState
function useReducer(reducer, initialArg, init) { let initState = void 0 if (typeof init !== 'undefined') { initState = init(initialArg) } else { initState = initialArg } function dispatch(action) { memoizedState = reducer(memoizedState, action) // React的渲染 // render() } memoizedState = memoizedState || initState return [memoizedState, dispatch] } function useState(initState) { return useReducer((oldState, newState) => { if (typeof newState === 'function') { return newState(oldState) } return newState }, initState) } 複製代碼
在某些場景下,useReducer 比 useState 更加適用。Kent C. Dodds 提供了一個 useReducer
的最佳實踐:當你一個元素中的狀態,依賴另外一個元素中的狀態,最好使用 useReducer。瀏覽器
useEffect(effect, array);
複製代碼
useEffect 接收兩個參數,沒有返回值。緩存
[]
,componentDidUpdate 時不會觸發 returnFunction 和 effect。
useLayoutEffect(effect, array);
複製代碼
與 useEffect 使用方法同樣,只是執行回調函數的時機有着略微區別,運行時機更像是 componentDidMount 和 componentDidUpdate。可是要注意的是,該方法是同步方法,在瀏覽器 paint 以前執行,會阻礙瀏覽器 paint,只有當咱們須要進行DOM的操做時才使用該函數(好比設定 DOM 佈局尺寸,這樣能夠防抖動)。性能優化
useLayoutEffect 與 useEffect
正常狀況用默認的 useEffect 鉤子就夠了,這能夠保證狀態變動不阻塞渲染過程,但若是 effect 更新(清理)中涉及 DOM 更新操做,用 useEffect 就會有意想不到的效果,這時咱們最好使用 useLayoutEffect 。
好比逐幀動畫 requestAnimationFrame ,要作一個 useRaf hook 就得用上後者,須要保證同步變動。這也符合做者說到的 useEffect的時期是很是晚,能夠保證頁面是穩定下來再作事情。
鉤子的執行順序:useLayoutEffect > requestAnimationFrame > useEffect
要理解 Context Hooks 中的 api,首先須要瞭解 context 和其使用場景。
設計目的: context 設計目的是爲共享那些被認爲對於一個組件樹而言是「全局」的數據。
使用場景: context 經過組件樹提供了一個傳遞數據的方法,從而避免了在每個層級手動的傳遞 props 屬性。
注意點: 不要僅僅爲了不在幾個層級下的組件傳遞 props 而使用 context,它是被用於在多個層級的多個組件須要訪問相同數據的情景。
const {Provider, Consumer} = React.createContext(defaultValue, calculateChangedBits)
複製代碼
該方法建立一對{ Provider, Consumer }
。當 React 渲染 context 組件 Consumer 時,它將從組件樹的上層中最接近的匹配的 Provider 讀取當前的 context 值。Consumer 是 Provider 提供數據的使用者。
若是上層的組件樹沒有一個匹配的 Provider,而此時你須要渲染一個 Consumer 組件,那麼你能夠用到 defaultValue 。這有助於在不封裝它們的狀況下對組件進行測試。例如:
import React, { useContext} from 'react';
import ReactDOM from 'react-dom'; /* 結果讀取爲123,由於沒有找到Provider */ const { Provider, Consumer } = React.createContext(123); function Bar() { return <Consumer>{color => <div>{color}</div>}</Consumer>; } function Foo() { return <Bar />; } function App() { return ( <Foo /> ); } ReactDOM.render( <App />, document.getElementById('root') ) 複製代碼
React 組件容許 Consumers 訂閱 context 的改變
。而 Provider 就是發佈這種狀態的組件,該組件接收一個 value 屬性
傳遞給 Provider 的後代 Consumers。一個 Provider 能夠聯繫到多個 Consumers
。Providers 能夠被嵌套以覆蓋組件樹內更深層次的值。
export const ProviderComponent = props => {
return ( <Provider value={}> {props.children} </Provider> ) } 複製代碼
在createContext()
函數中的第二個參數爲calculateChangedBits
,它是一個接受 newValue 與 oldValue 的函數,返回值做爲 changedBits,在 Provider 中,當 changedBits = 0,將再也不觸發更新。而在 Consumer 中有一個不穩定的 props,unstable_observedBits,若 Provider 的changedBits & observedBits = 0
,也將不觸發更新。
const Context = React.createContext({foo: 0, bar: 0}, (a, b) => {
let result = 0 if (a.foo !== b.foo) { result |= 0b01 } if (a.bar !== b.bar) { result |= 0b10 } return result }) 複製代碼
<Consumer>
{value => /* render something based on the context value */} </Consumer> 複製代碼
createContext()
的 defaultValue 。
每當 Provider 的值發生改變時, 做爲 Provider 後代的全部 Consumers 都會從新渲染。 從 Provider 到其後代的Consumers 傳播不受 shouldComponentUpdate 方法的約束,所以即便祖先組件退出更新時,後代Consumer也會被更新。
// 建立一個 theme Context, 默認 theme 的值爲 light
const ThemeContext = React.createContext('light'); function ThemedButton(props) { // ThemedButton 組件從 context 接收 theme return ( <ThemeContext.Consumer> {theme => <Button {...props} theme={theme} />} </ThemeContext.Consumer> ) } // 中間組件 function Toolbar(props) { return ( <div> <ThemedButton /> </div> ) } class App extends React.Component { render() { return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ) } } 複製代碼
const context = useContext(Context)
複製代碼
使用效果和 Consumer 相似,可是是函數式的使用方式,仍然須要與 Provider 配合使用。
該函數接收一個 Context 類型的參數(就是包裹了 Provider 和 Consumer 的那個對象),返回 Provider 中的 value 屬性對象的值。
const Context = React.createContext('light');
// Provider class Provider extends Component { render() { return ( <Context.Provider value={'dark'}> <DeepTree /> </Context.Provider> ) } } 複製代碼
// Consumer
function Consumer(props) { const context = useContext(Context) return ( <div> {context} // dark </div> ) } 複製代碼
// Color.jsx
import React, { createContext, useReducer } from 'react' export const ColorContext = createContext() export const UPDATE_COLOR = 'UPDATE_COLOR' function reducer(state, action) { switch (action.type) { case UPDATE_COLOR: return action.color default: return state } } export const Color = props => { const [color, dispatch] = useReducer(reducer, 'blue') return ( <ColorContext.Provider value={{ color, dispatch }}> {props.children} </ColorContext.Provider> ) } 複製代碼
// Button.jsx
import React, { useContext } from 'react' import { ColorContext, UPDATE_COLOR } from './Color' function Buttons() { const { dispatch } = useContext(ColorContext) return ( <div> <button onClick={() => { dispatch({ type: UPDATE_COLOR, color: 'red' }) }} > red </button> <button onClick={() => { dispatch({ type: UPDATE_COLOR, color: 'yellow' }) }} > yellow </button> </div> ) } export default Buttons 複製代碼
// ShowArea.jsx
import React, { useContext } from 'react' import { ColorContext } from './Color' function ShowArea() { const { color } = useContext(ColorContext) return <div style={{ color }}>color:{color}</div> } export default ShowArea 複製代碼
// index.jsx
import React from 'react' import ShowArea from './ShowArea' import Buttons from './Buttons' import { Color } from './Color' function Demo() { return ( <div> <Color> <ShowArea /> <Buttons /> </Color> </div> ) } export default Demo 複製代碼
const RefElement = createRef(initialValue)
複製代碼
useRef 能夠須要傳遞一個參數,該參數通常是用於 useRef 的另外一種用法,若是是引用元素對象通常不傳參數,返回一個可變的 ref 對象,該對象下面有一個 current 屬性指向被引用對象的實例。
要說到 useRef,咱們須要說到 createRef ,以及爲何要有這個 api 出現。(createRef 使用方法和 useRef 一致,返回的是一個 ref 對象)
二者當作 ref 正常使用時效果基本徹底同樣:
createRef
import { React, createRef } from 'react'
const FocusInput = () => { const inputElement = createRef() const handleFocusInput = () => { inputElement.current.focus() } return ( <> <input type='text' ref={inputElement} /> <button onClick={handleFocusInput}>Focus Input</button> </> ) } export default FocusInput 複製代碼
useRef
import { React, useRef } from 'react'
const FocusInput = () => { const inputElement = useRef() const handleFocusInput = () => { inputElement.current.focus() } return ( <> <input type='text' ref={inputElement} /> <button onClick={handleFocusInput}>Focus Input</button> </> ) } export default FocusInput 複製代碼
可是,這二者對應 ref 的引用實際上是有着本質區別的:createRef 每次渲染都會返回一個新的引用,而 useRef 每次都會返回相同的引用。
像這樣:
const App = () => {
const [renderIndex, setRenderIndex] = React.useState(1) const refFromUseRef = React.useRef() const refFromCreateRef = createRef() if (!refFromUseRef.current) { refFromUseRef.current = renderIndex } if (!refFromCreateRef.current) { refFromCreateRef.current = renderIndex } return ( <> <p>Current render index: {renderIndex}</p> <p> <b>refFromUseRef</b> value: {refFromUseRef.current} </p> <p> <b>refFromCreateRef</b> value:{refFromCreateRef.current} </p> <button onClick={() => setRenderIndex(prev => prev + 1)}> Cause re-render </button> </> ) } 複製代碼
由於一直都存在 refFromUseRef.current,因此並不會改變值。
那麼,爲何要賦予 useRef 這種特性,在什麼場景下咱們須要這種特性呢?
一個經典案例:
import React, { useRef, useState } from 'react'
function App() { const [count, setCount] = useState() function handleAlertClick() { setTimeout(() => { alert(`Yout clicked on ${count}`) }, 3000) } return ( <div> <p>You click {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> <button onClick={handleAlertClick}>Show alert</button> </div> ) } export default App 複製代碼
當咱們更新狀態的時候, React 會從新渲染組件, 每一次渲染都會拿到獨立的 count 狀態, 並從新渲染一個 handleAlertClick 函數. 每個 handleAlertClick 裏面都有它本身的 count。
你會發現,count 的值並不可以實時的顯示更新的數據,這個是因爲 JS 中一值就存在的閉包機制致使的,當點擊顯示彈窗的按鈕時,此時的 count 的值已經肯定,而且傳入到了alert
方法的回調中,造成閉包,後續值的改變不會影響到定時器的觸發。
而若是在類組件中,若是咱們使用的是this.state.count
,獲得的結果又會是實時的,由於它們都是指向的同一個引用對象。
在函數組件中,咱們可使用 useRef 來實現實時獲得新的值,這就是 useRef 的另一種用法,它還至關於 this , 能夠存聽任何變量。useRef 能夠很好的解決閉包帶來的不方便性。
import React, { useRef, useState } from 'react'
function App() { const [count, setCount] = useState(0) const lastestCount = useRef() lastestCount.current = count function handleAlertClick() { setTimeout(() => { alert(`You clicked on ${lastestCount.current}`) // 實時的結果 }, 3000) } return ( <div> <p>Yout click {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> <button onClick={handleAlertClick}>Show alert</button> </div> ) } export default App 複製代碼
要值得注意的是,若是咱們在 useRef 中傳入參數(通常 useRef 中傳值就用在這裏),使用下面這種方法來訪問值,結果又會不一樣:
import React, { useRef, useState } from 'react'
function App() { const [count, setCount] = useState(0) const lastestCount = useRef(count) // 直接傳入count function handleAlertClick() { setTimeout(() => { alert(`You clicked on ${lastestCount.current}`) }, 3000) } return ( <div> <p>Yout click {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> <button onClick={handleAlertClick}>Show alert</button> </div> ) } export default App 複製代碼
點擊的時候咱們會發現彈出來的值永遠是0,正如咱們所說,useRef 返回的都是相同的引用,參數在第一個傳入進去的時候已經賦值給了 current 屬性,返回了一個實例回來,後續由於已經有了實例了,因此會直接將原來的實例返回,傳入的參數也就再也不起做用了。
forwardRef((props, ref) => {
// dosomething return ( <div ref={ref}></div> ) }) 複製代碼
forwardRef 準確來講不是 hooks 中的內容,可是若是咱們要使用 useImperativeHandle,就須要使用它來進行搭配。
該方法的做用是:引用父組件的 ref 實例,成爲子組件的一個參數,能夠引用父組件的 ref 綁定到子組件自身的節點上。
該方法能夠看作是一個高階組件,自己 props 只帶有 children 這個參數,它能將從父組件拿到的 ref 和 props 傳入給子組件,由子組件來調用父組件傳入的 ref。
傳入的組件會接收到兩個參數,一個是父組件傳遞的 props,另外一個就是 ref 的引用。
// 咱們可使用三層組件嵌套,把傳入forwardRef的函數當作傳值的中間層
function InputWithLabel(props) { // 這裏的myRef爲經過外部打入的父級ref節點 const { label, myRef } = props const [value, setValue] = useState("") const handleChange = e => { const value = e.target.value setValue(value) } return ( <div> <span>{label}:</span> <input type="text" ref={myRef} value={value} onChange={handleChange} /> </div> ) } // 這裏用forwardRef來承接獲得父級傳入的ref節點,並將其以參數的形式傳給子節點 const RefInput = React.forwardRef((props, ref) => ( <InputWithLabel {...props} myRef={ref} /> )) // 調用該RefInput的過程 function App() { // 經過useRef hook 得到相應的ref節點 const myRef = useRef(null) const handleFocus = () => { const node = myRef.current console.log(node) node.focus() } return ( <div className="App"> <RefInput label={"姓名"} ref={myRef} /> <button onClick={handleFocus}>focus</button> </div> ) } 複製代碼
useImperativeHandle(ref, () => ({
a:1, b:2, c:3 })) 複製代碼
官方建議useImperativeHandle和forwardRef同時使用,減小暴露給父組件的屬性,避免使用 ref 這樣的命令式代碼。
useImperativeHandle 有三個參數:
function Example(props, ref) {
const inputRef = useRef() useImperativeHandle(ref, () => ({ // 父組件能夠經過this.xxx.current.focus的方式使用子組件傳遞出去的focus方法 focus: () => { inputRef.current.focus() } })) return <input ref={inputRef} /> } export default forwardRef(Example) 複製代碼
class App extends Component {
constructor(props){ super(props) this.inputRef = createRef() } render() { return ( <> <Example ref={this.inputRef}/> <button onClick={() => {this.inputRef.current.focus()}}>Click</button> </> ) } } 複製代碼
MemoComponent = memo(Component)
複製代碼
咱們都知道,對於類組件來講,有 PureComponent 能夠經過判斷父組件傳入的 props 是否進行改變來優化渲染性能。因此,在函數式組件中,React 也有一個相似 PureComponent 功能的高階組件 memo,效果同 PureComponent,都會判斷父組件傳入的 props 是否發生改變來從新渲染當前組件。
使用方法很簡單:
import React, { memo } from 'react'
function Demo(props){ return ( <div>{props.name}</div> ) } export default memo(Demo) 複製代碼
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
複製代碼
useMemo 是 React 推出用於優化函數式組件性能的 hooks,它能夠傳入兩個參數:
import React, { useState, useMemo } from 'react'
function Child({ color }) { // color值不發生改變不會打印console,可是依舊會觸發從新渲染,若是連這個函數都不執行,在最外層加上memo const actionColor = useMemo(() => { console.log('color update') return color }, [color]) return <div style={{ actionColor }}>{actionColor}</div> } function MemoCount() { const [count, setCount] = useState(0) const [color, setColor] = useState('blue') return ( <div> <button onClick={() => { setCount(count + 1) }} > Update Count </button> <button onClick={() => { setColor('green') }} > Update Color </button> <div>{count}</div> <Child color={color} /> </div> ) } export default MemoCount 複製代碼
上面的例子其實並非 useMemo 最經常使用的場景,就像以前說的,在 props 發生改變的時候纔會觸發被 memo 的組件的從新渲染,可是若是隻是 props 的引用對象發生改變,實際的值並無發生改變,組件仍是會被從新渲染。就像下面這樣:
import React, { useState, memo } from 'react'
const Child = memo(({ config }) => { console.log(config) return <div style={{ color:config.color }}>{config.text}</div> }) function MemoCount() { const [count, setCount] = useState(0) const [color, setColor] = useState('blue') const config = { color, text:color } return ( <div> <button onClick={() => { setCount(count + 1) }} > Update Count </button> <button onClick={() => { setColor('green') }} > Update Color </button> <div>{count}</div> <Child config={config} /> </div> ) } export default MemoCount 複製代碼
當咱們改變 count 值的時候,咱們發現這其實和 config 對象是無關的,可是 Child 組件依舊會從新渲染,由於因爲父組件的從新渲染,config 被從新賦值了新的對象,雖然新的對象裏面的值都是相同的,但因爲是引用類型對象,因此依舊會改變值,要改變這種情況,咱們須要:
// 使用useMemo
import React, { useState,useMemo, memo } from 'react' const Child = memo(({ config }) => { console.log(config) return <div style={{ color:config.color }}>{config.text}</div> }) function MemoCount() { const [count, setCount] = useState(0) const [color, setColor] = useState('blue') // 只會根據color的改變來返回不一樣的對象,不然都會返回同一個引用對象 const config = useMemo(()=>({ color, text:color }),[color]) return ( <div> <button onClick={() => { setCount(count + 1) }} > Update Count </button> <button onClick={() => { setColor('green') }} > Update Color </button> <div>{count}</div> <Child config={config} /> </div> ) } export default MemoCount 複製代碼
這樣,當 count 的值發生改變時,子組件就不會再從新渲染了。
const memoizedCallback = useCallback(
() => { doSomething(a, b) }, [a, b], ) 複製代碼
useCallback 的用法和 useMemo 相似,是專門用來緩存函數的 hooks,也是接收兩個參數,同時,咱們第一個參數傳入額回調函數就是要緩存的函數。
注意:第二個參數目前只用於指定須要判斷是否變化的參數,並不會做爲形參傳入回調函數。建議回調函數中使用到的變量都應該在數組中列出。
要在回調函數中傳入參數,咱們最好使用高階函數的方法,useCallback 會幫咱們緩存這個高階函數,如上所示。
能夠看出,都是當依賴項方式改變時,才觸發回調函數。所以,咱們能夠認爲:useCallback(fn, inputs)
等同於 useMemo(() => fn, inputs)
// useCallback的實現原理
let memoizedState = null function useCallback(callback, inputs) { const nextInputs = inputs !== undefined && inputs !== null ? inputs : [callback] const prevState = memoizedState; if (prevState !== null) { const prevInputs = prevState[1] if (areHookInputsEqual(nextInputs, prevInputs)) { return prevState[0] } } memoizedState = [callback, nextInputs] return callback } // useMemo的實現原理 function useMemo(callback, inputs){ return useCallback(callbak(),inputs) } 複製代碼
更多狀況,useCallback通常用於在 React 中給事件綁定函數並須要傳入參數的時候:
// 下面的狀況能夠保證組件從新渲染獲得的方法都是同一個對象,避免在傳給onClick的時候每次都傳不一樣的函數引用
import React, { useState, useCallback } from 'react' function MemoCount() { const [count, setCount] = useState(0) memoSetCount = useCallback(()=>{ setCount(count + 1) },[]) return ( <div> <button onClick={memoSetCount} > Update Count </button> <div>{color}</div> </div> ) } export default MemoCount 複製代碼
useDebugValue(value)
// or useDebugValue(date, date => date.toDateString()); 複製代碼
useDebugValue 可用於在 React 開發者工具中顯示自定義 hook 的標籤。
useDebugValue 接收兩個參數,根據傳入參數數量的不一樣有不一樣的使用方式:
直接傳 debug 值
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null); // ... // 在開發者工具中的這個 Hook 旁邊顯示標籤 // e.g. "FriendStatus: Online" useDebugValue(isOnline ? 'Online' : 'Offline'); return isOnline; } 複製代碼
延遲格式化 debug 值
const date = new Date()
useDebugValue(date, date => date.toDateString()) 複製代碼
自定義 Hook 是一個函數,其名稱以use
開頭,函數內部能夠調用其餘的 Hook
// myhooks.js
// 下面自定義了一個獲取窗口長寬值的hooks import React, { useState, useEffect, useCallback } from 'react' function useWinSize() { const [size, setSize] = useState({ width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }) const onResize = useCallback(() => { setSize({ width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }) }, []) useEffect(() => { window.addEventListener('resize', onResize) return () => { window.removeEventListener('reisze', onResize) } }, [onResize]) return size } export const useWinSize 複製代碼
import { useWinSize } from './myhooks'
function MyHooksComponent() { const size = useWinSize() return ( <div> 頁面Size:{size.width}x{size.height} </div> ) } export default MyHooksComponent 複製代碼
本文使用 mdnice 排版