React Hooks詳解

useState

1.基本使用
import { useState } from "react";
function App() {
  // 數(shù)組里的第一項是sate里的變量浆熔,第二項是修改state的函數(shù)
  // useState里的值就是count的初始值
  const [count, setCount] = useState(0);
  const add = () => {
    setCount(count + 1);
  };
  return (
    <div>
      <div>{count}</div>
      <div>
        <button onClick={add}>+1</button>
      </div>
    </div>
  );
}
ReactDOM.render(<App />, document.querySelector("#root"));

等價于

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      count: 0
    };
  }
  setCount = () => {
    this.setState({
      count: this.state.count + 1
    });
  };
  render() {
    return (
      <div>
        <div>{this.state.count}</div>
        <button onClick={this.setCount}>+1</button>
      </div>
    );
  }
}
2. 復雜的state
import { useState } from "react";
function App() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({
    name: "lifa",
    age: 18,
    habits: ["小改改", "明星"]
  });
  const add = () => {
    setCount(count + 1);
  };
  const minus = () => {
    setCount(count - 1);
  };
  const addNum = () => {
    setUser({
      ...user,
      age: user.age + 1,
      habits: [...user.habits, "Lifa"]
    });
  };
  const minusNum = () => {
    const newHabits = user.habits.splice(1, 1);
    setUser({
      ...user,
      age: user.age - 1,
      habits: newHabits
    });
  };
  return (
    <div>
      <div>{count}</div>
      <button onClick={add}>+1</button>
      <button onClick={minus}>-1</button>
      <div>
        {user.name}, {user.age} <br />
        {user.habits.join(",")}
      </div>
      <button onClick={addNum}>變大</button>
      <button onClick={minusNum}>減少</button>
      <div />
    </div>
  );
}
3.使用狀態(tài)
const [n,setN] = React.useState(0)
const [user, setUser] = React.useState({name: 'F'})
4. 注意事項

1). 如果state是一個對象,我們不能對對象里的部分屬性setState呵晨,需要我們每次都把之前的屬性全部重新結(jié)構(gòu)一遍,然后下面再寫你要修改的屬性

// 錯誤代碼
const [user,setUser] = useState({name:'lifa', age: 18})
const onClick = ()=>{
    setUser({
      name: 'Jack'
    })
  }

//正確代碼
setUser({
   ...user,
   name: 'Jack'
 })

2). 地址要變
setState(obj)如果obj地址不變兄渺,那么React就認為數(shù)據(jù)沒有變化

// 錯誤代碼
const [user,setUser] = useState({name:'lifa', age: 18})
const onClick = () => {
  // 在原來的引用地址上修改name屬性遭庶,不會起作用
  user.name = 'jack'
  setUser(user)
}

// 正確代碼
const [user,setUser] = useState({name:'lifa', age: 18})
const onClick = () => {
  // 重新生成一個引用地址
  setUser({
    ...user,
    name: 'jack'
  })
}

3). useState只能放在函數(shù)組件內(nèi)部魄懂,不能單獨拿出來

5. useState可以接受函數(shù)
const [state, setState] = useState(()=>{
  return initialState
})

該函數(shù)返回初始state, 且只執(zhí)行一次

6. setState可以接受函數(shù)

我們?nèi)绻啻螌seState進行操作的話推薦使用函數(shù)
以兩次修改useState對其進行加一操作為例

const [n,setN] = useState(1)
const onClick = () => {
  setN(n+1)
  setN(n+1)
}

上面我們在點擊事件里執(zhí)行了兩次修改n熊楼,每次讓他加一鸭叙,可實際上他只會變一次觉啊,因為n本身是不會變的,而是每次生成一個新的n沈贝,所以上面結(jié)果是2而不是3杠人,如果想要它加2的話就要用函數(shù)

setN(i=>i+1)
setN(i=>i+1)

上面的i是一個占位符,隨便什么都可以,就是我們傳一個值給setN嗡善,每次返回當前的值+1辑莫,所以最后會加2,得到的結(jié)果是3

往數(shù)組 push 一條數(shù)據(jù)

const handleAddAuth = () => {
    const rateCfg: Base[] = [];
    for (let i = 0; i < num; i++) {
      rateCfg.push({
        id: '',
        rate: '',
      });
    }
    const newAuth = {
      authId: '',
      rateCfg,
    };
    setAuthBase((odlAuth) => [...odlAuth, newAuth]);
  };

useReducer

用來踐行Flux/Redux的思想
代碼過程

  1. 創(chuàng)建初始值initialState
const intialState = {
  n: 0
}

2.創(chuàng)建所有操作reducer(state,action)

const reducer = (state, action) => {
  if(action.type === 'add') {
    return {n: state.n + action.number}
  } else if (action.type === 'mul') {
    return {n: state.n * 2}
  } else {
    throw new Error('unknow type')
  }
}

3.傳給useReducer罩引,得到讀和寫API

const [state, dispatch] = useReducer(reducer, intialState)

4.調(diào)用寫({type: '操作類型'})

const onClick = () => {
    dispatch({ type: "add", number: 1 });
}

總的來說useRducer是useState的復雜版

一個用useReducer的表單例子
https://codesandbox.io/s/awesome-mahavira-foyk3

用useReducer代替redux

這里以一個簡單的列表為例

function User () {
  return (
    <div>
      <h1>個人信息</h1>
    </div>
  )
}
function Books () {
  return (
    <div>
      <h1>我的書籍</h1>
    </div>
  )
}
function Movies () {
  return (
    <div>
      <h1>我的電影</h1>
    </div>
  )
}
function App () {
  return (
    <div>
      <User/>
      <hr />
      <Books />
      <Movies />
    </div>
  )
}
  • 步驟
    1). 將數(shù)據(jù)集中在一個store對象
const store = {
  user: null,
  books: null,
  movies: null
}

2). 將所有操作集中在reducer上

const 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()    
  }
}

3). 創(chuàng)建Context

const Context = createContext(null

4). 創(chuàng)建對數(shù)據(jù)的讀寫API

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

5). 將第4步的內(nèi)容放到第3步的Context

function App () {
  const [state, dispatch] = useReducer(reducer, store)
  return (
    <Context.Provider value={{state, dispatch}}>
    </Context.Provider>
  )
}

6). 用Context.Provider將Context提供給所有組件

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

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

function User () {
  const {state, dispatch} = useContext(Context)
  ajax('/user').then(user => {
    dispatch({ type: 'setUser', user })
  })
  return (
    <div>
      <h1>個人信息</h1>
      <div>name: {state.user ? state.user.name : ""}</div>
    </div>
  )
}
function ajax(path) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (path === '/user') {
        resolve({
          id: 1,
          name: 'Lifa'
        })
      } else if (path === '/books') {
        resolve([
          {
            id: 1,
            name: '金瓶梅'
          },
          {
            id: 2,
            name: '肉蒲團'
          }
        ])
      } else if (path === '/movies') {
        resolve([
          {
            id: 1,
            name: '性女傳奇'
          },
          {
            id: 2,
            name: '電車癡漢'
          }
        ])
      }
    }, 2000)
  })
}

上面的User每次執(zhí)行的時候都會修改state各吨,state一修改就會重新調(diào)一下User,User重新調(diào)了又會重新請求ajax蜒程,也就是每次render都會請求一次绅你,所以我們需要使用useEffect

function User() {
  const { state, dispatch } = useContext(Context);
  useEffect(() => {
    ajax("/user").then(user => {
      console.log("111");
      dispatch({ type: "setUser", user });
    });
  }, []);
  return (
    <div>
      <h1>個人信息</h1>
      <div>name: {state.user ? state.user.name : ""}</div>
    </div>
  );
}
  • 代碼模塊化
    1). 方法分模塊放到components里
    2). Context單獨放到當前目錄下
    3). 接口請求單獨放到當前目錄下
  • components/books.js
import React, { useContext, useEffect } from "react";
import ajax from '../ajax'
import Context from '../Context'
function Books() {
  const { state, dispatch } = useContext(Context);
  useEffect(() => {
    ajax("/books").then(books => {
      console.log("111");
      dispatch({ type: "setBooks", books });
    });
  }, []);
  return (
    <div>
      <h1>我的書籍</h1>
      <div>
        <ul>
          {state.books ? (
            state.books.map(book => <li>{book.name}</li>)
          ) : (
            <li>加載中</li>
          )}
        </ul>
      </div>
    </div>
  );
}
export default Books;
  • Context.js
import { createContext} from "react";
const Context = createContext(null);
export default Context;

4). 對reducer分模塊
首先把之前的reducer改寫成對象的形式

const obj = {
  'setUser': (state, action) => {
    return { ...state, user: action.user }
  },
  'setBooks': (state, action) => {
    return { ...state, books: action.books };
  },
  'setMovies': (state, action) => {
    return { ...state, movies: action.movies };
  }
}
function reducer(state, action) {
  const fn = obj[action.type]
  if (fn) {
    fn(state, action)
  } else {
    throw new Error()
  }
}

然后新建reducers目錄

  • reducers/user_reducer.js
export default {
  setUser: (state, action) => {
    return { ...state, user: action.user };
  },
  removeUser: () => {}
};
  • index.js
import React, { createContext, useReducer, useContext, useEffect } from "react";
import ReactDOM from "react-dom";
import Books from "./components/books";
import User from "./components/user";
import Movies from "./components/movies";
import Context from "./Context";
import userReducer from "./reducers/user_reducer";
import moviesReducer from "./reducers/movies_reducer";
import booksReducer from "./reducers/books_reducer";
const store = {
  user: null,
  books: null,
  movies: null
};
const obj = {
  ...userReducer,
  ...moviesReducer,
  ...booksReducer
};
function reducer(state, action) {
  const fn = obj[action.type];
  if (fn) {
    return fn(state, action);
  } else {
    throw new Error();
  }
}

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

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

完整代碼:https://codesandbox.io/s/staging-pine-s2k5k

useContext

1.一個簡單的四層函數(shù)參數(shù)傳遞的demo

1.1 原生js寫法

function f1(n1) {
  console.log(n1);
  f2(n1);
}
function f2(n2) {
  console.log(n2);
  f3(n2);
}
function f3(n3) {
  console.log(n3);
  f4(n3);
}
function f4(n4) {
  console.log(n4);
}
function f(n) {
  f1(n);
}
{
  let n = 100;
  f(n);
}

上面的代碼我如果要在每個函數(shù)里拿到n就需要一直把n作為參數(shù)傳遞下去
1.2. react寫法

function F1(props) {
  return (
    <div>
      {props.n1}
      <F2 n2={props.n1} />
    </div>
  );
}
function F2(props) {
  return (
    <div>
      {props.n2}
      <F3 n3={props.n2} />
    </div>
  );
}
function F3(props) {
  return (
    <div>
      {props.n3}
      <F4 n4={props.n3} />
    </div>
  );
}
function F4(props) {
  return <div>{props.n4}</div>;
}
class App extends React.Component {
  constructor() {
    super();
    this.state = {
      n: 100
    };
  }
  render() {
    return (
      <div>
        aaa
        <F1 n1={this.state.n} />
      </div>
    );
  }
}
ReactDOM.render(<App />, document.querySelector("#root"));

現(xiàn)在我們?nèi)绻朐贔4里獲取到state里的n,我們也必須得一層一層通過props傳遞下去昭躺,也就是說即使我們不需要在F2和F3中獲取n我們也得傳下去忌锯,這樣的代碼寫起來就很冗余很復雜

2.代碼改進

2.1. 對原生js代碼改進
(1). 把n作為全局變量,這樣f4就可以直接訪問到n了

let n = 100
function f4() {
  console.log(n) // 100
}

問題:全局變量有可能會被人隨意的修改领炫,所以我們要慎用全局變量
(2). 使用局部全局變量

{
  let context = {};
  window.setContext = (key, value) => {
    context[key] = value;
  };
  window.f1 = () => {
    f2();
  };
  function f2() {
    f3();
  }
  function f3() {
    f4();
  }
  function f4() {
    console.log(context.n);
  }
}
window.setContext("n", 100);
window.f1();

上面的代碼我們的context是一個局部變量偶垮,我們外界獲取不到它,而修改它的唯一方式是通過一個全局的setContext方法修改帝洪,因為我們的f4和context是在同一個作用域所以可以直接獲取到我們的context里面的值

2.2. 對react代碼進行改進

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
function F1(props) {
    return (
        <F2 />
    )
}
function F2(props) {
    return (
        <F3 />
    )
}
function F3(props) {
    return (
        <div>
            <nContext.Consumer>
                {(n) => <F4 n4={n} />}
            </nContext.Consumer>
        </div>
    )
}
function F4(props) {
    return (
        <div>{props.n4}</div>
    )
}
const nContext = React.createContext()
class App extends React.Component {
    render() {
        return (
            <div>
                <nContext.Provider value="999">
                    <F1 />
                </nContext.Provider>
            </div>
        )
    }
}

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

聲明一個React.createContext變量似舵,然后通過它的Provider指定一個value為初始值,需要獲取值的地方通過它的Consumer葱峡,然后標簽里面是一個函數(shù)返回你引用的組件砚哗,然后通過函數(shù)里的參數(shù)可以拿到value的值,之后在對應組件中還是通過props獲取

3.自己寫一個接受函數(shù)的組件砰奕,以便理解<nContext.Consumer>

3.1.

function Consumer(props) {
    // 這里打印出來的是F1函數(shù)
    console.log(props.children)
    return (
        <div>{props.c1}</div>
    )
}
function F1() {
    return 'F1'
}
class App extends React.Component {
    render() {
        return (
            <div>
                <Consumer c1="c1">
                    {F1}
                </Consumer>
            </div>
        )
    }
}

3.2. 我們可以ton過props.children拿到Consumer標簽里的內(nèi)容也就是{F1}蛛芥,所以我們可以直接在Consumer函數(shù)里調(diào)用F1

function Consumer(props) {
    // 調(diào)用標簽里的函數(shù)
    props.children()
    return (
        <div>{props.c1}</div>
    )
}
<Consumer c1="c1">
   {F1}
</Consumer>

3.3. 因為我們的F1實際上就是一個函數(shù)聲明,所以我們可以直接寫成函數(shù)聲明

<Consumer c1="c1">
   {() => console.log('我被調(diào)用了')}
</Consumer>

3.4. 在我們的箭頭函數(shù)聲明里面?zhèn)魅胍粋€參數(shù)

<Consumer>
   {(n) => console.log('我被調(diào)用了', n)}
</Consumer>

我們就需要在調(diào)用的地方傳入一個實參

function Consumer(props) {
    // 調(diào)用標簽里的函數(shù)
    let x = 100
    props.children(x)
    return (
        <div>{props.children}</div>
    )
}

所以我們的n就可以拿到100
3.5. 變成{(n) => <F4 n4={n} />}的形式

function Consumer(props) {
    let x = 100
    let result = props.children(x)
    return (
        <div>{result}</div>
    )
}
function F1() {
    return 'F1'
}
class App extends React.Component {
    render() {
        return (
            <div>
                <Consumer>
                    {(n) => <div>{n}</div>}
                </Consumer>
            </div>
        )
    }
}

上面的props.children(x)返回的是<div>{n}</div>,所以Consumer的返回值也就是<div><div>{n}</div></div>{n}是100军援,所以就等價于

function Consumer(props) {
    return (
        <div>
          <div>100</div>
        </div>
    )
}
4.更改context里的value值

4.1. 組件本身改變

class App extends React.Component {
    constructor() {
        super()
        this.state = {
            n: 100
        }
        setTimeout(() => {
            this.setState({
                n: this.state.n + 10
            })
        }, 2000)
    }
    render() {
        return (
            <div>
                <nContext.Provider value={this.state.n}>
                    <F1 />
                </nContext.Provider>
            </div>
        )
    }
}

4.2. 在其他組件中改變context的value

const nContext = React.createContext()
function F1() {
    return (
        <F2 />
    )
}
function F2() {
    return (
        <F3 />
    )
}
function F3() {
    return (
        <div>
            <nContext.Consumer>
                {x => <F4 n={x.n} setN={x.setN}/>}
            </nContext.Consumer>
        </div>
    )
}
function F4(props) {
    return (
        <div>
            {props.n}
            <button onClick={props.setN}>點我</button>
        </div>
    )
}
class App extends React.Component {
    constructor() {
        super()
        this.state = {
            x: {
                n: 300,
                setN: ()=> {
                    console.log('aaaa')
                    this.setState({
                        x: {
                            n: Math.random()
                        }
                    })
                }
            }
        }
    }
    render() {
        return (
            <div>
                <nContext.Provider value={this.state.x}>
                    <F1 />
                </nContext.Provider>
            </div>
        )
    }
}

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

問題:我們只有第一次點擊按鈕的時候value才會修改仅淑,之后再次點擊value就不會修改了

補充知識:修改state里一個對象的屬性,要把整個對象都重新寫一遍胸哥,然后這個對象里面還要把它之前的屬性都擴展到新的對象里涯竟,而不能直接對象.對應的屬性
比如修改下面的x里面的n,我們就得把整個x對象都重新賦值一遍空厌,然后對象里面還要寫...this.state.x

錯誤寫法:直接修改'x.n'

this.state = {
  x: {
    n: 300,
    setN: () => {
      'x.n': Math.random()
    }
  }
}

正確寫法:

this.state = {
    x: {
        n: 300,
        setN: ()=> {
            this.setState({
                x: {
                     ...this.state.x,
                    n: Math.random()
                }
            })
        }
    }
}
5.總結(jié)

5.1. 使用方法
5.1.1. 使用C = createContext(initial)創(chuàng)建上下文

import React, { createContext, useContext } from "react";
const C = createContext(null);

5.1.2. 使用<C.provider>圈定作用域

function App() {
  const [n, setN] = useState(0);
  return (
    <C.Provider value={{ n, setN }}>
      <div>
        <Father />
      </div>
    </C.Provider>
  );
}

5.1.3. 在作用域內(nèi)使用useContext(C)來使用上下文

function Father() {
  const { n, setN } = useContext(C);
  return (
    <div>
      我是父組件n: {n} <Child />
    </div>
  );
}
function Child() {
  const { n, setN } = useContext(C);
  const onClick = () => {
    setN(i => i + 1);
  };
  return (
    <div>
      我是子組件 我得到的n: {n}
      <button onClick={onClick}>+1</button>
    </div>
  );
}
6.注意事項
  • 不是響應式
    你在一個模塊將C里面的值改變庐船,另一個模塊不會感知到這個變化,每次都是App重新render的過程

useEffect

官方文檔解釋:Effect Hook 可以讓你在函數(shù)組件中執(zhí)行副作用操作

什么叫副作用:
就是一個函數(shù)里依賴的東西不知道是哪里來的嘲更,那么這個未知的東西就有可能改變你函數(shù)的結(jié)果醉鳖,也就是副作用

比如:

function f1(){
  console.log(1)
}
fucntion f2(a, b) {
  return a+ b
}

上面的f1函數(shù)里的console就是一個未知的,當我們執(zhí)行f1函數(shù)的時候會打印出1哮内,但這不是必然的,因為console不是函數(shù)內(nèi)部的東西,所以我們可以修改它

console.log = function(){}
f1()

這時候我們再次執(zhí)行f1就不會打印出1北发,所以我們每次執(zhí)行的結(jié)果都是未知的纹因,也就是所謂的副作用,而f2函數(shù)里所有的代碼都是函數(shù)內(nèi)部的琳拨,不管怎么運行返回的都是你兩個參數(shù)的和

案例:

<div id="output"></div>

- index.js
import { useState, useEffect } from "react";
function App() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({
    name: "lifa",
    age: 18,
    habits: ["小改改", "明星"]
  });
  useEffect(() => {
    document.querySelector("#output").innerText = count;
  });
  const add = () => {
    setCount(count + 1);
  };
}

上面代碼中的#output也是一個未知的不屬于函數(shù)內(nèi)部的瞭恰,所以也是有副作用的,所以我們就可以把它放在useEffect

可以使用生命周期
const App: React.FunctionComponent<Props> = props => {
  const [n, setN] = useState(1);
  const x = () => {
    setN(n + 1);
  };
  useEffect(() => {
    console.log("aaa");
  });
  return (
    <div>
      <h1>{props.message}</h1>
      <div>{n}</div>
      <button onClick={x}>+1</button>
    </div>
  );
};

只要有數(shù)據(jù)更新就會觸發(fā)這個api狱庇,如果我們想針對某一個數(shù)據(jù)的改變才調(diào)用這個api惊畏,那么需要在后面指定一個數(shù)組,數(shù)組里面是需要更新的那個值密任,它變了就會觸發(fā)這個api

useEffect(() => {
    console.log("aaa");
  }, [n]);

只有在n改變的時候才會觸發(fā)

如果我們想只在mounted的時候觸發(fā)一次颜启,那我們需要指定后面的為空數(shù)組,那么就只會觸發(fā)一次浪讳,適合我們做ajax請求

useEffect(() => {
    console.log("mounted");
  }, []);

如果想在組件銷毀之前執(zhí)行缰盏,那么我們就需要在useEffect里return 一個函數(shù)

useEffect(() => {
    console.log("mounted");
    return () => {
      console.log('我死了')
    }
  }, []);

如果在有依賴項的 useEffect 里 return一個函數(shù),那么只有這個依賴項被 setState 了才會執(zhí)行(也就是說return里面的操作只有在依賴項改變了才執(zhí)行淹遵,而return外面的第一次mouted也會執(zhí)行)比如:

const [b, setB] = useState(2);
  const onClick = () => {
    setA(567);
  };
  const onClickB = () => {
    setB(6)
  }
  useEffect(() => {
    console.log('bbb')
    return () => {
      console.log(1);
    };
  }, [b]);
  return (
    <div className="App" onClick={onClick}>
      <h1 onClick={onClickB}>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );

上面的代碼 'bbb' 頁面初始化就會執(zhí)行口猜,而 1 只有 b 變了才會執(zhí)行,所以return里面拿到的是上一次的state里的值透揣,而外面每次拿到的都是最新的state济炎,因為外面初始化的時候多執(zhí)行了一次(就相當于return 里拿到的是每次這個依賴項銷毀前上一次的數(shù)據(jù))

const App = () => {
  const [obj, setObj] = useState({})
  const handleClick = () => {
    setObj({
      a: Math.random()
    })
  }
  useEffect(() => {
    console.log(obj, 'obj')
    return () => {
      console.log(obj, 'return obj')
    }
  }, [obj])
  useEffect(() => {
    setObj({
      a: Math.random()
    })
  }, [])
  return (
    <div onClick={handleClick}>
      {obj.a}
    </div>
  )
}

代替 shouldComponentUpdate
該函數(shù)返回 true 時表示不更新函數(shù),返回 false 則重新更新

function Child(props){
    return <h2>{props.count}</h2>
}
// 模擬shouldComponentUpdate
const areEqual = (prevProps, nextProps) => {
   //比較
};

const PureChild = React.memo(Child, areEqual)
總結(jié)

1.副作用
對環(huán)境的改變即為副租用辐真,如修改document.title须尚,useEffect每次render后運行
2.用途
作為componentDidMount使用,[]作第二個參數(shù)
作為componentDidUpdate使用拆祈,可指定依賴
作為componentWillUnmount使用恨闪,通過return
以上三種用途可同時存在
3.特點
如果同時存在多個useEffect,會按照出現(xiàn)次序執(zhí)行

useLayoutEffect

  1. 布局副作用
    useEffect在瀏覽器渲染完成后執(zhí)行放坏,而useLayoutEffect在瀏覽器渲染前執(zhí)行
    案例:
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")
);

上面的代碼在我們頁面刷新或者打開的時候會閃一下(白屏)咙咽,主要原因與我們一個組件的渲染過程有關(guān),如下圖

首先調(diào)用App淤年,然后執(zhí)行構(gòu)造一個對應的虛擬DOM(VDOM)钧敞,之后將虛擬DOM渲染到DOM里,然后加到頁面中麸粮,頁面改變溉苛,最后才會執(zhí)行useEffect,而實際上我們最初的值0已經(jīng)掛載到了頁面上弄诲,這時候再在useEffect中修改就會出現(xiàn)二次更新頁面白屏的情況愚战,而useLayoutEffect是在DOM元素還未掛載到頁面中的時候就執(zhí)行了娇唯,所以它初次展現(xiàn)在頁面中就是1000,而不是0寂玲,也就不會有白屏現(xiàn)象

  1. 特點

useLayoutEffect總是比useEffect先執(zhí)行塔插,為了用戶體驗,優(yōu)先使用useEffect(優(yōu)先渲染)

useMemo

memo

問題1:當我們引用一個組件的時候如果這個組件依賴的屬性沒變拓哟,我們不希望這個組件去重新渲染想许,但是實際上只要是頁面上有任何數(shù)據(jù)變化了當前頁面上的組件就都會重新渲染,比如:

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

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

我們點擊按鈕n被修改了断序,但是我們的Child組件并沒有依賴于n流纹,而是m,可是n變了违诗,Child也重新執(zhí)行了漱凝,解決辦法:對不需要每次更新的組件使用memo

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

這樣我們在修改m之外的屬性都不會重新執(zhí)行我們的Child了

問題2:如果我們在上面的Child組件添加一個監(jiān)聽函數(shù),那么當我們點擊按鈕更新
n后较雕,Child組件又會重新執(zhí)行

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

  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <Child2 data={m} onClick={onClickChild} />
      {/* Child2 居然又執(zhí)行了 */}
    </div>
  );
}

function Child(props) {
  console.log("child 執(zhí)行了");
  console.log("假設這里有大量代碼");
  return <div onClick={props.onClick}>child: {props.data}</div>;
}

const Child2 = React.memo(Child);

原因是我們更新了n碉哑,App就會重新渲染,然后onClickChild每次都會生成功能相同的一個新的引用地址的函數(shù)亮蒋,所以Child就會認為onClick對應的屬性函數(shù)變了扣典,就會重新更新。
解決方法:使用useMemo

const onClickChild = useMemo(() => {}, [m])

這樣就只有m改變的時候才會重新渲染Child

總結(jié)

特點:
第一個參數(shù)是()=>value
第二個參數(shù)是依賴[m,n]
只有當依賴變化時慎玖,才會計算出新的value
如果依賴不變贮尖,那么久重用之前的value
注意:
如果你的value是個函數(shù),那么你就要寫成useMemo(()=>(x)=>console.log(x))趁怔,這是一個返回函數(shù)的函數(shù)湿硝,我們也可以使用usecallback簡寫

useCallback

用法:

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

useCallback 使用場景:有一個父組件,其中包含子組件润努,子組件接收一個函數(shù)作為props关斜;通常而言,如果父組件更新了铺浇,子組件也會執(zhí)行更新痢畜;但是大多數(shù)場景下,更新是沒有必要的鳍侣,我們可以借助useCallback來返回函數(shù)丁稀,然后把這個函數(shù)作為props傳遞給子組件;這樣倚聚,子組件就能避免不必要的更新线衫。

import React, { useState, useCallback, useEffect } from 'react';
function Parent() {
    const [count, setCount] = useState(1);
    const [val, setVal] = useState('');
 
    const callback = useCallback(() => {
        return count;
    }, [count]);
    return <div>
        <h4>{count}</h4>
        <Child callback={callback}/>
        <div>
            <button onClick={() => setCount(count + 1)}>+</button>
            <input value={val} onChange={event => setVal(event.target.value)}/>
        </div>
    </div>;
}
 
function Child({ callback }) {
    const [count, setCount] = useState(() => callback());
    useEffect(() => {
        setCount(callback());
    }, [callback]);
    return <div>
        {count}
    </div>
}

為什么要用 useMemo 和 useCallback?什么時候需要用惑折?
原因:因為我們每次 setState 的時候組件都會重新 render授账,對于 hooks 來說除了寫在 useEffect 里的方法不會重新聲明和額外執(zhí)行外枯跑,寫在useEffect 外的代碼隨著組件重新render都會從上而下把組件里的代碼重新運行一遍,對象和方法就會生成一個新的引用地址矗积,這個時候如果我們需要把useEffect 外聲明的對象和方法傳遞給其他組件的話全肮,那么其他組件使用的這個對象和方法的屬性就會一直改變,就會帶來不必要的 render棘捣,所以如果我們要把一個把一個屬性方法傳遞給其他組件的話,一定要使用useMemo 和 useCallback

useMemo 和 useEffect 依賴項不變的情況下會緩存之前的值和方法

  • 在子組件不需要父組件的值和方法的情況下休建,只需要使用 memo 函數(shù)包裹子組件即可乍恐。

  • 如果有方法傳遞給子組件,使用 useCallback

  • 如果有值傳遞給子組件测砂,使用 useMemo

  • useEffect茵烈、useMemo、useCallback 都是自帶閉包的砌些。也就是說呜投,每一次組件的渲染,其都會捕獲當前組件函數(shù)上下文中的狀態(tài)(state, props)存璃,所以每一次這三種hooks的執(zhí)行仑荐,反映的也都是當前的狀態(tài),你無法使用它們來捕獲上一次的狀態(tài)纵东。對于這種情況粘招,我們應該使用 ref 來訪問。

useRef

目的:如果你需要一個值偎球,在組件不斷render的過程中保持不變(永遠都是同一個n洒扎,而不是說值不變)那么你就需要使用useRef
初始化:const count = useRef(0)
讀取:count.current
問題:問什么需要count.current來讀取值而不能直接count哪衰絮?
答:為了保證每次useRef是同一個引用地址袍冷,假設我們的count是一個對象,初始的時候是useRef({x:1})猫牡,然后你修改它就得count = {x:2}胡诗,這樣就會生成一個新的對象,就沒法保證每次都是同一個了镊掖,而如果是count = useRef({current: {x:1}})那么你每次修改都得count.current它的引用地址就不會變
useState/useReducer --> n每次都會變(都是不同的變量n)
useMemo/useCallback --> 只有依賴的[m]變的時候乃戈,fn才會變
useRef --> 永遠不變

與vue3的ref相比

初始化:const count = ref(0)
讀取:count.value
不同點:當count.value變化時亩进,Vue3會自動render

forwardRef

如果我們使用的是函數(shù)組件症虑,我們想在組件里獲取到外界傳來的ref的話,那么我們直接通過props來獲取就會報錯

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

const Button2 = props => {
  return <button className="red">{props.ref}</button>;
};

所以我們需要使用forwardRef

function App() {
  const buttonRef = useRef(null);
  return (
    <div className="App">
      <Button3 ref={buttonRef}>按鈕</Button3>
    </div>
  );
}

const Button3 = React.forwardRef((props, ref) => {
  console.log(ref);
  return (
    <button className="red" ref={ref} {...props}>
      {props.children}
    </button>
  );
});
useRef與forwardRef的比較
  • useRef
    可以用來引用DOM對象归薛,也可以用來引用普通對象
  • forwardRef
    由于props不包含ref谍憔,所以需要forwardRef

useImperativeHandle

應該叫setRef匪蝙,用于自定義ref的屬性
不用useImperativeHandle的代碼
https://codesandbox.io/s/awesome-goldwasser-v7vsp

使用useImperativeHandle的代碼
https://codesandbox.io/s/elegant-poitras-mxoym

自定義Hook

  1. 封裝數(shù)據(jù)操作
    簡單例子:
    https://codesandbox.io/s/wizardly-tesla-sy077
  2. 復雜案例
    https://codesandbox.io/s/jovial-villani-v0xue

過時閉包

function createIncrement(i) {
  let value = 0;
  function increment() {
    value += i;
    console.log(value);
    const message = `Current value is ${value}`;
    return function logValue() {
      console.log(message);
    };
  }
  
  return increment;
}

const inc = createIncrement(1);
const log = inc(); // logs 1
inc();             // logs 2
inc();             // logs 3
// Does not work!
log();             // logs "Current value is 1"

上面代碼我們的vlaue已經(jīng)變成了3了,可我們的message打印出來還是1习贫,這就是因為我們調(diào)用log()的時候逛球,實際上保留了第一次的值;
解決辦法
1.每次log調(diào)用的時候都重新取上一次的log

const inc = createIncrement(1);

inc();  // logs 1
inc();  // logs 2
const latestLog = inc(); // logs 3
// Works!
latestLog(); // logs "Current value is 3"
  1. 如果用舊的log那么你每次都要去讀新的value苫昌,也就是把message放到最內(nèi)層
function createIncrementFixed(i) {
  let value = 0;
  function increment() {
    value += i;
    console.log(value);
    return function logValue() {
      const message = `Current value is ${value}`;
      console.log(message);
    };
  }
  
  return increment;
}

const inc = createIncrementFixed(1);
const log = inc(); // logs 1
inc();             // logs 2
inc();             // logs 3
// Works!
log();             // logs "Current value is 3"

參考文章:https://dmitripavlutin.com/react-hooks-stale-closures/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末颤绕,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子祟身,更是在濱河造成了極大的恐慌奥务,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件袜硫,死亡現(xiàn)場離奇詭異氯葬,居然都是意外死亡,警方通過查閱死者的電腦和手機婉陷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門帚称,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人秽澳,你說我怎么就攤上這事闯睹。” “怎么了肝集?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵瞻坝,是天一觀的道長。 經(jīng)常有香客問我杏瞻,道長所刀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任捞挥,我火速辦了婚禮浮创,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘砌函。我一直安慰自己斩披,他們只是感情好,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布讹俊。 她就那樣靜靜地躺著垦沉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪仍劈。 梳的紋絲不亂的頭發(fā)上厕倍,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音贩疙,去河邊找鬼讹弯。 笑死况既,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的组民。 我是一名探鬼主播棒仍,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼臭胜!你這毒婦竟也來了莫其?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤耸三,失蹤者是張志新(化名)和其女友劉穎榜配,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吕晌,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年临燃,在試婚紗的時候發(fā)現(xiàn)自己被綠了睛驳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡膜廊,死狀恐怖乏沸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情爪瓜,我是刑警寧澤蹬跃,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站铆铆,受9級特大地震影響蝶缀,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜薄货,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一翁都、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧谅猾,春花似錦柄慰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至敬矩,卻和暖如春概行,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背谤绳。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工占锯, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留袒哥,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓消略,卻偏偏與公主長得像堡称,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子艺演,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345