跟著案例一次搞定React Hooks

1. React Hooks 是什么

React Hooks 是 React V16.8 版本新增的特性,即在不編寫類組件的情況下使用 state 以及 React 的新特性。React 官網(wǎng)提供了 10 個 Hooks API,來滿足我們在函數(shù)組件中定義狀態(tài)墙牌,提供類似生命周期的功能和一些高級特性拓轻。

2. Hooks 的誕生背景

2.1. 類組件的不足

  • 狀態(tài)邏輯難以復(fù)用:
    在舊版本的 React 中,想要實(shí)現(xiàn)邏輯的復(fù)用,需要使用到HOC或者Render Props啡邑,增加了組件的使用層級产徊,同時學(xué)習(xí)使用成本也比較高嫩海。
  • 使用趨于復(fù)雜且維護(hù)成本較高
    有多個監(jiān)聽狀態(tài)的生命周期,同一個功能的整個過程可能要在不同的生命周期完成囚痴,不夠統(tǒng)一叁怪;尤其是引入 Redux 后,會變得復(fù)雜深滚,維護(hù)成本較高奕谭。
  • this 綁定問題
    在類組件中如果不使用箭頭函數(shù),需要顯示的綁定 this痴荐,容易造成 this 丟失血柳,導(dǎo)致數(shù)據(jù)混亂。

2.2. Hooks 的優(yōu)勢

  • 自定義 Hooks 可以實(shí)現(xiàn)公共的邏輯抽離生兆,便于復(fù)用
  • 可以將組件抽成更小的函數(shù)單元难捌,實(shí)現(xiàn)一個函數(shù)只關(guān)注一個功能,更加清晰
  • 更加豐富的性能優(yōu)化手段
  • 組件樹層級變淺鸦难,使用 HOC/Render Props 實(shí)現(xiàn)組件的狀態(tài)復(fù)用根吁,會增加組件的層級,但 Hooks 無需增加層級即可實(shí)現(xiàn)合蔽。

3. 10 個官方 Hooks 案例詳解

3.1. useState

import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import { Button,Modal } from 'antd'
/**
 * useState:定義組件的狀態(tài)
 * 作用:
 * 通過傳入 `useState` 參數(shù)后返回一個帶有默認(rèn)狀態(tài)和改變狀態(tài)函數(shù)的數(shù)組击敌。通過傳入新狀態(tài)給函數(shù)來改變原本的狀態(tài)值。
 */

// 類組件寫法
class Example extends React.Component {
  constructor() {
    super()
    this.state = { count: 0}
  }
  render() {
    return (
      <div>
        <div>你點(diǎn)擊了{(lán)this.state.count}次</div>
        <button onClick={() => this.setState({count: this.state.count +1})}>點(diǎn)擊</button>
      </div>
    )
  }
}
// hooks 寫法
function Example1() {
  // 定義一個count變量拴事,賦初始值0
  const [count,setCount] = useState(0)
  return (
    <div>
      <div>你點(diǎn)擊了{(lán)count}次</div>
      <button onClick={() => setCount(count +1 )}>點(diǎn)擊</button>
    </div>
  )
}
// setCount 接收函數(shù)作為參數(shù)
function Example2() {
  const [count,setCount] = useState(0)
  // preCount 參數(shù)為上一次的值
  const countAction = (preCount,a) =>  preCount + a
  return (
    <div>
      <div>你點(diǎn)擊了{(lán)count}次</div>
      <button onClick={() => setCount(countAction(count,1))}>點(diǎn)擊</button>
    </div>
  )
}
/**
 * 2 . renderProps 和 hooks 的比較沃斤。徹底理解 hooks 的價值和優(yōu)點(diǎn)。
 */
// renderProps 抽離公共邏輯
class Toggle extends React.Component {
  // 定義默認(rèn)屬性
  state= { on: false}
  constructor(props) {
    super(props)
    // 接收父組件傳遞的參數(shù)
    this.state.on = this.props.initial
  }
  toggle = () => {
    this.setState({ on: !this.state.on })
  }
  render() {
    // 向子組件傳遞了屬性和方法
    return this.props.children(this.state.on,this.toggle)
  }
}
function Example3() {
  return (
    <Toggle initial={false}>
      {/* 通過一個方法接收參數(shù) */}
      {
        (on,toggle) => (
          <React.Fragment>
            <Button type="primary" onClick={toggle}>打開彈框</Button>
            <Modal visible={on} onOk={toggle} onCancel={toggle}>我是彈框</Modal>
          </React.Fragment>
        )
      }
    </Toggle>
  )
}

// hooks 寫法 - 優(yōu)勢:多個狀態(tài)不會產(chǎn)生嵌套
function Example4 () {
  const [visible,setVisible] = useState(false)
  return (
    <div>
      <Button type='primary' onClick={() => setVisible(true)}>打開彈框</Button>
      <Modal visible={visible} onOk={() => setVisible(false)} onCancel={() => setVisible(false)}>我是彈框內(nèi)容</Modal>
    </div>
  )
}

const App = props => <div>
  <Example />
  <hr />
  <Example1 />
  <hr/>
  <Example2/>
  <hr />
  <Example3 />
  <hr />
  <Example4 />
</div>

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

3.2. useEffect

import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import { Button,Modal } from 'antd'
/**
 * useEffect: 處理副作用(副作用:指那些沒有發(fā)生在數(shù)據(jù)向視圖轉(zhuǎn)換過程中的邏輯刃宵,如 ajax 請求衡瓶、訪問原生dom 元素、本地持久化緩存牲证、綁定/解綁事件哮针、添加訂閱、設(shè)置定時器、記錄日志等诚撵。)
 * 作用:  函數(shù)組件能保存狀態(tài)缭裆,但是對于異步請求,副作用的操作還是無能為力寿烟,所以 React 提供了 useEffect 來幫助開發(fā)者處理函數(shù)組件的副作用澈驼,類似生命周期函數(shù),相當(dāng)于是 componentDidMount筛武,componentDidUpdate 和 componentWillUnmount 這三個函數(shù)的組合缝其,可以通過傳參及其他邏輯,分別模擬*這三個生命周期函數(shù)徘六。
 * useEffect具有以下5個特性:
 * 1. 第一次渲染時執(zhí)行内边,任何狀態(tài)發(fā)生變化都執(zhí)行 - 只指定一個回調(diào)函數(shù)作為參數(shù), 相當(dāng)于componentDidMount & componentDidUpdate
 * 2. 第一次渲染執(zhí)行待锈,任何狀態(tài)發(fā)生變化時不執(zhí)行
 * 3. 第一次渲染執(zhí)行漠其,通過第二個參數(shù)指定狀態(tài)發(fā)生變化時執(zhí)行,其他狀態(tài)發(fā)生變化不執(zhí)行
 * 4. 監(jiān)聽多個狀態(tài)時竿音,可以同時定義多個useEffect
 * 5. 組件卸載時會執(zhí)行回調(diào)函數(shù)返回的回調(diào)函數(shù) - 相當(dāng)于componentWillUnmount
 * 6. 未傳遞第二個參數(shù)和屎,所有狀態(tài)更新就執(zhí)行useEffect,或者指定狀態(tài)春瞬,對應(yīng)狀態(tài)更新執(zhí)行useEffect時柴信,會先執(zhí)行返回值回調(diào),再執(zhí)行第一個回調(diào)參數(shù)(第二個參數(shù)為空數(shù)組時任何狀態(tài)更新都不會執(zhí)行)
 *

/**
 * 1. useEffect只有一個回調(diào)函數(shù)作為第一個參數(shù)時:
 *   1.1.初始化時會執(zhí)行一次回調(diào)函數(shù)
 *   1.2.任一一個狀態(tài)數(shù)據(jù)發(fā)生變化時都會執(zhí)行回調(diào)函數(shù)
 */
function Example () {
  const [count,setCount] = useState(0)
  useEffect(() => {
    // 初始化時執(zhí)行一次宽气,count每次變化的時候都會執(zhí)行
    console.log('我執(zhí)行啦随常!')
  })
  return (
    <div>
       <div>點(diǎn)擊了{(lán)count}次</div>
      <Button type='primary' onClick={() => setCount(count+1)}>點(diǎn)擊</Button>
    </div>
  )
}

/**
 * 2. useEffect傳入兩個參數(shù):第一個參數(shù)是回調(diào)函數(shù),第二個參數(shù)是空數(shù)組:
 *    useEffect的回調(diào)函數(shù)只會在初始化渲染時執(zhí)行一次
 */
function Example1() {
  const [count,setCount] = useState(0)
  useEffect(() => {
    // 只會在初次渲染時執(zhí)行萄涯,任何狀態(tài)數(shù)據(jù)發(fā)生變化都不會執(zhí)行
    console.log('我執(zhí)行啦111111绪氛!')
  },[])
  return (
    <div>
      <div>你點(diǎn)擊了{(lán)count}次</div>
      <Button type='primary' onClick={() => setCount(count + 1)}>點(diǎn)擊</Button>
    </div>
  )
}

/**
 * 3. useEffect 傳入兩個參數(shù),第一個是回調(diào)函數(shù)窃判,第二個是指定數(shù)據(jù)的數(shù)組
 *   3.1 初次渲染時執(zhí)行一次回調(diào)函數(shù)
 *   3.2 指定數(shù)據(jù)發(fā)生變化時執(zhí)行一次回調(diào)函數(shù)
 */
function Example2() {
  const [visible,setVisible] = useState(false)
  const [count,setCount] = useState(0)
  useEffect(() => {
    // 初始渲染時會執(zhí)行一次钞楼,visible狀態(tài)發(fā)生變化時會執(zhí)行,count發(fā)生變化時則不會執(zhí)行
    console.log('我最帥了')
  },[visible])
  return (
    <div>
      <div>點(diǎn)擊了{(lán)count}次</div>
      <Button type='primary' onClick={() => setCount(count +1) }>點(diǎn)擊</Button>
      <Button type='primary' onClick={() => setVisible(true)}>打開彈框</Button>
      <Modal visible={visible} onOk={() => setVisible(false)} onCancel={() => setVisible(false)}>我是彈框內(nèi)容</Modal>
    </div>
  )
}

/**
 * 4. 監(jiān)聽多個狀態(tài)發(fā)生變化時執(zhí)行useEffect的回調(diào)函數(shù)時袄琳,可以同時使用多個useEffect
 */
 function Example3() {
  const [visible,setVisible] = useState(false)
  const [count,setCount] = useState(0)
  useEffect(() => {
    // 初始渲染的時候執(zhí)行一次,count狀態(tài)發(fā)生變化時會執(zhí)行
    console.log('我是count')
  },[count])
  useEffect(() => {
    // 初始渲染時會執(zhí)行一次燃乍,visible狀態(tài)發(fā)生變化時會執(zhí)行唆樊,count發(fā)生變化時則不會執(zhí)行
    console.log('我是彈框')
  },[visible])
  return (
    <div>
      <div>點(diǎn)擊了{(lán)count}次</div>
      <Button type='primary' onClick={() => setCount(count +1) }>點(diǎn)擊</Button>
      <Button type='primary' onClick={() => setVisible(true)}>打開彈框</Button>
      <Modal visible={visible} onOk={() => setVisible(false)} onCancel={() => setVisible(false)}>我是彈框內(nèi)容</Modal>
    </div>
  )
}
/**
 * 5. useEffect的回調(diào)函數(shù)的返回值(回調(diào)函數(shù))執(zhí)行時機(jī):
 *  ① 組件銷毀時
 *  ② 未傳遞第二個參數(shù),所有狀態(tài)更新就執(zhí)行useEffect刻蟹,或者指定狀態(tài)逗旁,對應(yīng)狀態(tài)更新時,會先執(zhí)行返回值回調(diào),再執(zhí)行第一個回調(diào)參數(shù)
 * ps: 如果指定第二個參數(shù)為空數(shù)組時狀態(tài)更新還是不會執(zhí)行的
 */
function Test() {
  const [ count, setCount ] = useState(0)
  useEffect(() => {
    console.log('Test組件渲染更新了')
    return () => {
      // 組件卸載時執(zhí)行
      // 狀態(tài)更新執(zhí)行第一個參數(shù)回調(diào)前會先執(zhí)行
      console.log('Test組件銷毀了')
    }
  },[count])
  return (
    <div>
      <Button type='primary' onClick={() => setCount(count + 1)}>點(diǎn)擊</Button>
      <div>測試子組件點(diǎn)擊了{(lán)count}次數(shù)</div>
    </div>
  )
}
function Example4 () {
  const [show,setShow] = useState(true)
  return (
    <div>
      <Button type='primary' onClick={() => setShow(!show)}>顯示/關(guān)閉</Button>
      {
        show ? <Test /> : null
      }
    </div>
  )
}
const App = props => {
  return (
    <div>
      <Example />
      <hr />
      <Example1 />
      <hr />
      <Example2 />
      <hr />
      <Example3 />
      <hr />
      <Example4 />
    </div>
  )
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

3.3. useContext

import React, { useContext, useState }  from "react";
import ReactDOM from 'react-dom';
/**
 * useContext:  減少組件層級
 * 是類組件的的context的的hooks版片效,主要用于在父組件中公共數(shù)據(jù)和邏輯的抽離红伦,方便子組件公用。
 */
// 1. 創(chuàng)建Context對象
const ThemeContext = React.createContext()
// 2. Provider 組件淀衣,發(fā)布數(shù)據(jù)昙读,向所有的子組件提供數(shù)據(jù)
const App = props => {
  const [theme,setTheme] = useState('green')
  return (
    // Provider 發(fā)送數(shù)據(jù),兩個屬性膨桥,用對象的形式
    <ThemeContext.Provider value={{theme,setTheme}}>
      <div>
        <Toolbar />
      </div>
    </ThemeContext.Provider>
  )
}
// 中間組件
const Toolbar = props => {
  return (
      <div>
          <ThemedButton />
      </div>
  )
}
// 3. 子孫組件使用useContext 接收收據(jù)
const ThemedButton = (props) => {
  // useContext接收頂層組件傳遞過來的context數(shù)據(jù)蛮浑, 傳遞過來是對象,就用對象結(jié)構(gòu)接收
  const { theme,setTheme }= useContext(ThemeContext)
  return (
    <div>
      {/* 可以直接使用接收到的數(shù)據(jù)和方法 */}
      <div style={{ 'color': theme }}>Theme: {theme}</div>
      <button onClick={() => setTheme('red')}>切換主題</button>
    </div>
  )
}
ReactDOM.render(
  <App />,
  document.getElementById('root')
);

3.4. useReducer

import React, { useReducer } from 'react';
import ReactDOM from 'react-dom';
/**
 * useReducer: 就是子當(dāng)以useState執(zhí)行了比較復(fù)雜的state更新
 * 以hook的方式定了新的全局狀態(tài)管理只嚣,可以用來替代redux(實(shí)際為同一個作者)
 */
// 接收派發(fā)的action沮稚,執(zhí)行對state進(jìn)行更改
function reducer(state,action) {
  // 傳入舊的state,返回新的state
  switch (action.type) {
    case 'reset':
      return { count: action.payload }
    case 'increment':
      return { count: state.count + 1}
    case 'decrement':
      return { count: state.count - 1}
    default:
      return state
  }
}
// 允許對初始state執(zhí)行二次變更
function init(initialCountState) {
  return { count : initialCountState.count + 1}
}
function Counter({initialCount}) {
  // state, dispatch 是useReducer返回的內(nèi)容
  const [state, dispatch] = useReducer(
    reducer,// 派發(fā)action 執(zhí)行state修改
    initialCount, // 傳遞給state的初始值
    init // 可選參數(shù)册舞,允許對初始state進(jìn)行二次變更
  )
  return (
    <React.Fragment>
      <div>Count: {state.count}</div>
      {/* 執(zhí)行dispatch派發(fā)變更state的action */}
      <button onClick={() => dispatch({ type: 'reset', payload: initialCount.count })}>重置</button>
      <button onClick={() => dispatch({type: 'increment'})}>增加</button>
      <button onClick={() => dispatch({type: 'decrement'})}>減少</button>
    </React.Fragment>
  )
}
const App = props => {
  const initialCountState = {count: 0}
  return (
    <div>
      <Counter initialCount={initialCountState}/>
    </div>
  )
}
ReactDOM.render(
  <App />,
  document.getElementById('root')
);

3.5. useCallback

 import React, { useCallback, useState,memo }  from "react";
 import  ReactDOM  from "react-dom";
 import { Modal } from 'antd'
 /**
  * useCallback:  記憶函數(shù)
  * 作用:性能優(yōu)化蕴掏,避免重復(fù)的創(chuàng)建引用和重復(fù)無意義的組件渲染,加大性能開銷调鲸,對于一些開銷昂貴的組件來說是很好的優(yōu)化手段囚似。
  * 特性:
  * 1. useCallback 會將第一個函數(shù)參數(shù)作為回調(diào)函數(shù)返回,使用useCallback優(yōu)化過的回調(diào)函數(shù)线得,會在組件初始化渲染時創(chuàng)建函數(shù)對象并生成引用饶唤,之后組件再次更新渲染時則不會再次創(chuàng)建新對象和引用(普通函數(shù)每次組件更新都會創(chuàng)新新的函數(shù)對象并生成引用)
  * 2. useCallback可以通過傳遞第二個參數(shù),控制對應(yīng)的狀態(tài)數(shù)據(jù)發(fā)生變化時才重新創(chuàng)建對象并生成新的引用贯钩,默認(rèn)值時空數(shù)組[]募狂,即不監(jiān)控狀態(tài)數(shù)據(jù)
  */
/**
 * 案例:
 */
 // 1. 未使用useCallback的組件函數(shù),每次數(shù)據(jù)更新時都會重復(fù)創(chuàng)建函數(shù)對象生成新的引用
 let fn = null
const Example1 = ({count,setCount}) => {
  // 組件內(nèi)普通函數(shù)
  const ordinaryCallback = () => {
    console.log('我是函數(shù)函數(shù)')
  }
  // 狀態(tài)發(fā)生變化角雷,組件渲染祸穷,一直都返回false => 表明每次渲染都會創(chuàng)建新的函數(shù)對象,產(chǎn)生新的引用
  console.log('是否是Example1的同一個回調(diào)函數(shù):',Object.is(fn,ordinaryCallback)) // false
  fn = ordinaryCallback
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>增加</button>
      <div>Count:{count}</div>
    </div>

  )
}
// 2. 使用useCallback優(yōu)化的組件函數(shù)勺三,組件重新渲染時不會重新創(chuàng)建函數(shù)對象
let fn1 = null
const Example2 = ({count,setCount}) => {
  // 使用useCallback 優(yōu)化后的組件函數(shù)雷滚,組件重復(fù)渲染時不會重復(fù)創(chuàng)新函數(shù)對象
  const memoizedCallback = useCallback(() => {
    console.log('我是組件函數(shù)')
  },[])
  // 狀態(tài)更新時,組件重新渲染吗坚,初次渲染返回false祈远,更新渲染一直返回true => 表示經(jīng)過useCallback優(yōu)化后的函數(shù),在組件更新渲染時不會重復(fù)創(chuàng)建函數(shù)對象商源,依舊保持第一次創(chuàng)建時的引用
  console.log('是否是Example2的同一個回調(diào)函數(shù):', Object.is(fn1,memoizedCallback)) // 初始渲染是false车份,之后一直是true
  fn1 = memoizedCallback
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>增加</button>
      <div>Count: {count}</div>
    </div>
  )
}

// 3. 通過useCallback的第二個參數(shù)控制指定狀態(tài)數(shù)據(jù)更新,組件重新渲染時牡彻,再創(chuàng)建新的函數(shù)對象 (感覺沒啥卵用)
let fn3 = null
function Example3({count,setCount}) {
  const [visible, setVisible] = useState(false)
  // 設(shè)定只有visible發(fā)生變化組件更新時才創(chuàng)新創(chuàng)建函數(shù)對象扫沼,其他情況下渲染不會重新創(chuàng)建
  const memoizedCallback = useCallback(() => {
    console.log('我是組件函數(shù)')
  },[visible])
 // count狀態(tài)發(fā)生變化時返回true => 表示不會重新創(chuàng)建函數(shù)
 // visible狀態(tài)發(fā)生變化時返回false => 表示會重新創(chuàng)建函數(shù)
  console.log('是否是Example3的同一個回調(diào)函數(shù):', Object.is(fn3,memoizedCallback))
  fn3 = memoizedCallback
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>增加數(shù)量</button>
      <div>Count: {count}</div>
      <button onClick={() => setVisible(!visible)}>顯示/隱藏彈框</button>
      <Modal visible={visible} onOk={() => setVisible(!visible)} onCancel={() => setVisible(!visible)}/>
    </div>
  )
}
// 4. 綜合案例
// 昂貴開銷的組件
const ExpensiveComponent = memo(({fn}) => {
  // 初始化時執(zhí)行,p1狀態(tài)更新時會執(zhí)行,p2狀態(tài)更新時則不會
  console.log('我被迫渲染啦6谐Q暇汀!')
  return <div onClick={fn}>我是一個渲染消耗昂貴的組件</div>
})
let fnn1 = null
const Child1 = () => {
  const fn1 = () => console.log('fn1')
  console.log('fnn1: ', Object.is(fnn1,fn1))
  fnn1 = fn1
  return <div>
    <ExpensiveComponent fn={fn1}/>
  </div>
}
let fnn2 = null
const Child2 = () => {
  // 使用useCallback 不會重復(fù)創(chuàng)建函數(shù)對象器罐,fn2不會重復(fù)創(chuàng)建增加ExpensiveComponent組件的重復(fù)渲染
  const fn2 = useCallback(() => console.log('fn2'),[])
  console.log('fnn2: ', Object.is(fnn2,fn2))
  fnn2 = fn2
  return <div>
    {/* p2狀態(tài)改變不會導(dǎo)致fn2引用變化梢为,因此該組件不會重復(fù)渲染 */}
    <ExpensiveComponent fn={fn2}/>
  </div>
}
const Example4 = () => {
  const [p1, setP1] = useState(0)
  const [p2, setP2] = useState(0)
  return (
    <div>
      <h4>每次點(diǎn)擊fn4都是新的</h4>
      <Child1 p1={p1}/>
      <button onClick={() => setP1(p1 + 1)}>按鈕1</button>
      <hr />
      <h4>每次點(diǎn)擊fn4不重新生成</h4>
      <Child2 p2={p2}/>
      <button onClick={() => setP2(p2 + 1)}>按鈕2</button>
    </div>
  )
}

const App = props => {
  const [count,setCount] = useState(0)
  return (
    <div>
      <Example1 count={count} setCount={setCount}/>
      <hr />
      <Example2 count={count} setCount={setCount}/>
      <hr />
      <Example3 count={count} setCount={setCount}/>
      <hr />
      <Example4 />
    </div>
  )
}
 ReactDOM.render(<App/>,document.getElementById('root'))

3.6. useMemo


import React, { useMemo, useState } from "react";
import ReactDOM from 'react-dom'
/**
 * useMemo  記憶組件
 * 作用:可以保存組件的渲染結(jié)果,根據(jù)條件確實(shí)是否重新渲染,主要是用來進(jìn)行性能優(yōu)化
 * 特性:
 * 1. 使用useMemo包括的jsx代碼段初次渲染后技矮,會將渲染結(jié)果保存抖誉,組件再次更新時不會重復(fù)渲染
 * 2. 指定狀態(tài)條件發(fā)生變化時,才會進(jìn)行重新渲染
 * 場景:
 * 在一些復(fù)雜計算的代碼段中衰倦,可能并不依賴很多組件狀態(tài)袒炉,如果任一狀態(tài)發(fā)生變化時都重新渲染,是很大的性能開銷樊零,此時就可以使用useMemo我磁,將這樣的代碼段包裹起來,
 * 只有依賴的狀態(tài)發(fā)生變化時才會重新渲染驻襟,可以進(jìn)行組件的性能提升夺艰。
 */
const Child = ({c}) => {
  console.log('Child重新渲染',c)
  return (
    <div>{c}</div>
  )
}

const Parent = ({a,b}) => {
  // a,b發(fā)生改變時都會重新渲染
  const child1 = <div>
    { console.log('這是一個復(fù)雜的計算child1')}
    <Child c={a}/>
  </div>
  // 初次渲染,之后只有b發(fā)生改變時才會重新渲染沉衣,否則保留上一次渲染的結(jié)果
  const child2 = useMemo(() =>
    <div>
      { console.log('這是一個復(fù)雜的計算child2')}
      <Child c=郁副/>
    </div>,
    [b]
  )
  return (
    <React.Fragment>
      { child1 }
      { child2 }
    </React.Fragment>
  )
}

const App = () => {
  const [a,setA] = useState(0)
  const [b,setB] = useState(0)
  return (
    <div>
      <Parent a={a} b=/>
      <button onClick={() => setA(a + 1)}>改變a</button>
      <button onClick={() => setB(b + 1)}>改變b</button>
    </div>
  )
}
ReactDOM.render(<App/>,document.getElementById('root'))

3.7. useRef

import React, { useEffect, useRef, useState } from "react";
import  ReactDOM  from "react-dom";
/**
 * useRef: 保存引用值
 * 兩個作用:
 * 1. 相當(dāng)于類組件的一個實(shí)例屬性豌习,只要組件實(shí)例不銷毀存谎,就一直保持著引用,組件更新時也不會重新初始化,返回一個包含current屬性的對象
 * 2. 獲取dom元素的一個引用
 *
 */
const Counter1 = () => {
  const [count,setCount] = useState(0)
  // 初始化后會一直保持著引用肥隆,狀態(tài)變化組件更新重新渲染時也不會被重新初始化既荚,返回一個具有current屬性的對象
  const countRef = useRef(0)
  console.log('countRef',countRef)
  useEffect(() => {
    // 組件初始化和狀態(tài)更新時執(zhí)行
    countRef.current = count
    console.log('組件渲染完成')
  })
  const prevCount = countRef.current
  return (
    <div>
      NowCount: {count}, beforeCount: {prevCount}
      { console.log('組件渲染中')}
      <button onClick={() => setCount(count + 1)}>更新count</button>
    </div>
  )
}
// 使用類組件實(shí)現(xiàn)相似功能: 使用useRef定義的變量,相當(dāng)于類組件的實(shí)例屬性
class Counter2 extends React.Component {
  state = { count: 0}
  prevCount = 0 // 相當(dāng)于useRef定義的變量栋艳,不會在更新渲染時重新初始化
  // 初始化渲染完成后執(zhí)行
  componentDidMount() {
    console.log('組件初始化渲染完畢Counter2')
  }
  // 數(shù)據(jù)狀態(tài)更新時執(zhí)行
  componentDidUpdate() {
    console.log('組件更新渲染完畢Counter2')
    this.prevCount = this.state.count
  }
  render() {
    return (
      <div>
      NowCount: {this.state.count}, beforeCount: {this.prevCount}
      { console.log('組件更新渲染中Counter2')}
      <button onClick={() => this.setState({ count:this.state.count+1})}>更新count</button>
    </div>
    )
  }
}
// 自定義屬性模擬類似功能
const countRef = { current: 0} // 唯一區(qū)別是因?yàn)槭褂玫娜肿兞壳∑福珻ounter3卸載時,該變量引用還在吸占,數(shù)據(jù)一直不會變晴叨,而使用useRef,組件卸載時引用會丟失
const Counter3 = () => {
  const [count,setCount] = useState(0)
  useEffect(() => {
    console.log('組件初始化渲染/更新渲染完成Counter3')
    countRef.current = count
  })
  const prevCount = countRef.current
  return (
    <div>
    NowCount: {count}, beforeCount: { prevCount }
    { console.log('組件渲染中Counter3')}
    <button onClick={() => setCount(count + 1)}>更新count</button>
  </div>
  )
}
// useRef第二個作用演示:獲取一個DOM元素的引用
const TextInputWithFocusButton = () => {
  // 配合ref屬性使用可以獲取input元素的引用旬昭,類似react中的基礎(chǔ)api:React.createRef(); 唯一區(qū)別是篙螟,createRef在每次組件更新時都重新創(chuàng)建一個新的變量,useRef則一直會保持初始化時創(chuàng)建的對象的引用
  const inputElement = useRef()
  const onButtonOnFocus = () => {
    console.log('inputElement',inputElement)
    inputElement.current.focus()
  }
  return (
    <React.Fragment>
      <input ref={inputElement} type='text'/>
      <button onClick={onButtonOnFocus}>Focus this input </button>
    </React.Fragment>
  )
}
const App = () => {
  const [show,setShow] = useState(true)
  return (
    <div>
      <h3>useRef第一個作用演示案例:</h3>
      <div>
        { show ? <Counter1 /> : null}
        <hr />
        { show ? <Counter2 /> : null}
        <hr />
        { show ? <Counter3 /> : null}
        <button onClick={() => setShow(!show)}>重新掛載</button>
      </div>
      <hr />
      <h3>useRef第二個作用演示案例:</h3>
      <div>
        <TextInputWithFocusButton />
      </div>
    </div>
  )
}

ReactDOM.render(<App/>,document.getElementById('root'))

3.8. useImperativehandle

  • 先理解 forwardRef
import React, { createRef, forwardRef, useCallback } from "react";
import ReactDOM from "react-dom";
/**
 * forwardRef:
 * 是React的一個高級特性问拘,理解useRef之前需要先理解forwardRef
 * 作用:forwardRef是一個高階組件,可以轉(zhuǎn)發(fā)收到的ref給其子組件,使其外部可以獲取對一個組件內(nèi)部子組件的引用
 */
// forwardRef是一個高階組件骤坐,它能將收到的ref轉(zhuǎn)發(fā)給它的子組件
const FancyButton = forwardRef((props,ref) => (
  <div>
    <input ref={ref}/>
    <button>
      { props.children }
    </button>
  </div>
))
const App = () => {
  // 創(chuàng)建一個ref引用
  const ref = createRef()
  /**
   * ref本身是綁定到FancyButton上的引用绪杏,ref.current正常獲取到的應(yīng)該是FancyButton,但是在FancyButton內(nèi)部通過forwardRef就將引用轉(zhuǎn)發(fā)給了input纽绍,此時就將ref轉(zhuǎn)發(fā)到了input上蕾久,所以ref.current獲取到的就是input元素,因此才可以調(diào)用input元素的focus方法拌夏。
   */
  const handleClick = useCallback(() => ref.current.focus(),[])
  return (
    <div>
      {/* 將ref綁定引用到FancyButton上 */}
      <FancyButton ref={ref}>點(diǎn)擊</FancyButton>
      <button onClick={handleClick}>獲得焦點(diǎn)</button>
    </div>
  )
}
ReactDOM.render(<App/>,document.getElementById('root'))
  • 搭配 forwardRef 和 useImperativeHandle 一起使用
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from "react";
import  ReactDOM from "react-dom";
/**
 * useImperativeHandle(): 透傳 Ref
 * 作用:使父組件具備了獲取子組件【實(shí)例】和【狀態(tài)數(shù)據(jù)】的能力僧著,還可以根據(jù)參數(shù)來定義傳遞的數(shù)據(jù)是否要隨著子組件對應(yīng)數(shù)據(jù)的更新而更新
 *      即:子組件通過useImperativeHandle自定義要傳遞給父組件的狀態(tài)或功能(想傳什么給父組件,就通過useImperativeHandle第二個參數(shù)返回值對象中定義即可)
 * 注意:需要配合forwardRef一起使用障簿,需要用到forwardRef轉(zhuǎn)發(fā)ref給子組件的能力盹愚,否則無法獲取到對應(yīng)的ref引用,數(shù)據(jù)就不知道傳給誰了站故。
 */
/**
 * 使用forwardRef皆怕,轉(zhuǎn)發(fā)FancyButton的ref引用到組件內(nèi)部
 */
const FancyButton = forwardRef((props,ref) => {
  const inputRef = useRef()
  const [inputValue,setInputValue] = useState(0)
  // 在useImperativeHandle中自定義要返回的屬性給ref引用
  useImperativeHandle(ref, () => ({
    // 傳遞功能
    focus: () => {
      inputRef.current.focus()
    },
    // 傳遞狀態(tài)數(shù)據(jù)
    inputValue,
    // 傳遞實(shí)例屬性
    inputRef
  }))
  return <input ref={inputRef} value={inputValue} onChange={(e) => setInputValue(e.target.value)}/>
})
const App = () => {
  // 創(chuàng)建一個ref引用
  const ref = useRef()
  // 獲取input的焦點(diǎn)
  const handleInputFocus = useCallback(() => {
    ref.current.focus()
  },[])
  // 獲取傳遞的狀態(tài)數(shù)據(jù)
  const handleGetChildState = useCallback(() => {
    // 因此ref被轉(zhuǎn)發(fā)到了FancyButton內(nèi)部,經(jīng)過useImperativeHandle就可以獲取到返回的相關(guān)屬性
    console.log('ref',ref.current)
    // {inputValue: '11111', inputRef: {…}, focus: ?}
  },[])
  return (
    <div>
      {/* 綁定ref引用到 FancyButton上 */}
      <FancyButton ref={ref}/>
      <button onClick={handleInputFocus}>獲取子組件input的焦點(diǎn)</button>
      <button onClick={handleGetChildState}>獲取子組件的狀態(tài)</button>
    </div>
  )
}
ReactDOM.render(<App />,document.getElementById('root'))

3.9. useLayoutEffect

import React, {useState,useLayoutEffect,useEffect} from "react";
import ReactDOM from "react-dom";
/**
 * useLayOutEffect(): 同步執(zhí)行副作用
 * 作用:
 *    大部分情況下西篓,使用 useEffect 就可以幫我們處理組件的副作用愈腾,但是如果想要同步調(diào)用一些副作用,比如對 DOM 的操作岂津,就需要使用 useLayoutEffect虱黄,useLayoutEffect 中的副作用會在 DOM 更新之后同步執(zhí)行。與useEffect類似吮成,只是執(zhí)行時間不一樣,與類組件的componentDidMount 和 componentDidUpdate生命周期執(zhí)行時機(jī)一致
 * 區(qū)別:
 *  1. useLayoutEffect總是比useEffect先執(zhí)行
 *  2. useEffect在全部渲染完畢后才會執(zhí)行(先渲染橱乱,后改變DOM),當(dāng)改變屏幕內(nèi)容時可能會產(chǎn)生閃爍
 *  3. useLayoutEffect是會在瀏覽器 layout之后赁豆,painting 之前執(zhí)行(會推遲頁面顯示的事件仅醇,先改變DOM后渲染),不會產(chǎn)生閃爍
 * 注意:
 *   為了用戶體驗(yàn)魔种,優(yōu)先使用useEffect析二。以避免阻塞視圖更新,但如果涉及到同步調(diào)用一些副作用节预,比如操作dom叶摄,可以放在useLayoutEffect中
 */
 function App() {
  const [width, setWidth] = useState(0);
  useLayoutEffect(() => {
    // 會在render,dom更新之后就執(zhí)行安拟,不會等到渲染完
    const title = document.querySelector("#title");
    const titleWidth = title.getBoundingClientRect().width;
    console.log("useLayoutEffect"); // 先打印
    if (width !== titleWidth) {
      setWidth(titleWidth);
    }
  });
  useEffect(() => {
    //dom渲染完畢后執(zhí)行
    console.log("useEffect");
  });
  return (
    <div>
      <h1 id="title">hello</h1>
      <h2>{width}</h2>
    </div>
  );
}
ReactDOM.render(<App />, document.getElementById('root'))

3.10. useDebugValue

import React, { useDebugValue, useState,useEffect } from "react";
import ReactDOM from "react-dom";
/**
 * useDebugValue():
 * 作用: 在自定義hooks中使用向開發(fā)者工具輸出一個調(diào)試值蛤吓,方便我們調(diào)試
 */

 function useFriendStatus() {
   const [isOnline, setIsOnline] = useState(true);
   useEffect(() => {
     const interval = setInterval(() => {
       setIsOnline(isOnline => !isOnline);
     }, 1000);
     return () => clearInterval(interval);
   }, []);
   // 在React Developer Tools中hooks一欄顯示:
   // e.g. "FriendStatus: Online"
   useDebugValue(isOnline ? "Online" : "Offline");
   return isOnline;
 }
function App() {
   const isOnline = useFriendStatus();
   useDebugValue(isOnline ? "Online" : "Offline");
   return <div className="App">用戶: {isOnline ? '在線' : '離線'}</div>;
 }
ReactDOM.render(<App/>,document.getElementById('root') )

4. 自定義 Hooks

  • React Hooks 中允許我們通過自定義 Hooks 實(shí)現(xiàn)公共邏輯的抽離,在不同組件之間復(fù)用糠赦。
  • 自定義 Hooks 中可以使用官方提供的 Hooks 特性定義狀態(tài)數(shù)據(jù)和實(shí)現(xiàn)邏輯会傲,將邏輯封裝起來锅棕,通過 return 的方式返回外部需要的狀態(tài)和方法,不同的組件調(diào)用同一個 hook 只是復(fù)用了組件的邏輯淌山,并不會共享狀態(tài)裸燎。
  • 自定義 Hooks 都以use開頭。
import React, {  useState } from 'react';
import ReactDOM from 'react-dom';
// 自定義hook
function useCount(){
  // 公共邏輯放在內(nèi)部實(shí)現(xiàn)
  let [count,setCount] = useState(0);
  const setMyCount = () => {
    setCount(count + 1)
  }
  // 只暴露外部需要的數(shù)據(jù)
  return [count,setMyCount];
}
// 在不同組件中使用不會共享同一份數(shù)據(jù)泼疑,都是獨(dú)立的一份
function Example1(){
  // setCount 為自定義hooks中返回的setMyCount
    let [count,setCount] = useCount();
    return (
        <div>
          Count: {count}
          <button onClick={()=>{setCount()}}>更新count</button>
        </div>
    )
}
function Example2(){
    let [count,setCount] = useCount();
    return (
        <div>
          Count: {count}
          <button  onClick={()=>{ setCount()}}>更新count</button>
        </div>
    )
}
ReactDOM.render(<><Example1 /><Example2 /></>, document.getElementById('root'));

5. Hooks 參考周邊

本文首發(fā)于微信公眾號'前端螺絲釘'

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末德绿,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子退渗,更是在濱河造成了極大的恐慌移稳,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件会油,死亡現(xiàn)場離奇詭異个粱,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)钞啸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進(jìn)店門几蜻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人体斩,你說我怎么就攤上這事梭稚。” “怎么了絮吵?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵弧烤,是天一觀的道長。 經(jīng)常有香客問我蹬敲,道長暇昂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任伴嗡,我火速辦了婚禮急波,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瘪校。我一直安慰自己澄暮,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布阱扬。 她就那樣靜靜地躺著泣懊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪麻惶。 梳的紋絲不亂的頭發(fā)上馍刮,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天,我揣著相機(jī)與錄音窃蹋,去河邊找鬼卡啰。 笑死静稻,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的碎乃。 我是一名探鬼主播姊扔,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼惠奸,長吁一口氣:“原來是場噩夢啊……” “哼梅誓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起佛南,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤梗掰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后嗅回,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體及穗,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年绵载,在試婚紗的時候發(fā)現(xiàn)自己被綠了埂陆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡娃豹,死狀恐怖焚虱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情懂版,我是刑警寧澤鹃栽,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站躯畴,受9級特大地震影響民鼓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蓬抄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一丰嘉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嚷缭,春花似錦饮亏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至优床,卻和暖如春劝赔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背胆敞。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工着帽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留杂伟,地道東北人。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓仍翰,卻偏偏與公主長得像赫粥,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子予借,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容