React Hooks:功能强大的新工具

由于掘金字数限制续上上一篇文章。 地址("React学习笔记:从新手到专家的成长历程" - 掘金 (juejin.cn))

23.Hooks

  • 总结一下 hooks:
    • 它可以让我们在不编写 class 的情况下使用 state 以及其他的 React 特性;
    • 但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决
  • Hook 的使用场景:
  • Hook 的出现基本可以代替我们之前所有使用 class 组件的地方;
  • 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为 Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
  • Hook 只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用
  • 请记住 Hook 是:
    • 完全可选的:你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。
    • 100% 向后兼容的:Hook 不包含任何破坏性改动。
    • 现在可用:Hook 已发布于 v16.8.0。

23.1 useState Hook

  • 参数:初始化值,如果不设置为 undefined;

  • 返回值:数组,包含两个元素;

    • 元素一:当前状态的值(第一调用为初始化值);
    • 元素二:设置状态值的函数;
  • useState 的返回值是一个数组,我们可以通过数组的解构,来完成赋值会非常方便

  • 也可以在一个组件中定义多个变量和复杂变量(数组、对象)

  • 使用它们会有两个额外的规则:

    • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
    • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。
jsx 复制代码
import React, { memo, useState } from "react";

const App = memo(() => {
  const [message, setMessage] = useState("zimo");

  function changeMessage() {
    setMessage("紫陌");
  }
  return (
    <div>
      <h2>App:{message}</h2>
      <button onClick={changeMessage}>修改文本</button>
    </div>
  );
});

export default App;

23.1 useEffect Hook

  • useEffect Hook 可以让你来完成一些类似于 class 中生命周期的功能;
  • 类似于网络请求、手动更新 DOM、一些事件的监听,都是 React 更新 DOM 的一些副作用(Side Effects);
  • 所以对于完成这些功能的 Hook 被称之为 Effect Hook;
  1. useEffect 的解析
  • 通过 useEffect 的 Hook,可以告诉 React 需要在渲染后执行某些操作;
  • useEffect 要求我们传入一个回调函数,在React 执行完更新 DOM 操作之后,就会回调这个函数;
  • 默认情况下,**无论是第一次渲染之后,**还是每次更新之后,都会执行这个 回调函数
  1. 需要清除 Effect
  • 在 class 组件的编写过程中,某些副作用的代码,我们需要在 componentWillUnmount 中进行清除:
  • 比如我们之前的事件总线或 Redux 中手动调用 subscribe;
  • 都需要在 componentWillUnmount 有对应的取消订阅;

为什么要在 Effect 中返回一个函数?

  • 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数;
  • 如此可以将添加和移除订阅的逻辑放在一起;
  • 它们都属于 effect 的一部分;

React 何时清除 effect?

  • React 会在组件更新和卸载的时候执行清除操作;
  • Effect 在每次渲染的时候都会执行;
jsx 复制代码
import React, { memo, useEffect, useState } from "react";

const App = memo(() => {
  const [count, setCount] = useState(0);

  // 负责告知React,在执行完当前组件渲染之后要执行的副作用代码
  useEffect(() => {
    console.log("监听redux中数据变化, 监听eventBus中的zimo事件");

    // 返回值:回调函数 => 组件被重新渲染或者组件卸载的时候执行
    return () => {
      console.log("取消监听redux中数据变化, 取消监听eventBus中的zimo事件");
    };
  });

  return (
    <div>
      <button onClick={(e) => setCount(count + 1)}>+1({count})</button>
    </div>
  );
});

export default App;
  1. 多个 Effect
  • 使用 Effect Hook,我们可以将它们分离到不同的 useEffect 中:
  • React 将按照 effect 声明的顺序依次调用组件中的每一个 effect
jsx 复制代码
import React, { memo, useEffect } from "react";
import { useState } from "react";

const App = memo(() => {
  const [count, setCount] = useState(0);

  // 负责告知react, 在执行完当前组件渲染之后要执行的副作用代码
  useEffect(() => {
    // 1.修改document的title(1行)
    console.log("修改title");
  });

  // 一个函数式组件中, 可以存在多个useEffect
  useEffect(() => {
    // 2.对redux中数据变化监听(10行)
    console.log("监听redux中的数据");
    return () => {
      // 取消redux中数据的监听
    };
  });

  useEffect(() => {
    // 3.监听eventBus中的why事件(15行)
    console.log("监听eventBus的zimo事件");
    return () => {
      // 取消eventBus中的zimo事件监听
    };
  });

  return (
    <div>
      <button onClick={(e) => setCount(count + 1)}>+1({count})</button>
    </div>
  );
});

export default App;
  1. Effect 性能优化
  • 默认情况下,useEffect 的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:
  • 某些代码我们只是希望执行一次即可,类似于 componentDidMount 和 componentWillUnmount 中完成的事情;(比如网 络请求、订阅和取消订阅);
  • 多次执行也会导致一定的性能问题;

useEffect实际上有两个参数:

  • 参数一:执行的回调函数
  • 参数二:该 useEffect 在哪些 state 发生变化时,才重新执行;(受谁的影响)

如果一个函数我们不希望依赖任何的内容时,也可以传入一个空的数组 \[\]:

  • 这里的两个回调函数分别对应的就是 componentDidMount 和 componentWillUnmount 生命周期函数了;
jsx 复制代码
import React, { memo, useEffect } from "react";
import { useState } from "react";

const App = memo(() => {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState("Hello World");

  useEffect(() => {
    console.log("修改title:", count);
  }, [count]);

  useEffect(() => {
    console.log("监听redux中的数据");
    return () => {};
  }, []);

  useEffect(() => {
    console.log("监听eventBus的why事件");
    return () => {};
  }, []);

  useEffect(() => {
    console.log("发送网络请求, 从服务器获取数据");

    return () => {
      console.log("会在组件被卸载时, 才会执行一次");
    };
  }, []);

  return (
    <div>
      <button onClick={(e) => setCount(count + 1)}>+1({count})</button>
      <button onClick={(e) => setMessage("你好啊")}>修改message({message})</button>
    </div>
  );
});

export default App;

23.3 useContext

在组件中使用共享的 Context 有两种方式

  1. 类组件可以通过 类名.contextType = MyContext 方式,在类中获取 context;
  2. 多个 Context 或者在函数式组件中通过 MyContext.Consumer 方式共享 context;

但是多个 Context 共享时的方式会存在大量的嵌套:

  • Context Hook 允许我们通过 Hook 来直接获取某个 Context 的值;
jsx 复制代码
index.js-------------------------------------------------------------------------------

root.render(
  <UserContext.Provider value={{name: "zimo", level: 99}}>
    <ThemeContext.Provider value={{color:"red",size:18}}>
        <App />
    </ThemeContext.Provider>
  </UserContext.Provider>
)

函数组件中---------------------------------------------------------------------------

import React, { memo, useContext } from 'react'
import { UserContext, ThemeContext } from "./context"

const App = memo(() => {
  // 使用Context
  const user = useContext(UserContext)
  const theme = useContext(ThemeContext)

  return (
    <div>
      <h2>User: {user.name}-{user.level}</h2>
      <h2 style={{color: theme.color, fontSize: theme.size}}>Theme</h2>
    </div>
  )
})

export default App

当组件上层最近的 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的 context value 值。

23.4 useReducer

  • 很多人看到 useReducer 的第一反应应该是 redux 的某个替代品,其实并不是。
  • useReducer 仅仅是 useState 的一种替代方案:
  • 在某些场景下,如果 state 的处理逻辑比较复杂,我们可以通过 useReducer 来对其进行拆分;
  • 或者这次修改的 state 需要依赖之前的 state 时,也可以使用;

注释是 useState 写法

jsx 复制代码
import React, { memo, useReducer } from "react";
// import { useState } from 'react'

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { ...state, counter: state.counter + 1 };
    case "decrement":
      return { ...state, counter: state.counter - 1 };
    case "add_number":
      return { ...state, counter: state.counter + action.num };
    case "sub_number":
      return { ...state, counter: state.counter - action.num };
    default:
      return state;
  }
}

// useReducer+Context => redux

const App = memo(() => {
  // const [count, setCount] = useState(0)
  const [state, dispatch] = useReducer(reducer, { counter: 0, friends: [], user: {} });

  // const [counter, setCounter] = useState()
  // const [friends, setFriends] = useState()
  // const [user, setUser] = useState()

  return (
    <div>
      {/* <h2>当前计数: {count}</h2>
      <button onClick={e => setCount(count+1)}>+1</button>
      <button onClick={e => setCount(count-1)}>-1</button>
      <button onClick={e => setCount(count+5)}>+5</button>
      <button onClick={e => setCount(count-5)}>-5</button>
      <button onClick={e => setCount(count+100)}>+100</button> */}

      <h2>当前计数: {state.counter}</h2>
      <button onClick={(e) => dispatch({ type: "increment" })}>+1</button>
      <button onClick={(e) => dispatch({ type: "decrement" })}>-1</button>
      <button onClick={(e) => dispatch({ type: "add_number", num: 5 })}>+5</button>
      <button onClick={(e) => dispatch({ type: "sub_number", num: 5 })}>-5</button>
      <button onClick={(e) => dispatch({ type: "add_number", num: 100 })}>+100</button>
    </div>
  );
});

export default App;

23.5 useCallback

useCallback 实际的目的是为了进行性能的优化

  • useCallback 会返回一个函数的 memoized(记忆的) 值;
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的
  • 通常使用 useCallback 的目的是不希望子组件进行多次渲染,并不是为了函数进行缓存;
jsx 复制代码
import React, { memo, useState, useCallback, useRef } from "react";

// useCallback性能优化的点:
// 1.当需要将一个函数传递给子组件时, 最好使用useCallback进行优化, 将优化之后的函数, 传递给子组件

// props中的属性发生改变时, 组件本身就会被重新渲染
const Home = memo(function (props) {
  const { increment } = props;
  console.log("Home被渲染");
  return (
    <div>
      <button onClick={increment}>increment+1</button>

      {/* 100个子组件 */}
    </div>
  );
});

const App = memo(function () {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState("hello");

  // 闭包陷阱: useCallback
  // const increment = useCallback(function foo() {
  //   console.log("increment")
  //   setCount(count+1)
  // }, [count])

  // 进一步的优化: 当count发生改变时, 也使用同一个函数(了解)
  // 做法一: 将count依赖移除掉, 缺点: 闭包陷阱
  // 做法二: useRef, 在组件多次渲染时, 返回的是同一个值
  const countRef = useRef();
  countRef.current = count;
  const increment = useCallback(function foo() {
    console.log("increment");
    setCount(countRef.current + 1);
  }, []);

  // 普通的函数
  // const increment = () => {
  //   setCount(count+1)
  // }

  return (
    <div>
      <h2>计数: {count}</h2>
      <button onClick={increment}>+1</button>

      <Home increment={increment} />

      <h2>message:{message}</h2>
      <button onClick={(e) => setMessage(Math.random())}>修改message</button>
    </div>
  );
});

export default App;

当需要将一个函数传递给子组件时, 最好使用 useCallback 进行优化, 将优化之后的函数, 传递给子组件!

优化体现在传递子组件时。 父组件重新渲染都会重新定义函数体现不到优化。所以体现在子组件。函数传给子组件,子组件重新渲染不会重新定义父组件的函数。

23.6 useMemo

  • useMemo 实际的目的也是为了进行性能的优化。
    • useMemo 返回的也是一个 memoized(记忆的) 值;
    • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
jsx 复制代码
import React, { memo } from "react";
import { useMemo, useState } from "react";

const HelloWorld = memo(function (props) {
  console.log("HelloWorld被渲染~");
  return <h2>Hello World</h2>;
});

function calcNumTotal(num) {
  // console.log("calcNumTotal的计算过程被调用~")
  let total = 0;
  for (let i = 1; i <= num; i++) {
    total += i;
  }
  return total;
}

const App = memo(() => {
  const [count, setCount] = useState(0);

  // const result = calcNumTotal(50)

  // 1.不依赖任何的值, 进行计算
  const result = useMemo(() => {
    return calcNumTotal(50);
  }, []);

  // 2.依赖count
  // const result = useMemo(() => {
  //   return calcNumTotal(count*2)
  // }, [count])

  // 3.使用useMemo对子组件渲染进行优化
  const info = useMemo(() => ({ name: "why", age: 18 }), []);

  return (
    <div>
      <h2>计算结果: {result}</h2>
      <h2>计数器: {count}</h2>
      <button onClick={(e) => setCount(count + 1)}>+1</button>

      <HelloWorld result={result} info={info} />
    </div>
  );
});

export default App;

useMemo 是给子组件传一个值的。传引用型才有优化 {} \[\]。普通的值没有优化。因为对象和数组会重新声明。

23.7 useRef

  1. 用法一:引入 DOM(或者组件,但是需要是 class 组件)元素
jsx 复制代码
import React, { memo, useRef } from "react";

const App = memo(() => {
  const titleRef = useRef();
  const inputRef = useRef();

  function showTitleDom() {
    console.log(titleRef.current);
    inputRef.current.focus();
  }

  return (
    <div>
      <h2 ref={titleRef}>Hello,zimo</h2>
      <input type="text" ref={inputRef} />
      <button onClick={showTitleDom}>查看title的DOM</button>
    </div>
  );
});

export default App;
  1. 用法二:保存一个数据,这个对象在整个生命周期中可以保存不变
jsx 复制代码
import React, { memo, useRef, useState, useCallback } from "react";

let obj = null;

const App = memo(() => {
  const [count, setCount] = useState(0);
  const nameRef = useRef();
  console.log(obj === nameRef); //true
  obj = nameRef;

  // 通过useRef解决闭包陷阱
  const countRef = useRef();
  countRef.current = count;

  const increment = useCallback(() => {
    setCount(countRef.current + 1);
  }, []);

  return (
    <div>
      <h2>Hello World: {count}</h2>
      <button onClick={(e) => setCount(count + 1)}>+1</button>
      <button onClick={increment}>+1</button>
    </div>
  );
});

export default App;

useRef 返回一个 ref 对象,返回的 ref 对象再组件的整个生命周期保持不变。

28.8 useImperativeHandle

  • 回顾一下 ref 和 forwardRef 结合使用:
    • 通过 forwardRef 可以将 ref 转发到子组件;
    • 子组件拿到父组件中创建的 ref,绑定到自己的某一个元素中;
  • forwardRef 的做法本身没有什么问题,但是我们是将子组件的 DOM 直接暴露给了父组件:
  • 直接暴露给父组件带来的问题是某些情况的不可控;
  • 父组件可以拿到 DOM 后进行任意的操作;
jsx 复制代码
import React, { memo, useRef, forwardRef, useImperativeHandle } from "react";

const HelloWorld = memo(
  forwardRef((props, ref) => {
    const inputRef = useRef();

    // 子组件对父组件传入的ref进行处理
    useImperativeHandle(ref, () => {
      return {
        focus() {
          console.log("focus");
          inputRef.current.focus();
        },
        setValue(value) {
          inputRef.current.value = value;
        },
      };
    });

    return <input type="text" ref={inputRef} />;
  })
);

const App = memo(() => {
  const titleRef = useRef();
  const inputRef = useRef();

  function handleDOM() {
    // console.log(inputRef.current)
    inputRef.current.focus();
    // inputRef.current.value = ""
    inputRef.current.setValue("哈哈哈");
  }

  return (
    <div>
      <h2 ref={titleRef}>哈哈哈</h2>
      <HelloWorld ref={inputRef} />
      <button onClick={handleDOM}>DOM操作</button>
    </div>
  );
});

export default App;
  • 上面的案例中,我们只是希望父组件可以操作的 focus,其他并不希望它随意操作;
    • 通过 useImperativeHandle 可以值暴露固定的操作:
    • 通过 useImperativeHandle 的 Hook,将传入的 ref 和 useImperativeHandle 第二个参数返回的对象绑定到了一起;
    • 所以在父组件中,使用 inputRef.current 时,实际上使用的是返回的对象;
    • 比如我调用了 focus 函数,甚至可以调用 printHello 函数;

23.9 useLayoutEffect

  • useLayoutEffect 看起来和 useEffect 非常的相似,事实上他们也只有一点区别而已:
    • useEffect 会在渲染的内容更新到 DOM 上后执行,不会阻塞 DOM 的更新;
    • useLayoutEffect 会在渲染的内容更新到 DOM 上之前执行,会阻塞 DOM 的更新;
  • 如果我们希望在某些操作发生之后再更新 DOM,那么应该将这个操作放到 useLayoutEffect。
jsx 复制代码
import React, { memo, useEffect, useLayoutEffect, useState } from "react";

const App = memo(() => {
  const [count, setCount] = useState(100);

  useLayoutEffect(() => {
    console.log("useLayoutEffect");
    if (count === 0) {
      setCount(Math.random() + 99);
    }
  });

  console.log("App render");

  return (
    <div>
      <h2>count: {count}</h2>
      <button onClick={(e) => setCount(0)}>设置为0</button>
    </div>
  );
});

export default App;

官方更推荐使用 useEffect 而不是 useLayoutEffect。

23.10 自定义 Hook

获取滚动位置

jsx 复制代码
import { useState, useEffect } from "react";

function useScrollPosition() {
  const [scrollX, setScrollX] = useState(0);
  const [scrollY, setScrollY] = useState(0);

  useEffect(() => {
    function handleScroll() {
      // console.log(window.scrollX, window.scrollY)
      setScrollX(window.scrollX);
      setScrollY(window.scrollY);
    }

    window.addEventListener("scroll", handleScroll);
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, []);

  return [scrollX, scrollY];
}

export default useScrollPosition;

localStorage 数据存储

jsx 复制代码
import { useEffect, useState } from "react";

function useLocalStorage(key) {
  // 1.从localStorage中获取数据, 并且数据数据创建组件的state
  const [data, setData] = useState(() => {
    const item = localStorage.getItem(key);
    if (!item) return "";
    return JSON.parse(item);
  });

  // 2.监听data改变, 一旦发生改变就存储data最新值
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(data));
  }, [data]);

  // 3.将data/setData的操作返回给组件, 让组件可以使用和修改值
  return [data, setData];
}

export default useLocalStorage;

23.11 redux hooks(useDispatch useSelector)

  • 在之前的 redux 开发中,为了让组件和 redux 结合起来,我们使用了 react-redux 中的 connect:

    • 但是这种方式必须使用高阶函数结合返回的高阶组件;
    • 并且必须编写:mapStateToProps 和 mapDispatchToProps 映射的函数;
  • 在 Redux7.1 开始,提供了 Hook 的方式,我们再也不需要编写 connect 以及对应的映射函数了

  • useSelector 的作用是将 state 映射到组件中:

  • 参数一:将 state 映射到需要的数据中;

  • 参数二:可以进行比较来决定是否组件重新渲染;

  • useDispatch 非常简单,就是直接获取 dispatch 函数,之后在组件中直接使用即可;

  • 我们还可以通过 useStore 来获取当前的 store 对象;

jsx 复制代码
import React, { memo } from "react";
import { useSelector, useDispatch, shallowEqual } from "react-redux";
import { addNumberAction, changeMessageAction, subNumberAction } from "./store/modules/counter";

// memo高阶组件包裹起来的组件有对应的特点: 只有props发生改变时, 才会重新渲染
const Home = memo((props) => {
  // 1.使用useSelector将redux中store的数据映射到组件内
  const { message } = useSelector(
    (state) => ({
      message: state.counter.message,
    }),
    shallowEqual
  );

  const dispatch = useDispatch();
  function changeMessageHandle() {
    // 2.使用dispatch直接派发action
    dispatch(changeMessageAction("你好啊, zimo!"));
  }

  console.log("Home render");

  return (
    <div>
      <h2>Home: {message}</h2>
      <button onClick={(e) => changeMessageHandle()}>修改message</button>
    </div>
  );
});

const App = memo((props) => {
  // 1.使用useSelector将redux中store的数据映射到组件内
  const { count } = useSelector(
    (state) => ({
      count: state.counter.count,
    }),
    shallowEqual
  );

  // 2.使用dispatch直接派发action
  const dispatch = useDispatch();
  function addNumberHandle(num, isAdd = true) {
    if (isAdd) {
      dispatch(addNumberAction(num));
    } else {
      dispatch(subNumberAction(num));
    }
  }

  console.log("App render");

  return (
    <div>
      <h2>当前计数: {count}</h2>
      <button onClick={(e) => addNumberHandle(1)}>+1</button>
      <button onClick={(e) => addNumberHandle(6)}>+6</button>
      <button onClick={(e) => addNumberHandle(6, false)}>-6</button>

      <Home />
    </div>
  );
});

export default App;

感谢每一位陪伴我走过React学习之路的朋友,你们的支持和鼓励是我前进的动力。感谢React社区提供的丰富资源和无私分享,让我能够站在巨人的肩膀上,看得更远,走得更稳。感谢React的开发者们,你们的智慧和努力为我们带来了如此强大的工具。最后,感谢阅读这篇总结笔记的你,你的关注和支持是我继续学习和分享的动力。期待与你一同在React的世界里探索更多可能。

相关推荐
weixin_382395235 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__5 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.6 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~7 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华8 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子10 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅10 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni10 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学12 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端