Hooks 各個擊破

React文檔
Hooks:useState、useEffect、useLayoutEffect、useContext、useReducer、useMemo、React.memo、callCallback、useRef、useImperativeHandle、自定義Hook、useDebugValue

useState(最常用)

在React的函數(shù)組件里,默認只有屬性,沒有狀態(tài)。

1.使用狀態(tài)

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

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

例1:不可局部更新
如果state是個對象,能否部分setState?
不行,因為setState不會幫我們合并屬性。所以當只更新部分屬性時,未更新的屬性就會消失。

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

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新版的所有東西都不會幫你合并,它認為這是你自己要做的事。

例2.地址要變
我想把name改下:于是直接修改user.name然后setUser(user)
你會發(fā)現(xià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ù)的好處是:減少多余的計算過程,因為JS引擎不會立即執(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ù))
點擊button后你會發(fā)現(xiàn)n=1而不是2,因為當你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進行多次操作時,優(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)需要導入useReducer或者直接使用全稱React.useReducer
(2)useReducer接收2個參數(shù):所有操作reducer初始狀態(tài)initical
(3)你將得到讀API、寫API寫API一般叫dispatch,因為你必須通過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操作
  })
}

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

調(diào)用傳參:+2時傳了參數(shù)number:2,那么reducer里的1就可以變成一個參數(shù)。因為dispatch()里傳的對象就是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的復雜版。好處是用來踐行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對對象進行整體的操作。

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事件。用戶輸入即更新,因為內(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ù)里面運行。

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);
  });
}

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

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

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

項目代碼

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)自己刷新,因為React知道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,是如何實現(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)的代碼分別剪切進去,并導出。
第3步.對于共用的函數(shù),也要新建文件,單獨拎出來。
(1)Context是組件共用的,所以要新建文件Context.js,把相關(guān)代碼剪切出來,并導出。

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

(2)使用Context、ajax

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

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

細化reducer

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

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

變成對象之后就好弄了,因為對象很好合并,函數(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個模塊呢?

第二部分.細化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(常用)

概念
上下文就是你運行一個程序所需要知道的所有其它變量(全局變量)。
全局變量是全局的上下文,所有變量都可以訪問它。
上下文是局部的全局變量,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注意事項
不是響應(yīng)式的
你在一個模塊將C里面的值改變,另一個模塊不會感知到這個變化。
更新的機制并不是響應(yīng)式的,而是重新渲染的過程。
比如,當我們點擊+1時:setN去通知useState,useState重新渲染App,發(fā)現(xiàn)n變了,于是問里面的組件<Baba />有沒有用到n?沒有,就繼續(xù)問<Child />有沒有用到n?用到了,這時候兒子就知道要刷新了,是一個從上而下逐級通知的過程,并不是響應(yīng)式的過程。

Vue3是你改n時,它就知道n變了,于是它就找誰用到了n,它就把誰直接改變了。它不會從上而下整體過一遍,沒有這么復雜,因為它是一個響應(yīng)式的過程。
總結(jié): useContext的更新機制式是自頂向下,逐級更新數(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
以上三種用途可同時存在
特點
如果同時存在多個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("只有當n變了才會執(zhí)行這句話")//監(jiān)聽某個值變化時執(zhí)行,包含第一次
  },[n])
  useEffect(()=>{
    if(n !== 0){
      console.log("n變化時會執(zhí)行這句話,剔除第一次")//默認包含第1次,要想排除第1次可以判斷下
    }
  },[n])
  //第一次進來時使實現(xiàn)setInterval,每秒打印一個hi
  //當組件消失時,把定時器關(guān)掉,不然會一直打印hi
  //告訴React return一個函數(shù):當組件掛掉時要執(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.當?shù)?個參數(shù)是[]時,表示只會在第一次渲染后執(zhí)行前面的函數(shù)。
2.當不寫第2個參數(shù)時,表示每次update都會執(zhí)行前面的函數(shù)。
3.當?shù)?個參數(shù)是[n]時,表示只會在某個值變化(n)時才會去執(zhí)行前面的函數(shù),包含第一次。
要想剔除第一次可以,可以加個判斷。
4.加return死亡時執(zhí)行
如果我這個組件要掛了,我這個組件正要離開頁面,一般在使用router時會經(jīng)常去用。
比如,一開始是第1個頁面,點了按鈕后會跳到第2個頁面,那么第1個頁面的所有組件都掛掉了。
掛掉的時候你可能需要做一些清理動作。用return,return一個函數(shù):函數(shù)里面是當組件掛掉時要執(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?
不是,因為大部分時候不會去改變DOM,不用截胡。
因為用戶想看的就是外觀,本來只需要1ms的,現(xiàn)在加了幾句話變成3ms了,影響用戶體驗。
所以從經(jīng)驗上來說,我們更希望將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) //打點一:setN后馬上打點
    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);

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

特點
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ō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)]

點擊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)在點擊button后,2個log就再也不會執(zhí)行了。除了第一次渲染時會執(zhí)行console,之后再也不會執(zhí)行。除非當m第一次渲染時才會執(zhí)行,因為m的數(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)聽,在點擊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í)行了,為什么呢?
因為當我點擊n+1時,App會重新執(zhí)行,const onClickChild=()=>{}這句話也會重新執(zhí)行。之前是一個空函數(shù),現(xiàn)在又是另一個空函數(shù),2個不同的空函數(shù)就代表onClick變了。

那為什么n可以呢?
因為當你寫m=0時,第一次的0和第二次的0都是數(shù)值,數(shù)值是相等的。但是函數(shù)是個對象,第一、二次的空函數(shù)的地址是不相等的,這就是值與引用的區(qū)別。

那怎么解決這個問題呢?
我不希望用戶在更新n時,由于函數(shù)的更新而去渲染自己。
用useMemo,useMemo可以實現(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 ()=>{} //復用
  },[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í)行。因為函數(shù)已經(jīng)被我們復用,只有在m變化時,你再重新給我生成一個,因為有可能這個函數(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ù)。這就導致可props本質(zhì)上還是變了,變了就會一秒破功。新舊函數(shù)雖然功能一樣,都是地址不一樣。我們可以使用React.useMemo

useMemo特點
1.第一個參數(shù)一定是函數(shù)()= value,不接受參數(shù)。
2.第二個參數(shù)是數(shù)組
3.只有當依賴變化時,才會計算出新的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實現(xiàn)count +=1操作:
全局變量window.count可記錄render的次數(shù)。但是全局變量有個壞處,變量名容易沖突。
這時我們可以用useRef。
每次更新完后用useEffect對conut.current進行操作。

conut規(guī)定: 如果你要對count進行操作的話,必須要用conut.current,因為current才是它真正的值。
在我們不停的渲染中,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)學了3個關(guān)于"是否要變化"的hook。
1.useState/useReducer
它們兩個每次的n都會變化,n每次變
2.useMemo/useCallback
只在依賴m,[m]變的時候fn才會變,有條件的改變
3.useRef
永遠不變

延伸
Vue3的ref就是抄襲React的ref,但是有一點不一樣:
如果你對Vue的ref進行改變,UI會自動變化,不需要手動刷新。但是React不會自動變化。
例子:點擊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,當ref.current變化時,調(diào)用setX即可。
1.useRef
初始化:const count=useRef(0)
讀取:count.current
2.Vue3
初始化:const count=ref(0)
讀取:count.value
不同點:當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);

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

相當于:
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.實現(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是可以包含到外面給我傳進來的button ref的。
[圖片上傳失敗...(image-3eec8d-1651443540128)]

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

優(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} />;
});

// 僅用于實驗目的,不要在公司代碼中使用
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呢?因為大部分時候不需要
如果你希望一個組件支持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進行一些操作。
setRef是個假的ref,把它暴露在外面
我自己使用真的refuseImperativeHandle
這樣別人引用我時,只能引用到假的setRef
所以這個hook真正意圖是對ref進行設(shè)置,以達到某種不可告人的目的,這個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就會變,引用的人也就知道了。[] 確保只在第一次運行, 把讀寫接口return出去,引用/調(diào)用useList函數(shù)時就可以得到讀寫接口,list是同一個對象的引用,把地址傳給外面list(index.js的list引用)。

在我調(diào)用setList時,我set的雖然是我這個state(useState),但是由于useList是在App組件里調(diào)用的。所以在使用useList時,相當于把代碼(useList函數(shù)里的代碼)拷到App組件里了。所以雖然我的useState不是在App里寫的,但是依然不報錯,因為我是在這里運行的。

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之處。
但是你既然可以封裝,不妨封裝的更厲害一點,不要只有一個讀和寫,增刪改查全部都可以做出來。

比如說,我們對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。

點按鈕就刪除: 當你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ù)里運行
只要這個函數(shù)在函數(shù)組件里運行即可
希望大家在React項目中盡量使用自定義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就永遠只會打0,不會打后面的。因為后面的是由自己的log,那么這個log就叫做過時的log,因為i已經(jīng)創(chuàng)建了3次,log也創(chuàng)建了3次,但是你卻保留的是初始值log,這就導致它過時了。

怎么解決?
每次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放在依賴里,同時把之前的id清掉。
生成了id又把id給clearInterval了,這不就相當于什么都沒做嘛?
不是,生成的是最新的id,刪掉的是上一次組件消失時的id,調(diào)用時機不同。

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根本不知道它變了,你用的永遠都是舊的count。

解決方法:堅持使用函數(shù)作為setState的參數(shù)。
這樣你就不會受制于舊的還是新的,因為你傳的是一個動作,這個動作是不關(guān)心這個數(shù)據(jù)當前的值是什么的,不關(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提前一點點。
但是很少用,因為會影響渲染的效率,除非特殊情況才會用。
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,非常好用。
有個默認的自定義HookuseDebugValue就是你在debugger時,可以給你的組件加上名字,很少用。

更多文章,請點擊 我的博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容