React Hooks详解

useState

1.基本使用
import { useState } from "react";
function App() {
  // 数组里的第一项是sate里的变量,第二项是修改state的函数
  // 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.使用状态
const [n,setN] = React.useState(0)
const [user, setUser] = React.useState({name: 'F'})
4. 注意事项

1). 如果state是一个对象,我们不能对对象里的部分属性setState,需要我们每次都把之前的属性全部重新结构一遍,然后下面再写你要修改的属性

// 错误代码
const [user,setUser] = useState({name:'lifa', age: 18})
const onClick = ()=>{
    setUser({
      name: 'Jack'
    })
  }

//正确代码
setUser({
   ...user,
   name: 'Jack'
 })

2). 地址要变
setState(obj)如果obj地址不变,那么React就认为数据没有变化

// 错误代码
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只能放在函数组件内部,不能单独拿出来

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

该函数返回初始state, 且只执行一次

6. setState可以接受函数

我们如果要多次对useState进行操作的话推荐使用函数
以两次修改useState对其进行加一操作为例

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

上面我们在点击事件里执行了两次修改n,每次让他加一,可实际上他只会变一次,因为n本身是不会变的,而是每次生成一个新的n,所以上面结果是2而不是3,如果想要它加2的话就要用函数

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

上面的i是一个占位符,随便什么都可以,就是我们传一个值给setN,每次返回当前的值+1,所以最后会加2,得到的结果是3

往数组 push 一条数据

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. 创建初始值initialState
const intialState = {
  n: 0
}

2.创建所有操作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.调用写({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). 将数据集中在一个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). 创建Context

const Context = createContext(null

4). 创建对数据的读写API

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

5). 将第4步的内容放到第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每次执行的时候都会修改state,state一修改就会重新调一下User,User重新调了又会重新请求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.一个简单的四层函数参数传递的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);
}

上面的代码我如果要在每个函数里拿到n就需要一直把n作为参数传递下去
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"));

现在我们如果想在F4里获取到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,然后标签里面是一个函数返回你引用的组件,然后通过函数里的参数可以拿到value的值,之后在对应组件中还是通过props获取

3.自己写一个接受函数的组件,以便理解<nContext.Consumer>

3.1.

function Consumer(props) {
    // 这里打印出来的是F1函数
    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标签里的内容也就是{F1},所以我们可以直接在Consumer函数里调用F1

function Consumer(props) {
    // 调用标签里的函数
    props.children()
    return (
        <div>{props.c1}</div>
    )
}
<Consumer c1="c1">
   {F1}
</Consumer>

3.3. 因为我们的F1实际上就是一个函数声明,所以我们可以直接写成函数声明

<Consumer c1="c1">
   {() => console.log('我被调用了')}
</Consumer>

3.4. 在我们的箭头函数声明里面传入一个参数

<Consumer>
   {(n) => console.log('我被调用了', n)}
</Consumer>

我们就需要在调用的地方传入一个实参

function Consumer(props) {
    // 调用标签里的函数
    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.总结

5.1. 使用方法
5.1.1. 使用C = createContext(initial)创建上下文

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. 在作用域内使用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 可以让你在函数组件中执行副作用操作

什么叫副作用:
就是一个函数里依赖的东西不知道是哪里来的,那么这个未知的东西就有可能改变你函数的结果,也就是副作用

比如:

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

上面的f1函数里的console就是一个未知的,当我们执行f1函数的时候会打印出1,但这不是必然的,因为console不是函数内部的东西,所以我们可以修改它

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

这时候我们再次执行f1就不会打印出1,所以我们每次执行的结果都是未知的,也就是所谓的副作用,而f2函数里所有的代码都是函数内部的,不管怎么运行返回的都是你两个参数的和

案例:

<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也是一个未知的不属于函数内部的,所以也是有副作用的,所以我们就可以把它放在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>
  );
};

只要有数据更新就会触发这个api,如果我们想针对某一个数据的改变才调用这个api,那么需要在后面指定一个数组,数组里面是需要更新的那个值,它变了就会触发这个api

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

只有在n改变的时候才会触发

如果我们想只在mounted的时候触发一次,那我们需要指定后面的为空数组,那么就只会触发一次,适合我们做ajax请求

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

如果想在组件销毁之前执行,那么我们就需要在useEffect里return 一个函数

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

如果在有依赖项的 useEffect 里 return一个函数,那么只有这个依赖项被 setState 了才会执行(也就是说return里面的操作只有在依赖项改变了才执行,而return外面的第一次mouted也会执行)比如:

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' 页面初始化就会执行,而 1 只有 b 变了才会执行,所以return里面拿到的是上一次的state里的值,而外面每次拿到的都是最新的state,因为外面初始化的时候多执行了一次(就相当于return 里拿到的是每次这个依赖项销毁前上一次的数据)

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
该函数返回 true 时表示不更新函数,返回 false 则重新更新

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

const PureChild = React.memo(Child, areEqual)
总结

1.副作用
对环境的改变即为副租用,如修改document.title,useEffect每次render后运行
2.用途
作为componentDidMount使用,[]作第二个参数
作为componentDidUpdate使用,可指定依赖
作为componentWillUnmount使用,通过return
以上三种用途可同时存在
3.特点
如果同时存在多个useEffect,会按照出现次序执行

useLayoutEffect

  1. 布局副作用
    useEffect在浏览器渲染完成后执行,而useLayoutEffect在浏览器渲染前执行
    案例:
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")
);

上面的代码在我们页面刷新或者打开的时候会闪一下(白屏),主要原因与我们一个组件的渲染过程有关,如下图

首先调用App,然后执行构造一个对应的虚拟DOM(VDOM),之后将虚拟DOM渲染到DOM里,然后加到页面中,页面改变,最后才会执行useEffect,而实际上我们最初的值0已经挂载到了页面上,这时候再在useEffect中修改就会出现二次更新页面白屏的情况,而useLayoutEffect是在DOM元素还未挂载到页面中的时候就执行了,所以它初次展现在页面中就是1000,而不是0,也就不会有白屏现象

  1. 特点

useLayoutEffect总是比useEffect先执行,为了用户体验,优先使用useEffect(优先渲染)

useMemo

memo

问题1:当我们引用一个组件的时候如果这个组件依赖的属性没变,我们不希望这个组件去重新渲染,但是实际上只要是页面上有任何数据变化了当前页面上的组件就都会重新渲染,比如:

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 执行了");
  console.log('假设这里有大量代码')
  return <div>child: {props.data}</div>;
}

我们点击按钮n被修改了,但是我们的Child组件并没有依赖于n,而是m,可是n变了,Child也重新执行了,解决办法:对不需要每次更新的组件使用memo

const Child = React.memo(props => {
  console.log("child 执行了");
  console.log("假设这里有大量代码");
  return <div>child: {props.data}</div>;
});

这样我们在修改m之外的属性都不会重新执行我们的Child了

问题2:如果我们在上面的Child组件添加一个监听函数,那么当我们点击按钮更新
n后,Child组件又会重新执行

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 居然又执行了 */}
    </div>
  );
}

function Child(props) {
  console.log("child 执行了");
  console.log("假设这里有大量代码");
  return <div onClick={props.onClick}>child: {props.data}</div>;
}

const Child2 = React.memo(Child);

原因是我们更新了n,App就会重新渲染,然后onClickChild每次都会生成功能相同的一个新的引用地址的函数,所以Child就会认为onClick对应的属性函数变了,就会重新更新。
解决方法:使用useMemo

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

这样就只有m改变的时候才会重新渲染Child

总结

特点:
第一个参数是()=>value
第二个参数是依赖[m,n]
只有当依赖变化时,才会计算出新的value
如果依赖不变,那么久重用之前的value
注意:
如果你的value是个函数,那么你就要写成useMemo(()=>(x)=>console.log(x)),这是一个返回函数的函数,我们也可以使用usecallback简写

useCallback

用法:

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

useCallback 使用场景:有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为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 里的方法不会重新声明和额外执行外,写在useEffect 外的代码随着组件重新render都会从上而下把组件里的代码重新运行一遍,对象和方法就会生成一个新的引用地址,这个时候如果我们需要把useEffect 外声明的对象和方法传递给其他组件的话,那么其他组件使用的这个对象和方法的属性就会一直改变,就会带来不必要的 render,所以如果我们要把一个把一个属性方法传递给其他组件的话,一定要使用useMemo 和 useCallback

useMemo 和 useEffect 依赖项不变的情况下会缓存之前的值和方法

  • 在子组件不需要父组件的值和方法的情况下,只需要使用 memo 函数包裹子组件即可。

  • 如果有方法传递给子组件,使用 useCallback

  • 如果有值传递给子组件,使用 useMemo

  • useEffect、useMemo、useCallback 都是自带闭包的。也就是说,每一次组件的渲染,其都会捕获当前组件函数上下文中的状态(state, props),所以每一次这三种hooks的执行,反映的也都是当前的状态,你无法使用它们来捕获上一次的状态。对于这种情况,我们应该使用 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

如果我们使用的是函数组件,我们想在组件里获取到外界传来的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. 封装数据操作
    简单例子:
    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已经变成了3了,可我们的message打印出来还是1,这就是因为我们调用log()的时候,实际上保留了第一次的值;
解决办法
1.每次log调用的时候都重新取上一次的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放到最内层
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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271