Hooks 各個擊破

React文檔
Hooks:useState蜓席、useEffect呀枢、useLayoutEffect缕减、useContext、useReducer捞奕、useMemo牺堰、React.memo、callCallback颅围、useRef伟葫、useImperativeHandle、自定義Hook院促、useDebugValue

useState(最常用)

在React的函數(shù)組件里筏养,默認(rèn)只有屬性,沒有狀態(tài)常拓。

1.使用狀態(tài)

//數(shù)組第1項(xiàng)是讀接口渐溶,第2項(xiàng)是寫接口,初始值0
const [n,setN] = React.useState(0) //數(shù)字
const [user,setUser] = React.useState({name:'F'}) //對象

2.注意事項(xiàng)(1):不可局部更新
更新部分屬性時弄抬,未更新的屬性會消失茎辐。
3.注意事項(xiàng)(2):地址要變
setState(obj)如果obj對象地址不變,那么React就認(rèn)為數(shù)據(jù)沒有變化,因此不會幫你改變內(nèi)容拖陆。
4.useState接受函數(shù)
5.setState接受函數(shù)

例1:不可局部更新
如果state是個對象弛槐,能否部分setState?
不行,因?yàn)閟etState不會幫我們合并屬性慕蔚。所以當(dāng)只更新部分屬性時,未更新的屬性就會消失斋配。

那怎么解決"未更新的屬性會消失"的問題孔飒?
...拷貝之前所有的屬性,然后再覆蓋屬性艰争。

import React, {useState} from "react";
import ReactDOM from "react-dom";

function App() {
  const [user,setUser] = useState({name:'Frank', age: 18})
  const onClick = ()=>{
    setUser({
      ...user, //拷貝user的所有屬性
      name: 'Jack' //覆蓋name
    })
  }
  return (
    <div className="App">
      <h1>{user.name}</h1>
      <h2>{user.age}</h2>
      <button onClick={onClick}>Click</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

題外話:useReducer也不會合并屬性坏瞄,React新版的所有東西都不會幫你合并,它認(rèn)為這是你自己要做的事甩卓。

例2.地址要變
我想把name改下:于是直接修改user.name然后setUser(user)
你會發(fā)現(xiàn)改不了鸠匀,因?yàn)槟愀牡氖峭粋€對象,地址是一樣的逾柿。
React不會看你里面的內(nèi)容它只看地址缀棍,你不改地址它就不幫你改內(nèi)容。

那怎么改地址机错?

const onClick=()=>{ 
  user.name="小李"
  setUser(user)   
}
const onClick=()=>{ //改地址
  setUser({ //新的對象
    ...user,
    name:"小李"
  })
}

例3.useState接受函數(shù)(很少用)
引用狀態(tài)爬范,可用函數(shù),但很少會這樣寫,多算一遍就多算唄弱匪。
useState寫成函數(shù)的好處是:減少多余的計算過程青瀑,因?yàn)镴S引擎不會立即執(zhí)行函數(shù)。

function App() {
  const [user,setUser]=useState({name:'Frank', age: 9+9})//引用狀態(tài)
                    //useState(()=>( {name:'Frank', age: 9+9} ))
  const onClick = ()=>{
    setUser({ ... }) //設(shè)置狀態(tài)
  }

例4.setState接受函數(shù)(推薦優(yōu)先使用函數(shù))
點(diǎn)擊button后你會發(fā)現(xiàn)n=1而不是2萧诫,因?yàn)楫?dāng)你setN(n+1)時斥难,n不會變。
不管你做多少次計算帘饶,只有最后一次有用哑诊。

解決方法: 改成函數(shù)

function App() {
  const [n, setN] = useState(0)
  const onClick = ()=>{
  //setN(n+1) 第1次計算
  //setN(n+1) 第2次計算,也是最后1次計算
    setN(n => n + 1) //形式化的操作
    setN(n => n + 1)
  }
  return (
    <div className="App">
      <h1>n: {n}</h1>
      <button onClick={onClick}>+2</button>
    </div>
  );
}

JS語法有問題:對象必須加()及刻。(JS的bug)
總結(jié):對state進(jìn)行多次操作時搭儒,優(yōu)先使用函數(shù)。

useReducer(最常用)

useReducer4步走:
1.創(chuàng)建初始值initicalState

const initical = { n:0 }

2.創(chuàng)建所有操作reducer(state,action)
reducer接受2個參數(shù):舊的狀態(tài)state操作的類型action(一般是類型)提茁,最后返回新的state淹禾。
怎么得到新的state?
看下動作的的類型是什么
規(guī)則和useState一樣,必須返回新的對象茴扁。(不能直接操作n)

const reducer=(state,action)=>{
  if(action.type==='add'){
    return { n:state.n+1 } //return新對象
  }else if(action.type==='mult'){
    return { n:state.n*2 }
  }else{
    console.log("unknown type")
  }
}

3.傳給useReducer,得到讀和寫API
(1)需要導(dǎo)入useReducer或者直接使用全稱React.useReducer
(2)useReducer接收2個參數(shù):所有操作reducer初始狀態(tài)initical
(3)你將得到讀API铃岔、寫API寫API一般叫dispatch,因?yàn)槟惚仨毻ㄟ^reducer才能setState,所以叫dispatch。

import React,{useReducer} from "react"

function App(){
  const [state,dispatch]=useReducer(reducer,initical)
}

拿出屬性n的2種方法: 1' {state.n} 2'const {n}=state然后{n}
4.調(diào)用 寫({type:'操作類型'})

const onClick=()=>{
  dispatch({
    type:'add' //調(diào)用reducer的add操作
  })
}

相當(dāng)于useState,只不過把所有操作聚攏在一個函數(shù)里,這樣的好處是:調(diào)用的代碼簡短了毁习。

調(diào)用傳參:+2時傳了參數(shù)number:2,那么reducer里的1就可以變成一個參數(shù)智嚷。因?yàn)閐ispatch()里傳的對象就是action。

if (action.type === "add") {
//return { n: state.n + 1 };
  return { n: state.n + action.number };
}
...
const onClick2 = () => {
//dispatch({type:'add'})
  dispatch({type:'add',number:2}) //里面的對象就是action
}

這就是useReducer對useState的升級操作,總的來說useReducer是useState的復(fù)雜版纺且。好處是用來踐行React社區(qū)一直推崇的flux/Redux思想盏道。隨著hooks的流行這個思想會退化。

完整代碼

import React, { useState, useReducer } from "react";
import ReactDOM from "react-dom";

const initial = { n: 0};
const reducer = (state, action) => {
  if (action.type === "add") {
    return { n: state.n + action.number };
  } else if (action.type === "multi") {
    return { n: state.n * 2 };
  } else {
    throw new Error("unknown type");
  }
};

function App() {
  const [state, dispatch] = useReducer(reducer, initial);
  const { n } = state;
  const onClick = () => {
    dispatch({ type: "add", number: 1 });
  };
  const onClick2 = () => {
    dispatch({ type: "add", number: 2 });
  };
  return (
    <div className="App">
      <h1>n: {n}</h1>
      <button onClick={onClick}>+1</button>
      <button onClick={onClick2}>+2</button>
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

如何選擇 使用useReducer還是useState?
事不過三原則
如果你發(fā)現(xiàn)有幾個變量應(yīng)該放一起(對象里)這時候就用useReducer對對象進(jìn)行整體的操作载碌。

useReducer的常用例子

const initFormData = {
  name: "",
  age: 18,
  nationality: "漢族"
};

function reducer(state, action) {
  switch (action.type) {
    case "patch": //更新
//把第1個對象的所有屬性和第2個對象的所有屬性全部放到第3個空對象里猜嘱,這就是更新
      return { ...state, ...action.formData }; 
    case "reset": //重置,返回最開始的對象
      return initFormData;
    default:
      throw new Error("你傳的啥 type 呀");
  }
}

function App() {
  const [formData, dispatch] = useReducer(reducer, initFormData);
  // const patch = (key, value)=>{
  //   dispatch({ type: "patch", formData: { [key]: value } })
  // }
  const onSubmit = () => {};
  const onReset = () => {
    dispatch({ type: "reset" });
  };
  return (
    <form onSubmit={onSubmit} onReset={onReset}>
      <div>
        <label>
          姓名
          <input value={formData.name} onChange={e => dispatch(
            {type:"patch", formData:{ name: e.target.value }})
            }
          />
        </label>
      </div>
      <div>
        <label>
          年齡
          <input value={formData.age} onChange={e =>dispatch(
            {type:"patch",formData: { age: e.target.value }})
            }
          />
        </label>
      </div>
      <div>
        <label>
          民族
          <input value={formData.nationality} 
            onChange={e => dispatch({type:"patch",
              formData:{nationality: e.target.value}})
            }
          />
        </label>
      </div>
      <div>
        <button type="submit">提交</button>
        <button type="reset">重置</button>
      </div>
      <hr />
      {JSON.stringify(formData)}
    </form>
  );
}

用戶一旦輸入就會觸發(fā)onChange事件嫁艇。用戶輸入即更新朗伶,因?yàn)閮?nèi)容不一樣了嘛。
每次更新步咪,App都會render遍论皆。
[圖片上傳失敗...(image-e51e4c-1651443540127)]

如何用useReducer代替Redux ?

前提:你得知道Redux是什么
用React的reducer+context即可代替Redux猾漫。

import React, { useReducer, useContext, useEffect } from "react";
import ReactDOM from "react-dom";

const store = { //第1步.將數(shù)據(jù)集中在一個store對象
  user: null,
  books: null,
  movies: null
};

function reducer(state, action) { //第2步.將所有操作集中在reducer
  switch (action.type) {
    case "setUser":
      return { ...state, user: action.user };
    case "setBooks":
      return { ...state, books: action.books };
    case "setMovies":
      return { ...state, movies: action.movies };
    default:
      throw new Error();
  }
}

const Context = React.createContext(null); //第3步.創(chuàng)建一個Context

function App() {
  const [state, dispatch] = useReducer(reducer, store); //第4步.創(chuàng)建對數(shù)據(jù)的讀寫API

  const api = { state, dispatch };
  return (
    <Context.Provider value={api}> //第5步.將創(chuàng)建的"數(shù)據(jù)的讀寫API"放到Context
      <User /> //第6步.用Context.Provider將Context提供給所有組件,就是將組件放里面
      <hr />
      <Books />
      <Movies />
    </Context.Provider>
  );
}

function User() {
  const { state, dispatch } = useContext(Context); //第7步.各個組件用useContext獲取讀寫API
  useEffect(() => {
    ajax("/user").then(user => {
      dispatch({ type: "setUser", user: user });
    });
  }, []);
  return (
    <div>
      <h1>個人信息</h1>
      <div>name: {state.user ? state.user.name : ""}</div>
    </div>
  );
}
function Books() {
  const { state, dispatch } = useContext(Context);//第7步.使用useContext獲取讀寫API
  useEffect(() => {
    ajax("/books").then(books => {
      dispatch({ type: "setBooks", books: books });
    });
  }, []);
  return (
    <div>
      <h1>我的書籍</h1>
      <ol>
        {state.books ? state.books.map(book =>
          <li key={book.id}>{book.name}</li>) : "加載中"}
      </ol>
    </div>
  );
}
function Movies() {
  const { state, dispatch } = useContext(Context);//使用useContext獲取讀寫API
  useEffect(() => {
    ajax("/movies").then(movies => {
      dispatch({ type: "setMovies", movies: movies });
    });
  }, []);
  return (
    <div>
      <h1>我的電影</h1>
      <ol>
        {state.movies ? state.movies.map(movie => 
          <li key={movie.id}>{movie.name}</li>)
          : "加載中"}
      </ol>
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

// 幫助函數(shù)
// 假 ajax
// 兩秒鐘后点晴,根據(jù) path 返回一個對象,必定成功不會失敗
function ajax(path) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (path === "/user") {
        resolve({
          id: 1,
          name: "Frank"
        });
      } else if (path === "/books") {
        resolve([
          {
            id: 1,
            name: "JavaScript 高級程序設(shè)計"
          },
          {
            id: 2,
            name: "JavaScript 精粹"
          }
        ]);
      } else if (path === "/movies") {
        resolve([
          {
            id: 1,
            name: "愛在黎明破曉前"
          },
          {
            id: 2,
            name: "戀戀筆記本"
          }
        ]);
      }
    }, 2000);
  });
}

解析
第1步.將數(shù)據(jù)集中在一個store對象

const store = { //加載信息
  user:null,
  books:null,
  movies:null
}

第2步.將所有操作集中在reducer
接收一個舊的狀態(tài)悯周,給我一個操作觉鼻,我就可以得到一個新的狀態(tài)。

怎么得到新的狀態(tài)呢队橙?
看你操作的類型是什么坠陈。
比如說你要填充user:你得給我一個user,所以你的action里面要有一個user。我把你給我的user傳到store上捐康。

const reducer = (state,action) => { 
  switch(action.type){
    case 'setUser': //填充user
      return {...state,user:action.user};
    case 'setBooks':
      return {...state,books:action.books};
    case 'setMovies':
      return {...state,movies:action.movies};
    default:
      throw new Error();
  }
}

第3步.創(chuàng)建一個Context
createContext需要自動引入或者直接React.createContext

const Context = React.createContext(null) //初始值一般是null仇矾,不傳會報錯

第4步.創(chuàng)建對數(shù)據(jù)的讀寫API
useReducer的第2個參數(shù)是初始值。
useReducer一般寫在函數(shù)里面,只能在函數(shù)里面運(yùn)行解总。

const Context = React.createContext(null) 
function App() {
  const [state,dispatch]=useReducer(reducer,store) //(reducer,初始值)
}
//也可以寫在外面贮匕,不過要在函數(shù)里調(diào)用。
//function x(){ const [state,dispatch]=useReducer(reducer,store)  }
//function App() {
//  x()
//}

第5步.將創(chuàng)建的"數(shù)據(jù)的讀寫API"放到Context
方法:把<div>刪了改為<Context.Provider>花枫,value就是把讀寫API[state,dispatch]賦值給Context.Provider刻盐。

語法:value={JS}告訴React里面是JS。{state:state,dispatch:dispatch}這個{}里才是對象,對象的state就是上面的state變量劳翰,對象的dispatch就是上面的dispatch變量敦锌。

const Context = React.createContext(null)
function App() {
  const [state,dispatch]=useReducer(reducer,store)
  return (
    <Context.Provider value = {{state:state,dispatch:dispatch}}>
      <User />
      <hr />
      <Books />
      <Movies />
    </Context.Provider>
  )

value={{state:state,dispatch:dispatch}}ES6可以直接縮寫成value={{state,dispatch}}

第6步.用Context.Provider將Context提供給所有組件
就是將組件<User />、<Books />佳簸、<Movies />放到<Context.Provider>里面

return (
    <Context.Provider value = {{state:state,dispatch:dispatch}}>
      <User />
      <hr />
      <Books />
      <Movies />
    </Context.Provider>
  )

第7步.各個組件用useContext獲取讀寫API

現(xiàn)在各個組件就可以使用讀寫API了

useContext接收的值就是你創(chuàng)建的Context

import React, { useReducer, useContext, useEffect } from "react";

function User(){
  const {state,dispatch} = useContext(Context) //注意這里是{}
  ajax("/user").then((user)=>{ //初始化user:調(diào)用ajax()
  //dispatch觸發(fā)"setUser",user的值就是得到的user,形參占位
    dispatch({type:"setUser",user:user}) 
  })
  return (
    <div>
      <h1>個人信息</h1>
        //展示
        <div>name:{state.user ? state.user.name : ""}</div>
    </div>
  )
}

由誰來設(shè)置一開始的值呢乙墙?
一開始是null,所以name是空的。
用假的ajax獲取用戶信息,很簡單的promise。
// 幫助函數(shù),假的ajax
// 2s后,根據(jù) path 返回一個對象听想,必定成功不會失敗
function ajax(path) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (path === "/user") {
        resolve({
          id: 1,
          name: "Frank"
        });
      } else if (path === "/books") {
        resolve([
          {
            id: 1,
            name: "JavaScript 高級程序設(shè)計"
          },
          {
            id: 2,
            name: "JavaScript 精粹"
          }
        ]);
      } else if (path === "/movies") {
        resolve([
          {
            id: 1,
            name: "愛在黎明破曉前"
          },
          {
            id: 2,
            name: "戀戀筆記本"
          }
        ]);
      }
    }, 2000);
  });
}

知識點(diǎn)
1.useEffect設(shè)置只在第一次渲染時執(zhí)行某函數(shù)
每次User刷新時,代碼setStatedispatch就會再執(zhí)行一遍并重復(fù)請求ajax腥刹。
怎樣減少請求ajax,設(shè)置只在第一次進(jìn)入頁面時請求?

借助useEffect
需要自動引入或者直接React.useEffect
useEffect需要傳個函數(shù),當(dāng)?shù)?個參數(shù)是空數(shù)組時汉买,那么前面的函數(shù)就只會在第一次渲染時執(zhí)行衔峰,之后永遠(yuǎn)不會執(zhí)行。例子:

React.useEffect(()=>{},[])

項(xiàng)目代碼

import React, { useReducer, useContext, useEffect } from "react";

function User() {
  const { state , dispatch } = react.useContext(Context)
  useEffect(()=>{
    ajax("/user").then((user)=>{
      dispatch({type:"setUser",user:user})
    })
  },[])
}

請求user數(shù)據(jù)ajax("/user")蛙粘,得到user數(shù)據(jù)后(這里的user是形參)垫卤,用setUser把數(shù)據(jù)user:user放到上下文Context里面。然后它自己就會刷新了组题,不用手動調(diào)自己刷新葫男,因?yàn)镽eact知道state變了就要變了抱冷。

2.加載中怎么做的崔列?
如果movies存在就展示n個<li>,如果不存在就展示"加載中"

function Movies() {
  const { state, dispatch } = useContext(Context);//使用useContext獲取讀寫API
  useEffect(() => {
    ajax("/movies").then(movies => {
      dispatch({ type: "setMovies", movies: movies });
    });
  }, []);
  return (
    <div>
      <h1>我的電影</h1>
      <ol>
        {state.movies ? state.movies.map(movie => 
          <li key={movie.id}>{movie.name}</li>)
          : "加載中"}
      </ol>
    </div>
  );
}

總結(jié)

用useReducer代替Redux,是如何實(shí)現(xiàn)代替的旺遮?
1.redux有個store赵讯,我們對象代替了const store={}
2.redux有個reducer,我們用函數(shù)代替了function reducer(state,action){}
3.redux它可以在任意地方使用耿眉,我們用Context代替了const Context=React.createContext(null)
非常好的代替redux的方法边翼。

如何模塊化?

模塊化不屬于React內(nèi)容,屬于基礎(chǔ)知識鸣剪。
模塊就是文件组底,文件就是模塊,文件名小寫筐骇,組件名大寫债鸡。

步驟
我們有3個組件,把這3個組件分別放到不同的組件

第1步.新建目錄components
第2步.新建組件文件
(1)有幾個組件就建幾個文件:分別新建文件user.js铛纬、books.js厌均、movies.js
然后把各個部分相關(guān)的代碼分別剪切進(jìn)去,并導(dǎo)出告唆。
第3步.對于共用的函數(shù)棺弊,也要新建文件,單獨(dú)拎出來擒悬。
(1)Context是組件共用的模她,所以要新建文件Context.js,把相關(guān)代碼剪切出來,并導(dǎo)出懂牧。

同樣公共的ajax也是如此
出了組件放components里缝驳,其它都放外面(src)
新建文件ajax.js,把相關(guān)代碼剪切出來,并導(dǎo)出用狱。

(2)使用Context运怖、ajax

要想使用Context、ajax,那每個組件都需要import
import Context from '../Context.js' //導(dǎo)入Context`
import ajax from '../ajax' //導(dǎo)入ajax

第4步.使用模塊和公共的函數(shù)
index.js
[圖片上傳失敗...(image-157144-1651443540127)]

細(xì)化reducer

假設(shè)我的組件有很多夏伊,那reducer的switch的case豈不是要寫累死了摇展?

第一部分.先重構(gòu)代碼

變成對象之后就好弄了,因?yàn)閷ο蠛芎煤喜?函數(shù)難合并(基礎(chǔ)知識)溺忧。

function reducer(state, action) {
  switch (action.type) {
    case "setUser":
      return { ...state, user: action.user };
    case "setBooks":
      return { ...state, books: action.books };
    case "setMovies":
      return { ...state, movies: action.movies };
    default:
      throw new Error();
  }
}

重構(gòu)后

const obj = {
  setUser:(state, action)=>{
    return { ...state, user: action.user };
  },
//removeUser:()=>{},
  setBooks:(state, action)=>{
      return { ...state, books: action.books };
  },
//deleteBook:()=>{},
  setMovies:(state, action)=>{
     return { ...state, movies: action.movies };
  },
//deleteMovie:()=>{}
}

//使用obj
function reducer(state, action) {
  const fn = obj[action.type] //判空
  if(fn){
    fn(state,action)
  }else{
    throw new Error('你傳的什么鬼 type')
  }
}

分開后就好弄了咏连,setUser是user模塊的reducer、setBooks是books模塊的reducer、setMovies是movies模塊的reducer。

假如還有其他的窃判,比如除了setUser可能還有removeUser,除了setBooks可能還有deleteBook,除了setMovies可能還有deleteMovie...
那怎么對這6個函數(shù)分成3個模塊呢菠剩?

第二部分.細(xì)化reducer(模塊化)
1.新建目錄reducers
2.新建子文件
(1)新建user_reducer.js、books_reducer.js俯逾、movies_reducer.js
(2)然后將代碼剪切放到export default{ ... }

3.使用

import userReducer from './reducers/user_reducer'
import booksReducer from './reducers/books_reducer'
import moviesReducer from './reducers/movies_reducer'

const obj = {
  ...userReducer, //把userReducer里的2個函數(shù)地址拷過來
  ...booksReducer,
  ...moviesReducer
}

useContext(常用)

概念
上下文就是你運(yùn)行一個程序所需要知道的所有其它變量(全局變量)。
全局變量是全局的上下文,所有變量都可以訪問它草慧。
上下文是局部的全局變量,context只在<C.Provider>內(nèi)有用匙头,出了這個范圍的組件是用不到這個contextde漫谷。

使用方法:
一.使用C = createContext(initical)創(chuàng)建上下文
二.使用<C.provider value={}>初始化并圈定作用域
三.在作用域內(nèi)的組件里使用useContext(C)來獲取上下文

import React, { createContext } from "react";
const C = createContext(null)
 
<C.Provider value={}>
  ...
</C.Provider>

value的初始值可以是任何值,一般我們會給一個讀寫接口.
<C.Provider>內(nèi)的所有組件都可以用上下文C

import React, { createContext, useState, useContext } from "react";
import ReactDOM from "react-dom";

const C = createContext(null); 
function App() {
  console.log("App 執(zhí)行了");
  const [n, setN] = useState(0); 
  return (
    <C.Provider value={{ n, setN }}> 
      <div className="App">
        <Baba />
      </div>
    </C.Provider>
  );
}

function Baba() {
  const { n, setN } = useContext(C); //使用context
  return (
    <div>
      我是爸爸 n: {n} <Child />
    </div>
  );
}
function Child() {
  const { n, setN } = useContext(C); //使用context
  const onClick = () => {
    setN(i => i + 1);
  };
  return (
    <div>
      我是兒子 我得到的 n: {n}
      <button onClick={onClick}>+1</button>
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

+1操作的不是本身的state蹂析,而是從App那里得到的讀舔示、寫接口。
App也可以不用state电抚,用reducer:const [n, setN] = useState(0);惕稻,context不管你用啥,它只是告訴你n喻频、setN可以共享給你的子代的任何組件的缩宜,范圍就是由<C.Provider>圈定的。

useContext注意事項(xiàng)
不是響應(yīng)式的
你在一個模塊將C里面的值改變甥温,另一個模塊不會感知到這個變化锻煌。
更新的機(jī)制并不是響應(yīng)式的,而是重新渲染的過程姻蚓。
比如宋梧,當(dāng)我們點(diǎn)擊+1時:setN去通知useState,useState重新渲染App,發(fā)現(xiàn)n變了,于是問里面的組件<Baba />有沒有用到n?沒有狰挡,就繼續(xù)問<Child />有沒有用到n?用到了捂龄,這時候兒子就知道要刷新了释涛,是一個從上而下逐級通知的過程,并不是響應(yīng)式的過程倦沧。

Vue3是你改n時唇撬,它就知道n變了,于是它就找誰用到了n,它就把誰直接改變了展融。它不會從上而下整體過一遍窖认,沒有這么復(fù)雜,因?yàn)樗且粋€響應(yīng)式的過程告希。
總結(jié): useContext的更新機(jī)制式是自頂向下扑浸,逐級更新數(shù)據(jù)。
而不是監(jiān)聽這個數(shù)據(jù)變化燕偶,直接通知對應(yīng)的組件喝噪。

useEffect & useLayoutEffect

useEffect副作用

對環(huán)境的改變即為副作用,如修改document.title
但我們不一定非要把副作用放在useEffect里
useEffect API名字叫的不好指么,建議理解成afterRender酝惧,每次render后就會調(diào)用的一個函數(shù)。

用途: 它可以代替之前的3種鉤子:出生涧尿、更新系奉、死亡
1.作為componentDidMount使用檬贰,[]作第2個參數(shù)
2.作為componentDidUpdate使用姑廉,可指定依賴
3.作為componentWillUnmount使用,通過return
以上三種用途可同時存在
特點(diǎn)
如果同時存在多個useEffect,會按從上倒下的順序執(zhí)行翁涤。

如何使用

import React, { useState,useEffect } from "react";
import ReactDOM from "react-dom";

function App() {
  const [n, setN] = useState(0);
  const onclick=()=>{
    setN(i => i+1)
  }

  useEffect(()=>{
    console.log("第一次渲染后執(zhí)行這句話")
  },[])
  useEffect(()=>{
    console.log("每次都會執(zhí)行這句話桥言,update")
  })
  useEffect(()=>{
    console.log("只有當(dāng)n變了才會執(zhí)行這句話")//監(jiān)聽某個值變化時執(zhí)行,包含第一次
  },[n])
  useEffect(()=>{
    if(n !== 0){
      console.log("n變化時會執(zhí)行這句話葵礼,剔除第一次")//默認(rèn)包含第1次号阿,要想排除第1次可以判斷下
    }
  },[n])
  //第一次進(jìn)來時使實(shí)現(xiàn)setInterval,每秒打印一個hi
  //當(dāng)組件消失時鸳粉,把定時器關(guān)掉扔涧,不然會一直打印hi
  //告訴React return一個函數(shù):當(dāng)組件掛掉時要執(zhí)行的代碼
  afterRender(()=>{
    const id=setInterval(() => {
      console.log("hi")
    }, 1000);
    return ()=>{
      window.clearInterval(id)
    }
  })

  return (
      <div>
        n:{n}
        <button onClick={onclick}>+1</button>
      </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

如果你只是改變自己的狀態(tài)就不是副作用,如果改變環(huán)境或者全局變量就是副作用届谈。

注意:
1.當(dāng)?shù)?個參數(shù)是[]時枯夜,表示只會在第一次渲染后執(zhí)行前面的函數(shù)。
2.當(dāng)不寫第2個參數(shù)時艰山,表示每次update都會執(zhí)行前面的函數(shù)湖雹。
3.當(dāng)?shù)?個參數(shù)是[n]時,表示只會在某個值變化(n)時才會去執(zhí)行前面的函數(shù)曙搬,包含第一次摔吏。
要想剔除第一次可以鸽嫂,可以加個判斷。
4.加return死亡時執(zhí)行
如果我這個組件要掛了征讲,我這個組件正要離開頁面据某,一般在使用router時會經(jīng)常去用。
比如诗箍,一開始是第1個頁面哗脖,點(diǎn)了按鈕后會跳到第2個頁面,那么第1個頁面的所有組件都掛掉了扳还。
掛掉的時候你可能需要做一些清理動作才避。用return,return一個函數(shù):函數(shù)里面是當(dāng)組件掛掉時要執(zhí)行的代碼氨距。
這樣就不會造成內(nèi)存泄露或者是不必要的代碼桑逝。

useLayoutEffect

例子:一開始是value:0,然后迅速變成value:1000

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

const BlinkyRender = () => {
  const [value, setValue] = useState(0);

  useEffect(() => {
    document.querySelector('#x').innerText = `value: 1000`
  }, [value]);

  return (
    <div id="x" onClick={() => setValue(0)}>value: {value}</div>
  );
};

ReactDOM.render(
  <BlinkyRender />,
  document.querySelector("#root")
);

[圖片上傳失敗...(image-d484e4-1651443540127)]

useEffect在瀏覽器渲染完成后執(zhí)行: 一開始是value是0,然后迅速變成1000,中間閃爍了下,有閃爍過程俏让。

如果我們改變useEffect的執(zhí)行順序,在瀏覽器渲染前執(zhí)行,會有什么效果楞遏?
沒有閃爍過程
代碼

import React, {useState, useRef, useLayoutEffect, useEffect} from "react";
import ReactDOM from "react-dom";

function App() {
  const [n, setN] = useState(0)
  const time = useRef(null)
  const onClick = ()=>{
    setN(i=>i+1) 
    time.current = performance.now()
  }
  useLayoutEffect(()=>{ // 改成 useEffect 試試
    if(time.current){
      console.log(performance.now() - time.current)
    }
  })
  return (
    <div className="App">
      <h1>n: {n}</h1>
      <button onClick={onClick}>Click</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

useLayoutEffect總是比useEffect先執(zhí)行。用useEffect有閃爍首昔,用useLayoutEffect沒有閃爍寡喝。

那是不是應(yīng)該多用useLayoutEffect?
不是勒奇,因?yàn)榇蟛糠謺r候不會去改變DOM预鬓,不用截胡。
因?yàn)橛脩粝肟吹木褪峭庥^赊颠,本來只需要1ms的格二,現(xiàn)在加了幾句話變成3ms了,影響用戶體驗(yàn)竣蹦。
所以從經(jīng)驗(yàn)上來說顶猜,我們更希望將useEffect放到瀏覽器改變外觀之后,所以優(yōu)先使用useEffect痘括。

useEffect和useLayoutEffect的本質(zhì)區(qū)別:
useEffect在瀏覽器渲染完成后執(zhí)行,useLayoutEffect在瀏覽器渲染完成前執(zhí)行长窄。

總結(jié):
優(yōu)先使用useEffect,除非不能滿足你的需求再使用useLayoutEffect纲菌。
雖然useLayoutEffect的性能更好挠日,優(yōu)先級更高,但是會影響用戶看到畫面變換的時間驰后,得不償失肆资。

代碼佐證時間差別:從setN到副作用開始執(zhí)行,中間有多久灶芝?
結(jié)果: useLayoutEffect是0.3ms,useEffect是0.8ms郑原,相差0.5ms唉韭。
如果你改變的外觀越多,時間就越多犯犁,呈線性的属愤。

import React, {useState, useRef, useLayoutEffect, useEffect} from "react";
import ReactDOM from "react-dom";

function App() {
  const [n, setN] = useState(0)
  const time = useRef(null)
  const onClick = ()=>{
    setN(i=>i+1) //打點(diǎn)一:setN后馬上打點(diǎn)
    time.current = performance.now() //beforeRender
  }
  useLayoutEffect(()=>{ // 改成 useEffect 試試
  //afterRender
    if(time.current){
      console.log(performance.now() - time.current)
    }
  })
  return (
    <div className="App">
      <h1>n: {n}</h1>
      <button onClick={onClick}>Click</button>
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

知識點(diǎn):performance.now()是全局對象,用來打印當(dāng)前的時間

特點(diǎn)
1.useLayoutEffect總是比useEffect先執(zhí)行酸役。
下面的代碼打印2和3住诸,再打印1。

 useEffect(()=>{ 
   if(time.current){ console.log("1") },[])
 }
 useLayoutEffect(()=>{ 
   if(time.current){ console.log("2") },[])
 }
 useLayoutEffect(()=>{ 
   if(time.current){ console.log("3") },[])
 }  

2.useLayoutEffect里的任務(wù)最好影響了Layout
如果沒有改變屏幕外觀Layout涣澡,就沒必要放瀏覽器渲染前贱呐,占時間。
經(jīng)驗(yàn): 為了用戶體驗(yàn)入桂,優(yōu)先使用useEffect(優(yōu)先渲染)

useMemo & useCallback

useMemo(最常用)

要理解React.useMemo需要先了解React.memo奄薇。
useCallback是useMemo的語法糖。

React.memo

import React from "react";
import ReactDOM from "react-dom";

function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };

  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <Child data={m}/>
   // <Child2 data={m}/> 優(yōu)化版
    </div>
  );
* [ ] }

function Child(props) {
  console.log("child 執(zhí)行了");
  console.log('假設(shè)這里有大量代碼')
  return <div>child: {props.data}</div>;
}
const Child2 = React.memo(Child);//接收Child組件

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

[圖片上傳失敗...(image-5c0181-1651443540127)]

點(diǎn)擊button時n會變抗愁,那child會再次執(zhí)行嗎馁蒂?
child會再次執(zhí)行。child只依賴m,初始值為0蜘腌,既然參數(shù)不變?yōu)槭裁催€會再執(zhí)行呢沫屡,不應(yīng)該執(zhí)行的。
使用React.memo把child封裝下,Child2是Child的優(yōu)化版,它會只在它的props變化時渲染撮珠,代碼<Child2 data={m}/>

現(xiàn)在點(diǎn)擊button后沮脖,2個log就再也不會執(zhí)行了。除了第一次渲染時會執(zhí)行console劫瞳,之后再也不會執(zhí)行倘潜。除非當(dāng)m第一次渲染時才會執(zhí)行绷柒,因?yàn)閙的數(shù)據(jù)變了志于,這就是React.memo的好處。
React.memo使得一個組件只有在它的props變化時废睦,它才會再執(zhí)行一遍并且再次渲染

Child組件還可以優(yōu)化:

const Child = React.memo(props=>{
  console.log("child 執(zhí)行了");
  console.log('假設(shè)這里有大量代碼')
  return <div> child:{props.data} </dic>
})

但是有個bug
例子:假設(shè)onClick支持onClick事件伺绽,它希望別人給它傳個onClick監(jiān)聽,在點(diǎn)擊div時嗜湃,就會調(diào)用props.onClick奈应。給Child2傳個onClick。

function App() {
  console.log("App 執(zhí)行了")
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => { setN(n + 1); };
  
  const onClickChild=()=>{} //這句話重新執(zhí)行
  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
       <Child2 data={m} onClick={onClickChild}/>
    </div>
  );
}

function Child(props) {
  console.log("child 執(zhí)行了");
  console.log('假設(shè)這里有大量代碼')
  return <div onClick = {props.onClick}>child: {props.data}</div>;
}
const Child2 = React.memo(Child);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Child2是優(yōu)化過后的函數(shù)购披,理論上來說杖挣,只要m和onClickChild不變,它就不需要重新執(zhí)行刚陡。比如我更新n惩妇,它應(yīng)該不需要重新執(zhí)行株汉。

測試下:Child2竟然執(zhí)行了,為什么呢歌殃?
因?yàn)楫?dāng)我點(diǎn)擊n+1時乔妈,App會重新執(zhí)行,const onClickChild=()=>{}這句話也會重新執(zhí)行氓皱。之前是一個空函數(shù)路召,現(xiàn)在又是另一個空函數(shù),2個不同的空函數(shù)就代表onClick變了波材。

那為什么n可以呢股淡?
因?yàn)楫?dāng)你寫m=0時,第一次的0和第二次的0都是數(shù)值廷区,數(shù)值是相等的揣非。但是函數(shù)是個對象,第一躲因、二次的空函數(shù)的地址是不相等的早敬,這就是值與引用的區(qū)別

那怎么解決這個問題呢大脉?
我不希望用戶在更新n時搞监,由于函數(shù)的更新而去渲染自己。
用useMemo镰矿,useMemo可以實(shí)現(xiàn)函數(shù)的重用琐驴。
方法:useMemo接受一個函數(shù),這個函數(shù)的返回值就是你要緩存的東西秤标。

function App() {
  console.log("App 執(zhí)行了")
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => { setN(n + 1); };
  
  const onClickChild = useMemo(()=>{ 
    return ()=>{} //復(fù)用
  },[m])
  //const onClickChild=()=>{} 
  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
       <Child2 data={m} onClick={onClickChild}/>
    </div>
  );
}

function Child(props) {
  console.log("child 執(zhí)行了");
  console.log('假設(shè)這里有大量代碼')
  return <div onClick = {props.onClick}>child: {props.data}</div>;
}
const Child2 = React.memo(Child);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

[圖片上傳失敗...(image-4fadcd-1651443540128)]

App執(zhí)行了绝淡,child沒執(zhí)行。因?yàn)楹瘮?shù)已經(jīng)被我們復(fù)用苍姜,只有在m變化時牢酵,你再重新給我生成一個,因?yàn)橛锌赡苓@個函數(shù)用到了m衙猪。useMemo用來緩存一些馍乙,你希望在2次新舊組件迭代的時候,希望用上次的值垫释,這個值就是一個函數(shù)丝格。

總結(jié)
我們在使用React時經(jīng)常發(fā)現(xiàn)有多余的render,比如說n變了,但是依賴m的組件卻自動刷新了棵譬,為了解決這個問題可以使用React.memo,這個memo可以做到如果props不變就沒有必要再執(zhí)行了显蝌。但它有個bug,就算我2次用到的是空函數(shù)/函數(shù)订咸,由于我的App重新渲染了曼尊,所以這個函數(shù)的地址就變了,是一個新的空函數(shù)扭屁。這就導(dǎo)致可props本質(zhì)上還是變了,變了就會一秒破功涩禀。新舊函數(shù)雖然功能一樣料滥,都是地址不一樣。我們可以使用React.useMemo

useMemo特點(diǎn)
1.第一個參數(shù)一定是函數(shù)()= value,不接受參數(shù)艾船。
2.第二個參數(shù)是數(shù)組
3.只有當(dāng)依賴變化時葵腹,才會計算出新的value。如果依賴不變屿岂,那么就重用之前的value
這不就是Vue2的computed嗎践宴?
我這個值是根據(jù)計算得出來的,而且我會緩存使用之前的值爷怀。

注意
如果你的value是個函數(shù)阻肩,那么你就要寫成useMemo( ()=> (x)=> console.log(x))
這是一個返回函數(shù)的函數(shù),很難用运授,于是就有了useCallback烤惊。

useCallback(最常用)

用法
直接寫你return的函數(shù)就行了。

useCallback(x=>log(x),[m])等價于
useMemo(()=> x=> log(x),[m])

優(yōu)化技巧2

const onClickChild = useMemo(()=>{ 
    return ()=>{
      console.log(m)
    } 
},[m])

//useCallback語法糖
const onClickChild =useCallback(()=>{ console.log(m) },[m])

優(yōu)化技巧1
用useMemo使得一些函數(shù)被重用吁朦,這樣就不至于去更新你已經(jīng)用React.memo優(yōu)化過的組件,一般這2個是一起用的柒室,先memo再useMemo。
優(yōu)化技巧2
如果你覺得useMemo太難用逗宜,可以用useCallback代替雄右。

useRef & forwardRef & useImperativeHandle

useRef(常用)

forwardRef、useImperativeHandle跟useRef有非常大的關(guān)系

import React,{useRef} from "react"
import ReactDOM from "react-dom"
 
//window.count = 0;
function App() {
  console.log("App 執(zhí)行了");
 const count=useRef(0) //current是隨著App render不會變的量
  useEffect(()=>{
    count.current +=1
    console.log(count)
  })
//window.count +=1
  const [n, setN] = useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  return (
    <div className="App">
      <button onClick={onClick}>update n {n}</button>
    </div>
  );
}

useRef+useEffect實(shí)現(xiàn)count +=1操作:
全局變量window.count可記錄render的次數(shù)纺讲。但是全局變量有個壞處擂仍,變量名容易沖突。
這時我們可以用useRef熬甚。
每次更新完后用useEffect對conut.current進(jìn)行操作逢渔。

conut規(guī)定: 如果你要對count進(jìn)行操作的話,必須要用conut.current则涯,因?yàn)閏urrent才是它真正的值复局。
在我們不停的渲染中,count始終不會變化粟判,每一次得到的都是同一個count,count的值被記錄在useRef對應(yīng)的一個對象上峦剔,這個對象跟App一一對應(yīng)档礁。

為什么需要current?
App每次渲染都會得到一個count吝沫。
為了保證2次useRef是同一樣的值(只有引用能做到)
新舊組件引用的對象必須是同一個對象呻澜,否則就會出問題递礼。對象地址是同一個,只是值改變了羹幸。
如果沒有current你改的就是對象本身脊髓。

const count=useRef({current:0}) //一開始不是對象,這里假設(shè)它就是一個對象
count.current +=1

總結(jié):
目前為止栅受,我們已經(jīng)學(xué)了3個關(guān)于"是否要變化"的hook将硝。
1.useState/useReducer
它們兩個每次的n都會變化,n每次變
2.useMemo/useCallback
只在依賴m屏镊,[m]變的時候fn才會變依疼,有條件的改變
3.useRef
永遠(yuǎn)不變

延伸
Vue3的ref就是抄襲React的ref,但是有一點(diǎn)不一樣:
如果你對Vue的ref進(jìn)行改變,UI會自動變化,不需要手動刷新而芥。但是React不會自動變化律罢。
例子:點(diǎn)擊button后,雖然useRef改變了棍丐,但是UI不會自動變化误辑。

function App() {
//console.log("App 執(zhí)行了");
  const [n, setN] = useState(0);
//const [_, set_] = useState(null);
  const count = useRef(0);
  const onClick2 = () => {
    count.current +=1
  //set_(Math.random);
    console.log(count.current);
  };
  useEffect(() => {
    console.log(count.current);
  });
  return (
    <div className="App">
      <div>
        <button onClick={onClick2}> update count{count.current} </button>
      </div>
    </div>
  );
}

[圖片上傳失敗...(image-f864cd-1651443540128)]

要想刷新UI只需要調(diào)用setState下并手動set:

const [_,set_]=React.useState(null) //調(diào)用useState
//手動set,只要這次值跟上次不一樣UI就會更新
const onClick2 = ()=>{
  count.current +=1
  set_(Math.random())
  coneolr.log(count.current)
}

Vue3的思路就是,你不需要寫set_(Math.random())歌逢,我發(fā)現(xiàn)你對current變更就會自動更新UI稀余。

對比
React的理念是UI=f(data),你要想變化時自動render就自己加,監(jiān)聽ref,當(dāng)ref.current變化時趋翻,調(diào)用setX即可睛琳。
1.useRef
初始化:const count=useRef(0)
讀取:count.current
2.Vue3
初始化:const count=ref(0)
讀取:count.value
不同點(diǎn):當(dāng)count.value變化時,Vue3會自動render

forwardRef

forwardRef跟useRef有非常大的關(guān)系

例1.為什么要用forwardRef
原因:props無法傳遞ref屬性

import React, { useRef } from "react";
import ReactDOM from "react-dom";
function App() {
  const buttonRef = useRef(null);
  return (
    <div className="App">
      <Button2 ref={buttonRef}>按鈕</Button2>
      {/* 看瀏覽器控制臺的報錯 */}
    </div>
  );
}

const Button2 = props => {
  console.log(props)
  return <button className="red" {...props} />;
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

知識點(diǎn)
1.用buttonRef引用到Button2對應(yīng)的DOM對象踏烙,這樣我就不需要用jQuery去找了师骗。

相當(dāng)于:
const button =document.querySelector("#x")
<Button2 id="x">

[圖片上傳失敗...(image-c28286-1651443540128)]

error:函數(shù)組件不能接受refs,只有類組件才能接受refs,你應(yīng)該用forwardRef
log下props:只把按鈕傳過去了,ref沒有傳讨惩,這就是報錯的原因辟癌。
[圖片上傳失敗...(image-99732d-1651443540128)]

你給我的ref我根本讀不到引用,那我怎么把<button>給你凹瞿怼黍少?應(yīng)該用forwardRef

如何使用React.forwardRef
1.Button3先用forwardRef包裝Button2,把外邊給你的ref轉(zhuǎn)發(fā)給你的第二個參數(shù)处面,這樣你就可以使用refl了厂置。
2.Button2添加第二個參數(shù)ref
3.使用ref

例2.實(shí)現(xiàn)ref的傳遞

import React, { useRef } from "react";
import ReactDOM from "react-dom";
function App() {
  const buttonRef = useRef(null);
  return (
    <div className="App">
      <Button3 ref={buttonRef}>按鈕</Button2>
    </div>
  );
}

const Button2 = (props, ref) => { //2.添加ref
  console.log(props);
  console.log(ref)
  return <button className="red" ref={ref} {...props} />;//3.使用ref
};
const Button3 = React.forwardRef(Button2); //1.用forwardRef包裝Button2

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

這樣改就沒有任何問題了,同樣props里還是沒有ref,但是ref是可以包含到外面給我傳進(jìn)來的button ref的魂角。
[圖片上傳失敗...(image-3eec8d-1651443540128)]

總結(jié): 如果你的函數(shù)組件(Button2)昵济,想要接收別人App傳來的ref參數(shù),你必須把自己用React.forwardRef包起來。想用ref就必須要用React.forwardRef访忿,僅限函數(shù)組件瞧栗,class組件是默認(rèn)可以用的。

優(yōu)化代碼

const Button3 = React.forwardRef((props, ref) => {
  console.log(props);
  console.log(ref)
  return <button className="red" ref={ref} {...props} />;
})

例3.2次ref傳遞得到button的引用
通過ref引用到里面的button需要做兩次傳遞:
buttonRef第一次通過forwardRef傳給了Button2,Button2得到ref后傳遞給了button海铆。

function App() {
//MovableButton就是對Button2的一個包裝
  const MovableButton = movable(Button2);
  const buttonRef = useRef(null);
  useEffect(() => {
    console.log(buttonRef.curent);
  });
  return (
    <div className="App">
      <MovableButton name="email" ref={buttonRef}>//通過ref引用到里面的button
        按鈕
      </MovableButton>
    </div>
  );
}
const Button2 = React.forwardRef((props, ref) => {
  return <button ref={ref} {...props} />;
});

// 僅用于實(shí)驗(yàn)?zāi)康募?郑灰诠敬a中使用
function movable(Component) { //可以移動的組件
  function Component2(props, ref) { //接收組件1Component,返回組件2Component2
    console.log(props, ref);
    const [position, setPosition] = useState([0, 0]);
    const lastPosition = useRef(null);
    const onMouseDown = e => {
      lastPosition.current = [e.clientX, e.clientY];
    };
    const onMouseMove = e => {
      if (lastPosition.current) {
        const x = e.clientX - lastPosition.current[0];
        const y = e.clientY - lastPosition.current[1];
        setPosition([position[0] + x, position[1] + y]);
        lastPosition.current = [e.clientX, e.clientY];
      }
    };
    const onMouseUp = () => {
      lastPosition.current = null;
    };
    return (
      <div
        className="movable"
        onMouseDown={onMouseDown}
        onMouseMove={onMouseMove}
        onMouseUp={onMouseUp}
        style={{ left: position && position[0], top: position && position[1] }}
      >
        <Component {...props} ref={ref} />
      </div>
    );
  }
  return React.forwardRef(Component2);
}

總結(jié): 由于props不包含ref,所以需要forwardRef卧斟。
為什么props不包含ref呢殴边?因?yàn)榇蟛糠謺r候不需要
如果你希望一個組件支持ref屬性,那么你就需要用forwardRef把這個函數(shù)組件包起來唆涝,然后給他增加第二個屬性ref找都。

useImperativeHandle(用不著)

useImperativeHandle跟useRef相關(guān)的鉤子
使用一個重要的handle,名字起的稀爛廊酣,應(yīng)該叫setRef

分析:用于自定義ref的屬性

例1.不用useImperativeHandle的代碼:

import React, {useRef,useState,useEffect,useImperativeHandle,createRef} from "react";
import ReactDOM from "react-dom";

function App() {
  const buttonRef = useRef(null); //buttonRef就是buttonDOM對象的引用
  useEffect(() => { //渲染之前不存在能耻,只能在渲染之后打
    console.log(buttonRef.current);
  });
  return (
    <div className="App">
      <Button2 ref={buttonRef}>按鈕</Button2>
      <button className="close" onClick={() => {
          console.log(buttonRef);
          buttonRef.current.remove();
        }}
      >
        x
      </button>
    </div>
  );
}

const Button2 = React.forwardRef((props, ref) => {
  return <button ref={ref} {...props} />;
});

[圖片上傳失敗...(image-33afe5-1651443540128)]

buttonRef就是button DOM對象的引用,打印出來就是個<button>
如果你希望得到的不是<button>而是一個你對<button>的封裝呢亡驰?
這個需求很奇怪晓猛,所以大部分時候用不到。

例2.用了useImperativeHandle的代碼:

function App() {
  const buttonRef = useRef(null);
  useEffect(() => { 
    console.log(buttonRef.current);
  });
  return (
    <div className="App">
      <Button2 ref={buttonRef}>按鈕</Button2> //Button2想自定義ref
      <button className="close" onClick={() => {
          console.log(buttonRef);
          buttonRef.current.x();
        }}>
        x
      </button>
    </div>
  );
}

const Button2 = React.forwardRef((props, ref) => {
  const realButton = createRef(null);
//如何自定義ref
  const setRef = useImperativeHandle;
  setRef(ref, () => { //假的ref
    return {
      x: () => {
        realButton.current.remove();
      },
      realButton: realButton //真的ref(也可以給它真正的ref用)
    };
  });
  return <button ref={realButton} {...props} />;
});

[圖片上傳失敗...(image-448248-1651443540128)]

ref可以支持自定義
比如說Button2想自定義ref,不想把button給別人凡辱,那怎么自定義ref呢戒职?
把ref賦值成一個對象。
ref就是個對象透乾,ref的x就是一個函數(shù)洪燥,這個函數(shù)會去對button進(jìn)行一些操作。
setRef是個假的ref,把它暴露在外面
我自己使用真的refuseImperativeHandle
這樣別人引用我時乳乌,只能引用到假的setRef
所以這個hook真正意圖是對ref進(jìn)行設(shè)置捧韵,以達(dá)到某種不可告人的目的,這個useImperativeHandle幾乎不用汉操。

總結(jié): 如果一個函數(shù)組件暴露了ref在外面再来,那么你可以自定義這個ref。

自定義 Hook

例1.封裝數(shù)據(jù)操作
步驟
1.新建目錄hooks,新建文件useList.js

useList.js
import { useState, useEffect } from "react";

const useList = () => { 
  const [list, setList] = useState(null); //設(shè)置state
  useEffect(() => { 
    ajax("/list").then(list => { 
      setList(list);
    });
  }, []); 
  return {
    list: list, //是同一個對象的引用磷瘤,把地址傳給外面
    setList: setList
  };
};
export default useList;

function ajax() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([
        { id: 1, name: "Frank" },
        { id: 2, name: "Jack" },
        { id: 3, name: "Alice" },
        { id: 4, name: "Bob" }
      ]);
    }, 2000);
  });
}

useList.js解析
一開始就請求"/list"數(shù)據(jù):得到list之后就setList芒篷,setList之后list就會變,引用的人也就知道了采缚。[] 確保只在第一次運(yùn)行针炉, 把讀寫接口return出去,引用/調(diào)用useList函數(shù)時就可以得到讀寫接口仰担,list是同一個對象的引用糊识,把地址傳給外面list(index.js的list引用)绩社。

在我調(diào)用setList時摔蓝,我set的雖然是我這個state(useState),但是由于useList是在App組件里調(diào)用的赂苗。所以在使用useList時,相當(dāng)于把代碼(useList函數(shù)里的代碼)拷到App組件里了。所以雖然我的useState不是在App里寫的贮尉,但是依然不報錯拌滋,因?yàn)槲沂窃谶@里運(yùn)行的。

2.引用useList

index.js
import React from "react";
import ReactDOM from "react-dom";
import useList from "./hooks/useList";

function App() {
  const { list, setList } = useList();
  return ( //DOM
    <div className="App">
      <h1>List</h1>
      {list ? (
        <ol>
         {list.map(item=> (<li key={item.id}>{item.name}</li>))}
        </ol>
      ) : ("加載中...")}
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

[圖片上傳失敗...(image-7cd7e0-1651443540128)]

如何封裝猜谚?
1.你(useList.js)不管用到什么hook,你全部都把它寫在一個函數(shù)(useList)里面:把相關(guān)的邏輯都寫到一起败砂,最后把你的讀接口、寫接口暴露出去就行了魏铅。
2.然后別人(index.js)就只需要知道你的讀接口昌犹、寫接口,其它的一概不管览芳。

比如說你有很多數(shù)據(jù)

const { list } = useList()
const { user } = useUser()

useUser會自己去初始化user,自己去請求user,請求完了自己去setUser斜姥。
我這邊只需要讀user就行了,這就是自定義hook的牛B之處沧竟。
但是你既然可以封裝铸敏,不妨封裝的更厲害一點(diǎn),不要只有一個讀和寫悟泵,增刪改查全部都可以做出來杈笔。

比如說,我們對useList做了升級糕非。

import { useState, useEffect } from "react";

const useList = () => {
  const [list, setList] = useState(null);
  useEffect(() => {
    ajax("/list").then(list => {
      setList(list);
    });
  }, []); 
  return {
    list: list, //讀接口
    addItem: name => { //增接口
      setList([...list, { id: Math.random(), name: name }]);
    },
    deleteIndex: index => { //刪接口
      setList(list.slice(0, index).concat(list.slice(index + 1)));
    }
  };
};
export default useList;

function ajax() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([
        { id: "1", name: "Frank" },
        { id: "2", name: "Jack" },
        { id: "3", name: "Alice" },
        { id: "4", name: "Bob" }
      ]);
    }, 2000);
  });
}

給了一個讀接口蒙具,用來讀list。給了一個增接口朽肥,用來添加item禁筏。給了一個刪接口,用來刪除index鞠呈。

點(diǎn)按鈕就刪除: 當(dāng)你onClick時融师,我就直接調(diào)用deleteIndex,然后把index傳給你deleteIndex(index)就刪掉了。根本不需要知道list是從哪里請求數(shù)據(jù)蚁吝、是怎么刪除的旱爆、我一概不關(guān)心。我只需要得到一個讀或者幾個寫窘茁。

index.js
import React, { useRef, useState, useEffect } from "react";
import ReactDOM from "react-dom";
import useList from "./hooks/useList";

function App() {
  const { list, deleteIndex } = useList(); 
//const { list, deleteIndex, addItem} = useList();  //得到一個讀或者幾個寫
  return (
    <div className="App">
      <h1>List</h1>
      {list ? (
        <ol>
          {list.map((item, index) => (
            <li key={item.id}>
              {item.name}
              <button onClick={() => { deleteIndex(index);}} >
                x
              </button>
            </li>
          ))}
        </ol>
      ) : ( "加載中...")}
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

分析
1.你甚至還可以在自定義Hook里使用Context
這樣你可以把自定義Hook和useReducer以及useContext結(jié)合起來,完全代替了redux怀伦。
所以在新版的React里面沒有必要再使用redux了。
2.useState只說了不能在if else里使用山林,但沒說不能在函數(shù)里運(yùn)行
只要這個函數(shù)在函數(shù)組件里運(yùn)行即可
希望大家在React項(xiàng)目中盡量使用自定義Hook,不要再去搞一些useState房待、useEfect放到這個組件上部,不要出現(xiàn)這種代碼。

Stale Closure

Stale Closure(過時閉包)
用來描述你的函數(shù)引用的變量是之前產(chǎn)生的那個變量桑孩。

怎么避免呢拜鹤?
基本上是通過加個依賴,讓它自動刷新流椒,要記得清除舊的計時器敏簿。
所以一般來說不用計時器,比較麻煩。

JS中的Stale Closure

function createIncrement(i) { 

//每調(diào)用一次這個函數(shù)宣虾,就會對value+i的操作惯裕,閉包读宙。
  function increment() { 
    let value = 0;
    value += i;
    console.log(value);
  }
  const message = `Current value is ${value}`;
  function log() {
    console.log(message);
  }
  return [increment, log]; 
}
const [increment, log] = createIncrement(1);//析構(gòu)函數(shù)
increment(); // 1
increment(); // 2
increment(); // 3
// Does not work!
log();       // "Current value is 0"

useState里多次講過祸穷,由于每次你在執(zhí)行函數(shù)時都生成了一個message,所以第一次執(zhí)行message得到1屋彪,第二次執(zhí)行message得到2鹉胖,第三次執(zhí)行message得到3握玛。

那你要是初始就把message記住了,那這個message里面的value就是0啊次员,log就永遠(yuǎn)只會打0败许,不會打后面的。因?yàn)楹竺娴氖怯勺约旱膌og,那么這個log就叫做過時的log,因?yàn)閕已經(jīng)創(chuàng)建了3次淑蔚,log也創(chuàng)建了3次市殷,但是你卻保留的是初始值log,這就導(dǎo)致它過時了刹衫。

怎么解決醋寝?
每次log前重新去取這個log

  function log() {
    const message = `Current value is ${value}`;
    console.log(message);
  }

不要一開始就記下value,而是在調(diào)用log時,用log去取最新的值带迟。
這就是JS中過時閉包的解決方法音羞。

React中的Stale Closure
1' useEffect()

function WatchCount() {
  const [count, setCount] = useState(0);
  useEffect(function() {
    setInterval(function log() {
      console.log(`Count is: ${count}`);
    }, 2000);
  }, []);//只在第一次設(shè)置計時器,所以count是過時的仓犬。
  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1) }>
        Increase
      </button>
    </div>
  );
}

解決方法:把count放在依賴?yán)镄岽拢瑫r把之前的id清掉。
生成了id又把id給clearInterval了搀继,這不就相當(dāng)于什么都沒做嘛窘面?
不是,生成的是最新的id叽躯,刪掉的是上一次組件消失時的id财边,調(diào)用時機(jī)不同。

function WatchCount() {
  const [count, setCount] = useState(0);
  useEffect(function() {
    const id = setInterval(function log() {
      console.log(`Count is: ${count}`);
    }, 2000);
    return function() {
      clearInterval(id);
    }
  }, [count]);
  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1) }>
        Increase
      </button>
    </div>
  );
}

2' useState()

function DelayedCount() {
  const [count, setCount] = useState(0);
  function handleClickAsync() {
    setTimeout(function delay() {
      setCount(count + 1);
    }, 1000);
  }
  return (
    <div>
      {count}
      <button onClick={handleClickAsync}>
        Increase async
      </button>
    </div>
  );
}

1s后打印count点骑,在這1s之間count +=1根本不知道它變了酣难,你用的永遠(yuǎn)都是舊的count谍夭。

解決方法:堅持使用函數(shù)作為setState的參數(shù)。
這樣你就不會受制于舊的還是新的憨募,因?yàn)槟銈鞯氖且粋€動作紧索,這個動作是不關(guān)心這個數(shù)據(jù)當(dāng)前的值是什么的,不關(guān)心你現(xiàn)在是什么值馋嗜,只關(guān)心+1齐板。

function DelayedCount() {
  const [count, setCount] = useState(0);
  function handleClickAsync() {
    setTimeout(function delay() {
      setCount(count => count + 1);
    }, 1000);
  }
  function handleClickSync() {
    setCount(count + 1);
  }
  return (
    <div>
      {count}
      <button onClick={handleClickAsync}>Increase async</button>
      <button onClick={handleClickSync}>Increase sync</button>
    </div>
  );
}

總結(jié)

1.useState狀態(tài)
2.useEffect(副作用)就是afterRender
3.useLayoutEffect就是比useEffect提前一點(diǎn)點(diǎn)吵瞻。
但是很少用葛菇,因?yàn)闀绊戜秩镜男?除非特殊情況才會用。
4.useContext上下文橡羞,用來把一個讀眯停、寫接口給整個頁面用。
5.useReducer專門給Redux的用戶設(shè)計的(能代替Redux的使用)卿泽,我們甚至可以不用useReducer莺债。
6.useMemo(記憶)需要與React.Memo配合使用,useMemo不好用我們可以升級為更好用的useCallback(回調(diào))
7.useRef(引用)就是保持一個量不變签夭,關(guān)于引用還有個forwardRef,forwardRef并不是一個Hook,還有個useImperativeHandle就是setRef齐邦。
就是我支持ref時,可以自定義ref長什么樣子第租,那就使用useImperativeHandle措拇。
8.自定義Hook
示例中的useList就是自定義Hook,非常好用。
有個默認(rèn)的自定義HookuseDebugValue就是你在debugger時,可以給你的組件加上名字慎宾,很少用丐吓。

更多文章,請點(diǎn)擊 我的博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末趟据,一起剝皮案震驚了整個濱河市券犁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌汹碱,老刑警劉巖粘衬,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異咳促,居然都是意外死亡稚新,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進(jìn)店門等缀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來枷莉,“玉大人,你說我怎么就攤上這事尺迂◇悦睿” “怎么了冒掌?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蹲盘。 經(jīng)常有香客問我股毫,道長,這世上最難降的妖魔是什么召衔? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任铃诬,我火速辦了婚禮,結(jié)果婚禮上苍凛,老公的妹妹穿的比我還像新娘趣席。我一直安慰自己,他們只是感情好醇蝴,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布宣肚。 她就那樣靜靜地躺著,像睡著了一般悠栓。 火紅的嫁衣襯著肌膚如雪霉涨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天惭适,我揣著相機(jī)與錄音笙瑟,去河邊找鬼。 笑死癞志,一個胖子當(dāng)著我的面吹牛往枷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播今阳,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼师溅,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了盾舌?” 一聲冷哼從身側(cè)響起墓臭,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎妖谴,沒想到半個月后窿锉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡膝舅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年嗡载,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片仍稀。...
    茶點(diǎn)故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡洼滚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出技潘,到底是詐尸還是另有隱情遥巴,我是刑警寧澤千康,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站铲掐,受9級特大地震影響拾弃,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜摆霉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一豪椿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧携栋,春花似錦搭盾、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至磅摹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間霎奢,已是汗流浹背户誓。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留幕侠,地道東北人帝美。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像晤硕,于是被迫代替她去往敵國和親悼潭。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評論 2 354

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