react总结

关于hooks的系统性整理

你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。

why use hooks?

  1. 在组件之间复用状态逻辑很难
  2. 复杂组件变得难以理解
  3. 难以理解的 class

react-hooks思想更趋近于函数式编程。用函数声明方式代替class声明方式,虽说class也是es6构造函数语法糖,但是react-hooks写起来函数即是组件,无疑也提高代码的开发效率(无需像class声明组件那样写声明周期,写生命周期render函数等)

Hook-Rules

只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用 Hook
只在 React 函数中调用 Hook

  1. 确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。
  2. 不要在普通的 JavaScript 函数中调用 Hook
    react官网的规则说明

常用hooks说明

useState

const [state, setState] = useState(initialState)

import React,{useState} from "react";
function Example() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}
export default Example;

第一行: 引入 React 中的 useState Hook。它让我们在函数组件中存储内部 state。
第三行: 在 Example 组件内部,我们通过调用 useState Hook 声明了一个新的 state 变量。它返回一对值给到我们命名的变量上。我们把变量命名为 count,因为它存储的是点击次数。我们通过传 0 作为 useState 唯一的参数来将其初始化为 0。第二个返回的值本身就是一个函数。它让我们可以更新 count 的值,所以我们叫它 setCount。
第七行: 当用户点击按钮后,我们传递一个新的值给 setCount。React 会重新渲染 Example 组件,并把最新的 count 传给它。

//更新state值-->useState()
<button onClick={() => setCount(count=>count+1)}>Click me</button>
<button onClick={() => setPerson({name:'chimmy'})}>Click me</button>
//setPerson更新person时,不像 class 中的 this.setState,更新 state 变量总是替换它而不是合并它。上例中的person为{name:'chimmy'} 而不是{name:'chimmy',age:22}

useEffect

可以让你在函数组件中执行副作用(数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用)操作 >>>>useEffect(fn, array)

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount``componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

useEffect _componentDidMount(初次渲染)

//将第二参数置成空数组
import React, { useState, useEffect } from "react";
function Example() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log("我只会在组件初次挂载完成后执行");
  }, []);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}
export default Example;

useEffect _componentDidUpdate(初次渲染以及更新渲染)

//如果不传第二个参数,useEffect 会在初次渲染和每次更新时,都会执行。
import React, { useState, useEffect } from "react";
function Example() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log("我会在初次组件挂载完成后以及重新渲染时执行");
  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}
export default Example;

useEffect _componentWillUnmount(清除操作)

//effect 返回一个函数,React 将会在执行清除操作时调用它。
useEffect(() => {
    console.log("订阅一些事件");
    return () => {
      console.log("执行清除操作")
    }
  },[]);

注意:这里不只是组件销毁时才会打印“执行清除操作”,每次重新渲染时也都会执行。至于原因,我觉得官网解释的很清楚,请参考 解释: 为什么每次更新的时候都要运行 Effect

useEffect _async/await(异步执行)

如果你明白了useEffect的清除执行机制,那么面对异步操作:

const App = () => {
  useEffect(() => {
    (async function getDatas() {
      await getData();
    })();
  }, []);
  return <div></div>;
};

总结:What’s useEffect to do?

通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。
将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

useContext

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。
别忘记 useContext 的参数必须是 context 对象本身:

  • 🙆 useContext(MyContext)
  • 🙅useContext(MyContext.Consumer/provider)
//index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
// 创建两个context
export const UserContext = React.createContext();
export const TokenContext = React.createContext();
ReactDOM.render(
  <UserContext.Provider value={{ id: 1, name: "chimmy", age: "20" }}>
    <TokenContext.Provider value="我是token">
      <App />
    </TokenContext.Provider>
  </UserContext.Provider>,
  document.getElementById("root")
);


//app.js
import React, { useContext } from "react";
import { UserContext, TokenContext } from "./index";

function Example() {
  let user = useContext(UserContext);
  let token = useContext(TokenContext);
  console.log("UserContext", user);
  console.log("TokenContext", token);
  return (
    <div>
      name:{user?.name},age:{user?.age}
    </div>
  );
}
export default Example;

//

Tips:
如果你在接触 Hook 前已经对 context API 比较熟悉,那应该可以理解,useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>。
useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数

import React, { useReducer } from "react";
export default function Home() {
  function reducer(state, action) {
    switch (action.type) {
      case "increment":
        return { ...state, counter: state.counter + 1 };
      case "decrement":
        return { ...state, counter: state.counter - 1 };
      default:
        return state;
    }
  }
  const [state, dispatch] = useReducer(reducer, { counter: 0 });
  return (
    <div>
      <h2>Home当前计数: {state.counter}</h2>
      <button onClick={(e) => dispatch({ type: "increment" })}>+1</button>
      <button onClick={(e) => dispatch({ type: "decrement" })}>-1</button>
    </div>
  );

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
import React, { useState } from "react";
// 子组件
function Childs(props) {
  console.log("子组件渲染了");
  return (
    <>
      <button onClick={props.onClick}>改标题</button>
      <h1>{props.name}</h1>
    </>
  );
}
const Child = React.memo(Childs);
function App() {
  const [title, setTitle] = useState("这是一个 title");
  const [subtitle, setSubtitle] = useState("我是一个副标题");
  const callback = () => {
    setTitle("标题改变了");
  };
  return (
    <div className="App">
      <h1>{title}</h1>
      <h2>{subtitle}</h2>
      <button onClick={() => setSubtitle("副标题改变了")}>改副标题</button>
      <Child onClick={callback} name="成成" />
    </div>
  );
}

当我点击改副标题这个 button 之后,副标题会变为「副标题改变了」,并且控制台会再次打印出子组件渲染了,这就证明了子组件重新渲染了,但是子组件没有任何变化,那么这次 Child 组件的重新渲染就是多余的,那么如何避免掉这个多余的渲染呢?

codeReview

我们在解决问题的之前,首先要知道这个问题是什么原因导致的?
咱们来分析,一个组件重新重新渲染,一般三种情况:

  1. 要么是组件自己的状态改变
  2. 要么是父组件重新渲染,导致子组件重新渲染,但是父组件的 props 没有改变
  3. 要么是父组件重新渲染,导致子组件重新渲染,但是父组件传递的 props 改变

接下来用排除法查出是什么原因导致的:
第一种很明显就排除了,当点击改副标题 的时候并没有去改变 Child 组件的状态;
第二种情况,我们这个时候用 React.memo 来解决了这个问题,所以这种情况也排除。

那么当父组件重新渲染的时候,传递给子组件的 props 发生了改变,再看传递给 Child 组件的就两个属性,一个是 name,一个是 onClick ,name 是传递的常量,不会变,变的就是 onClick 了,为什么传递给 onClick 的 callback 函数会发生改变呢?其实在函数式组件里每次重新渲染,函数组件都会重头开始重新执行,那么这两次创建的 callback 函数肯定发生了改变,所以导致了子组件重新渲染。

useCallback-answer

const callback = () => { setTitle("标题改变了"); };
// 通过 useCallback 进行记忆 callback,并将记忆的 callback 传递给 Child
<Child onClick={useCallback(callback, [])} name="桃桃" />

这样我们就可以看到只会在首次渲染的时候打印出子组件渲染了,当点击改副标题和改标题的时候是不会打印子组件渲染了的。

useMemo

const cacheSomething = useMemo(create,deps)
  • 第一个参数为一个函数,函数的返回值作为缓存值
  • 第二个参数为一个数组,存放当前 useMemo 的依赖项,在函数组件下一次执行的时候,会对比 deps 依赖项里面的状态,是否有改变,如果有改变重新执行 create ,得到新的缓存值。
  • 返回值,执行 create 的返回值。如果 deps 中有依赖项改变,返回的重新执行 create 产生的值,否则取上一次缓存值。

useMemo 会记录上一次执行 create 的返回值,并把它绑定在函数组件对应的 fiber 对象上,只要组件不销毁,缓存值就一直存在,但是 deps 中如果有一项改变,就会重新执行 create ,返回值作为新的值记录到 fiber 对象上。

//应用场景一:随着父组件重新渲染产生的userInfo每一次都是新值
function Child(){
    console.log("子组件渲染了")
    return <div>Child</div> 
}
function APP(){
    const [count, setCount] = useState(0);
    const userInfo = useMemo(() => {
      return {
        name: "jimmy",
        age: count
      };
    }, [count]);
    return <Child userInfo={userInfo}>
}
import React, {useState, useMemo} from 'react';

// 应用场景二:计算和的函数,开销较大
function calcNumber(count) {
  console.log("calcNumber重新计算");
  let total = 0;
  for (let i = 1; i <= count; i++) {
    total += i;
  }
  return total;
}
export default function MemoHookDemo01() {
  const [count, setCount] = useState(100000);
  const [show, setShow] = useState(true);
  const total = useMemo(() => {
    return calcNumber(count);
  }, [count]);
  return (
    <div>
      <h2>计算数字的和: {total}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
      <button onClick={e => setShow(!show)}>show切换</button>
    </div>
  )
}

总结-useMemo/useCallback:

useCallback 与 useMemo 一个缓存的是函数,一个缓存的是函数的返回的结果。useCallback 是来优化子组件的,防止子组件的重复渲染。useMemo 可以优化当前组件也可以优化子组件,优化当前组件主要是通过 memoize 来将一些复杂的计算逻辑进行缓存。当然如果只是进行一些简单的计算也没必要使用 useMemo。
我们可以将 useMemo 的返回值定义为返回一个函数这样就可以变通的实现了 useCallback。useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

useRef

  • useRef 获取dom
  • useRef 缓存数据

dom获取:

import React, { useRef } from "react";
function Example() {
  const divRef = useRef();
  function changeDOM() {
    // 获取整个div
    console.log("整个div", divRef.current);
    // 获取div的class
    console.log("div的class", divRef.current.className);
    // 获取div自定义属性
    console.log("div自定义属性", divRef.current.getAttribute("data-clj"));
  }
  return (
    <div>
      <div className="div-class" data-clj="我是div的自定义属性" ref={divRef}>
        我是div
      </div>
      <button onClick={(e) => changeDOM()}>获取DOM</button>
    </div>
  );
}
export default Example;

useRef 缓存数据

useRef还有一个很重要的作用就是缓存数据,我们知道usestate ,useReducer 是可以保存当前的数据源的,但是如果它们更新数据源的函数执行必定会带来整个组件从新执行到渲染,如果在函数组件内部声明变量,则下一次更新也会重置,如果我们想要悄悄的保存数据,而又不想触发函数的更新,那么useRef是一个很棒的选择。

import React, { useRef, useState, useEffect } from "react";
function Example() {
  const [count, setCount] = useState(0);

  const numRef = useRef(count);

  useEffect(() => {
    numRef.current = count;
  }, [count]);

  return (
    <div>
      <h2>count上一次的值: {numRef.current}</h2>
      <h2>count这一次的值: {count}</h2>
      <button onClick={(e) => setCount(count + 10)}>+10</button>
    </div>
  );
}
export default Example;
//当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。所以,上面的例子中虽然numRef.current的值,已经改变了,但是页面上还是显示的上一次的值,重新更新时,才会显示上一次更新的值
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值