react hooks 核心原理与实战(笔记总结)

2,177 阅读18分钟

useState: 让函数组件具有维持状态的能力

在一个函数组件的多次渲染之间,这个state是共享的 下面看一个简单的代码: const [count , setCount] = useState(0)

useState的定义:

  1. useState(initialState)的参数initialState是创建state的初始值,它可以是任意类型,比如数字,对象,数组等等
  2. useState()返回值是一个有着两个元素的数组。第一个数组元素用来读取state的值,第二个则是用来设置这个state的值。第一数组元素state是一个只读的数,我们要设置它的值只能通过第二个元素setState来设置。
  3. 如果创建创建多个state,那么我们就需要多次调用useState

useState 和 类组件里面的setState类似。但是两者并不相同区别: 类组件中的state只能有一个,一般都是把一个对象作为一个state,然后通过不同的属性来表示不同的状态。而函数组件中用useState则可以很容易的创建多个state,所以更加语义化。

state中永远不要保存可以通过计算得到的值。例如:

  1. 从props传递过来的值。有时候props传递过来的值无法直接使用,而是通过一定的计算后再UI上展示,比如说排序。那么我们要做的就是每次用的时候,都重新排序一下,或者利用某些cache机制,而不是将结果直接放到state里。
  2. 从URL中读取的值。比如有时需要读取url中的参数,把它作为组件的一部分状态。那么我们可以在每次需要用的时候从URL中读取,而不是读出来直接放到state里。
  3. 从cookie localStorage中读取的值。通常来说,也是每次要用的时候直接读取,而不是读出来后放到state里。

state便于维护状态,但是也有缺点。一旦组件有自己状态,意味着组件如果重新创建,就需要有恢复状态的过程,这通常会让组件变得更复杂。

比如一个组件想在服务器段请求获取一个用户列表显示,如果把读取的数据放到本地的state里,那么每个用到这个组件的地方,就都需要重新获取一遍。

所以有时候我们需要一个状态管理框架去维护管理所有组件的state。

useEffect:执行副作用 副作用是指一段和当前执行结果无关的代码。比如说要修改函数外部的某些变量,要求发起一个请求,等等。在函数组件的执行过程中,useEffect 中的代码的执行是不影响渲染出来的ui的。 useEffect(callback,dependencies)

useEffect接受两个要执行的函数 callback,第二个是可选的依赖项数组dependencies。其中依赖项是可选的,如果不指定那么callback就会在每次函数组件执行完后都执行;如果指定了那么只有依赖项中的值发生变化的时候,他才会执行。

useEffect涵盖了class组件中ComponentDidmount, componentDidUpdate, componentWillUnmount 三个生命周期方法。useEffect 是每次组件render完后判断依赖并执行就可以了。 当useEffect 没有依赖项时,每次render后都会重新执行。 当空数组作为依赖项时,则只在首次执行时触发,对应class组件componentDidmount。

除此之外useEffect还允许返回一个函数,用于在组件销毁的时候做一些清理的操作。比如移除事件的监听。这个机制就几乎等价于类组件中的componentWillUnmount。

useEffect 的四种执行时机:

  1. 每次render后执行:不提供第二个依赖项参数。useEffect(()=>{})
  2. 仅第一次render后执行:提供一个空数组作为依赖项。useEffect(()=>{},[])
  3. 第一次及依赖项发生变化后执行:提供依赖项数组。useEffect(()=>{},[deps])
  4. 组件unmount后执行:返回一个回调函数。useEffect(()=>{return ()=>{}},[])

Hooks 本身作为纯粹的javaScript 函数,不是通过某个特殊的API创建的,而是直接定义一个函数。只能在函数组件的顶级作用域使用;只能在函数组件或其他Hooks中使用。

Hooks 只能在函数组件的顶级作用域使用 所谓顶层作用域,就是Hooks不能在循环,条件判断或者嵌套函数内执行,而必须在顶层。同时Hooks在组件的多次渲染之间,必须按顺序执行。因为在React组件内部,其实时维护了一个对应组件的固定Hooks执行列表的,以便在多次渲染之间保持Hooks的状态,并做对比。

Hooks规则总结:第一,所有Hook必须要被执行到。第二,必须按顺序执行。

useCallback: 缓存回调函数 在react函数组件中,每一次UI的变化,都是通过重新执行整个函数来完成的,这和传统的class组件有很大的区别:函数组件中并没有一个直接的方式在多次渲染之间维持一个状态。 每次组件状态发生变化的时候,函数组件实际上都会重新执行一遍。在每次执行的时候,实际上都会创建一个新的事件处理函数 例如handleIncrement(这个函数会让count+1)。这个事件处理函数包含了count这个变量的闭包,以确定每次能够得到正确的结果。 这也意味着,即使count没有发生变化,但是函数组件因为其它状态发生变化而重新渲染时,这种写法也会每次创建一个新的函数。创建一个新的事件处理函数,虽然不影响结果的正确性,但其实是没有必要的,因为这样做不仅增加了系统的开销,更重要的是:每次创建新函数的方式会让接收事件处理函数的组件,需要重新渲染。如果每次都是一个新的,那么这个react就会认为这个组件的props发生了变化,从而必须重新渲染。因此,我们需要做到的是:只有当count发生变化时,我们才需要重新定一个回调函数。 useCallback(fn,deps) 这里fn是定义的回调函数,deps是依赖的变量数组。只有当某个依赖变量发生变化时,才会重新声明fn这个回调

import React, { useState, useCallback } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const handleIncrement = useCallback(
    () => setCount(count + 1),
    [count], // 只有当 count 发生变化时,才会重新创建回调函数
  );
  // ...
  return <button onClick={handleIncrement}>+</button>
}

我们把count这个state,作为一个依赖传递给useCallback。只要count发生变化才需要创建一个回调函数,保证了组件不会创建重复的回调函数,而接收这个回调函数作为属性的组件,也不会频繁地需要重新渲染。 useCallback 和 useMemo 的区别: useCallback缓存的是一个函数,而useMemo缓存的是计算结果。

useMemo: 缓存计算的结果。 useMemo(fn,deps) 使用场景:如果某个数据是通过其他数据计算得到的,那么只有当用到的数据,也就是依赖的数据发生变化的时候,才应该需要重新计算。 useMemo 可以避免在用到的数据没发生变化时进行的重复计算,并且能避免子组件的重复渲染,如果父组件的某个变量每次都需要重新计算来得到,对着这个父组件来说就会每次都需要刷新。如果能将此属性缓存,就和useCallback一样,可以避免很多不必要的组件刷新。 其实useCallback的功能其实是可以利用useMemo来实现的。比如:

 const myEventHandler = useMemo(() => {
   // 返回一个函数作为缓存结果
   return () => {
     // 在这里进行事件处理
   }
 }, [dep1, dep2]);

他们两者其实很类似:都是建立了一个绑定某个结果到依赖数据的关系。只有当依赖变了,这个结果才需要被重新得到。

useRef: 在多次渲染之间共享数据

在类组件中我们定义类的成员变量,以便能够在对象上通过成员属性去保存一些数据。但是在函数组件中,是没有这样一个空间去保存数据的。因此,React让useRef来提供这样的功能。 返回的 ref 对象在组件的整个生命周期内持续存在。 const myRefContainer = useRef(initialValue) 我们可以吧useRef看作是阿兹函数组件之外创建的一个容器空间。在这个容器上,我们可以通过唯一的current属性设置一个值,从而在函数组件的多次渲染之间共享这个值。 例子:


import React, { useState, useCallback, useRef } from "react";

export default function Timer() {
  // 定义 time state 用于保存计时的累积时间
  const [time, setTime] = useState(0);

  // 定义 timer 这样一个容器用于在跨组件渲染之间保存一个变量
  const timer = useRef(null);

  // 开始计时的事件处理函数
  const handleStart = useCallback(() => {
    // 使用 current 属性设置 ref 的值
    timer.current = window.setInterval(() => {
      setTime((time) => time + 1);
    }, 100);
  }, []);

  // 暂停计时的事件处理函数
  const handlePause = useCallback(() => {
    // 使用 clearInterval 来停止计时
    window.clearInterval(timer.current);
    timer.current = null;
  }, []);

  return (
    <div>
      {time / 10} seconds.
      <br />
      <button onClick={handleStart}>Start</button>
      <button onClick={handlePause}>Pause</button>
    </div>
  );
}

我们使用useRef来创建一个保存window.setInterval返回句柄的空间,从而能够在用户点击暂停按钮时清楚定时器,达到暂停计时的目的。 使用useRef保存的数据一般和UI的渲染无关,当ref的值发生变化时,是不会触发组件的重新渲染的,这也是useRef区别于useState的地方。 除了存储跨渲染的数据之外,useRe还能保存某个DOM节点的引用。


function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // current 属性指向了真实的 input 这个 DOM 节点,从而可以调用 focus 方法
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

ref 这个属性提供了获得 DOM 节点的能力,并利用 useRef 保存了这个节点的应用。这样的话,一旦 input 节点被渲染到界面上,那我们通过 inputEl.current 就能访问到真实的 DOM 节点的实例了。

useContext:定义全局状态

react提供了context这样一个机制,能够让所有在某个组件开始的组件树上创建一个context,这样这个组件树上的所有组件,就能访问和修改这个context。 const value = useContext(MyContext); 一个context是从某个组件为根组件的组件树上可用的,所以我们需要有API能够创建一个context,这就是React,createContext API 如下: const Mycontext = React.createContext(initialValue)


const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};
// 创建一个 Theme 的 Context

const ThemeContext = React.createContext(themes.light);
function App() {
  // 整个应用使用 ThemeContext.Provider 作为根组件
  return (
    // 使用 themes.dark 作为当前 Context 
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

// 在 Toolbar 组件中使用一个会使用 Theme 的 Button
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

// 在 Theme Button 中使用 useContext 来获取当前的主题
function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{
      background: theme.background,
      color: theme.foreground
    }}>
      I am styled by theme context!
    </button>
  );
}

context看起来是一个全局变量,其实是有数据绑定的作用,当context值发生变化时会触发组件的自动刷新。 动态的切换context的值:


// ...

function App() {
  // 使用 state 来保存 theme 从而可以动态修改
  const [theme, setTheme] = useState("light");

  // 切换 theme 的回调函数
  const toggleTheme = useCallback(() => {
    setTheme((theme) => (theme === "light" ? "dark" : "light"));
  }, []);

  return (
    // 使用 theme state 作为当前 Context
    <ThemeContext.Provider value={themes[theme]}>
      <button onClick={toggleTheme}>Toggle Theme</button>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

context 相当于提供了一个定义React世界中全局变量的机制,而全局变量则意味着两点:

  1. 会让调试变得困难,因为你很难跟踪某个context的变化究竟是如何产生的。
  2. 让组件的复用变得困呐,因为一个组件如果使用某个context,他就必须确保被用到的地方一定有这个context的provider在其父组件的路径上。 context更多的是提供了一个强大的机制,让React应用具备定义全局的响应式数据的能力。

正确理解函数组件的生命周期

react 可能引起状态变化的原因:

  1. 用户操作产生的事件,比如点击了某个按钮。
  2. 副作用产生的事件,比如发起某个请求正确返回了。 这两种事件本身不会导致组件的重新渲染,一定是因为改变了某个状态,这个状态可能是State 或者 Context,从而导致了UI的重新渲染。 在函数组件中你要思考的方式永远是:当某个状态发生变化时,我要做什么,而不再是在class组件中的某个生命周期方法中我要做什么。 使用useRef来来实现一个初始化的实例的过程 useSingleton :

import { useRef } from 'react';

// 创建一个自定义 Hook 用于执行一次性代码
function useSingleton(callback) {
  // 用一个 called ref 标记 callback 是否执行过
  const called = useRef(false);
  // 如果已经执行过,则直接返回
  if (called.current) return;
  // 第一次调用时直接执行
  callBack();
  // 设置标记为已执行过
  called.current = true;
}

调用这个hooks:

import useSingleton from './useSingleton';

const MyComp = () => {
  // 使用自定义 Hook
  useSingleton(() => {
    console.log('这段代码只执行一次');
  });

  return (
    <div>My Component</div>
  );
};

useSingleton 这个 Hook 的核心逻辑就是定义只执行一次的代码。而是否在所有代码之前执行,则取决于在哪里调用,可以说,它的功能其实是包含了构造函数的功能的。 useEffect的写法并没有完全等价于传统的这几个生命周期方法:

  1. useEffect(callback)这个hook接收的callback,只有在依赖项变化时才执行,而传统的componentDidUpdate则一定会执行,Hook的机制其实更具有语义话,因为过去在componentDidUpdate中,我们通常需要手动判断某个状态是否发生变化,然后在执行特定的逻辑。
  2. callback返回的函数在下一次依赖项发生变化以及组件销毁之前执行,而传统的componentWillUnmount只在组件销毁时才会执行。

创建一个自定义Hooks

自定义hooks在形式上非常简单,声明一个名字以use开头的函数,如果内部并没有用其他任何hooks,那么这个函数就不是一个hook,而只是一个普通的函数。但是如果用了其他Hooks,那么他就是一个hook。 例如:


import { useState, useCallback }from 'react';
 
function useCounter() {
  // 定义 count 这个 state 用于保存当前数值
  const [count, setCount] = useState(0);
  // 实现加 1 的操作
  const increment = useCallback(() => setCount(count + 1), [count]);
  // 实现减 1 的操作
  const decrement = useCallback(() => setCount(count - 1), [count]);
  // 重置计数器
  const reset = useCallback(() => setCount(0), []);
  
  // 将业务逻辑的操作 export 出去供调用者使用
  return { count, increment, decrement, reset };
}
// 使用

import React from 'react';

function Counter() {
  // 调用自定义 Hook
  const { count, increment, decrement, reset } = useCounter();

  // 渲染 UI
  return (
    <div>
      <button onClick={decrement}> - </button>
      <p>{count}</p>
      <button onClick={increment}> + </button>
      <button onClick={reset}> reset </button>
    </div>
  );
}

在上述代码中,我们吧原来在函数组件中实现的逻辑提取出来,成为一个单独的Hook,一方面能让这个逻辑得到重用,另外一方面也能让代码更加语义化,并且易于理解和维护。 自定义Hooks的两个特点:

  1. 名字一定是以use开头的函数,这样react才能知道这个函数是一个Hook;
  2. 函数内部一定调用了其他的Hooks,可以是内置的Hooks,也可以是其他自定义Hooks。这样才能够让组件刷新,或者去产生副作用。

封装通用逻辑


import { useState } from 'react';

const useAsync = (asyncFunction) => {
  // 设置三个异步逻辑相关的 state
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  // 定义一个 callback 用于执行异步逻辑
  const execute = useCallback(() => {
    // 请求开始时,设置 loading 为 true,清除已有数据和 error 状态
    setLoading(true);
    setData(null);
    setError(null);
    return asyncFunction()
      .then((response) => {
        // 请求成功时,将数据写进 state,设置 loading 为 false
        setData(response);
        setLoading(false);
      })
      .catch((error) => {
        // 请求失败时,设置 loading 为 false,并设置错误状态
        setError(error);
        setLoading(false);
      });
  }, [asyncFunction]);

  return { execute, loading, data, error };
};

我们在组件中只需要关心业务逻辑相关的部分:


import React from "react";
import useAsync from './useAsync';

export default function UserList() {
  // 通过 useAsync 这个函数,只需要提供异步逻辑的实现
  const {
    execute: fetchUsers,
    data: users,
    loading,
    error,
  } = useAsync(async () => {
    const res = await fetch("https://reqres.in/api/users/");
    const json = await res.json();
    return json.data;
  });
  
  return (
    // 根据状态渲染 UI...
  );
}

利用Hooks能够管理React 组件状态的能力,将一个组件中的某一部分状态独立出来,从而实现了通用逻辑的重用。 这种类型的封装哦我们呢写一个工具类就可以了,为什么要通过Hooks进行封装呢? 因为在Hooks中,你可以管理当前组件的state,从而将更多的逻辑写在可重用的Hooks中。但是要知道,在普通的工具类中是无法直接修改组件state的,那么也就无法在数据改变的时候触发组件的重新渲染。

监听浏览器状态:useScroll

虽然react组件基本上不需要关心太多浏览器的API,但是有时候缺失必须的:

  1. 界面需要根据在窗口大小变化重新布局;
  2. 在页面滚动时,需要根据滚动条位置,来决定是否显示一个“返回顶部”的按钮 Hooks的优点就是可以让React的组件绑定在任何可能的数据源上。这样当数据源繁盛变化是,组件能够自动刷新。 例子:

import { useState, useEffect } from 'react';

// 获取横向,纵向滚动条位置
const getPosition = () => {
  return {
    x: document.body.scrollLeft,
    y: document.body.scrollTop,
  };
};
const useScroll = () => {
  // 定一个 position 这个 state 保存滚动条位置
  const [position, setPosition] = useState(getPosition());
  useEffect(() => {
    const handler = () => {
      setPosition(getPosition(document));
    };
    // 监听 scroll 事件,更新滚动条位置
    document.addEventListener("scroll", handler);
    return () => {
      // 组件销毁时,取消事件监听
      document.removeEventListener("scroll", handler);
    };
  }, []);
  return position;
};

监听滚动:


import React, { useCallback } from 'react';
import useScroll from './useScroll';

function ScrollTop() {
  const { y } = useScroll();

  const goTop = useCallback(() => {
    document.body.scrollTop = 0;
  }, []);

  const style = {
    position: "fixed",
    right: "10px",
    bottom: "10px",
  };
  // 当滚动条位置纵向超过 300 时,显示返回顶部按钮
  if (y > 300) {
    return (
      <button onClick={goTop} style={style}>
        Back to Top
      </button>
    );
  }
  // 否则不 render 任何 UI
  return null;
}

自定义hooks的四个使用场景:

  1. 抽取业务逻辑
  2. 封装通用逻辑
  3. 监听浏览器状态
  4. 拆分复杂组件

redux的两个特点:

  1. Redux Store是全局唯一的。(即整个应用程序一般只有一个store)
  2. Redux Store是树状结构,可以更天然地映射到组件树的结构,虽然不是必须的

Redux: state Action 和 Reducer 其中state即store,一般就是一个纯JS Object Action 也是一个对象,用于描述动作。 Reducer则是一个函数,接受Action 和 State 并作为参数,通过计算得到新的store

state + Action = new state

action 并不是一个具体的概念,而可以把它看作是redux的一个使用模式。他通过组合使用同步action,在没有引入新概念的同时,用一致的方式提供了处理异步逻辑的方案。

Hooks通过useState的内置Hook 来完成组件的更新。自定义Hooks要实现的逻辑要么用到state,要么用到state,要么用到副作用,是一定会用到内置Hooks或者其他自定义Hooks的。 useEffect 一定是在函数render之后执行,而函数中的代码,是直接影响当次render的结果。

异步发送请求


import { useState, useEffect } from "react";
import apiClient from "./apiClient";

export default (id) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
 // 当 id 不存在,直接返回,不发送请求
 if (!id) return;
 setLoading(true);
 setData(null);
 setError(null);
 apiClient
   .get(`/users/${id}`)
   .then((res) => {
     setLoading(false);
     setData(res.data);
   })
   .catch((err) => {
     setLoading(false);
     setError(err);
   });
}, [id]);
return {
 loading,
 error,
 data
};
};

import { useState } from "react";
import CommentList from "./CommentList";
import useArticle from "./useArticle";
import useUser from "./useUser";
import useComments from "./useComments";

const ArticleView = ({ id }) => {
const { data: article, loading, error } = useArticle(id);
const { data: comments } = useComments(id);
const { data: user } = useUser(article?.userId);
if (error) return "Failed.";
if (!article || loading) return "Loading...";
return (
  <div className="exp-09-article-view">
    <h1>
      {id}. {article.title}
    </h1>
    {user && (
      <div className="user-info">
        <img src={user.avatar} height="40px" alt="user" />
        <div>{user.name}</div>
        <div>{article.createdAt}</div>
      </div>
    )}
    <p>{article.content}</p>
    <CommentList data={comments || []} />
  </div>
);
};

Hooks 必须在顶层作用域调用,而不能放在条件判断,循环等语句,也不能放在return 语句之后。react需要在函数组件内部维护所用到的Hooks状态,所以无法在条件语句中使用Hooks。

render props 实现UI逻辑重用需求场景

import { Popover } from "antd";

function ListWithMore({ renderItem, data = [], max }) {
const elements = data.map((item, index) => renderItem(item, index, data));
const show = elements.slice(0, max);
const hide = elements.slice(max);
return (
  <span className="exp-10-list-with-more">
    {show}
    {hide.length > 0 && (
      <Popover content={<div style={{ maxWidth: 500 }}>{hide}</div>}>
        <span className="more-items-wrapper">
          and{" "}
          <span className="more-items-trigger"> {hide.length} more...</span>
        </span>
      </Popover>
    )}
  </span>
);
}

// 这里用一个示例数据
import data from './data';

function ListWithMoreExample () => {
return (
  <div className="exp-10-list-with-more">
    <h1>User Names</h1>
    <div className="user-names">
      Liked by:{" "}
      <ListWithMore
        renderItem={(user) => {
          return <span className="user-name">{user.name}</span>;
        }}
        data={data}
        max={3}
      />
    </div>
    <br />
    <br />
    <h1>User List</h1>
    <div className="user-list">
      <div className="user-list-row user-list-row-head">
        <span className="user-name-cell">Name</span>
        <span>City</span>
        <span>Job Title</span>
      </div>
      <ListWithMore
        renderItem={(user) => {
          return (
            <div className="user-list-row">
              <span className="user-name-cell">{user.name}</span>
              <span>{user.city}</span>
              <span>{user.job}</span>
            </div>
          );
        }}
        data={data}
        max={5}
      />
    </div>
  </div>
);
};

在react 中 父子组件的交互式通过props这个机制其实是双向的,父组件通过props把值传递给子组件,而子组件则通过暴露一些事件,给父组件反馈一些状态或者数据。(组件之间通信的基础)

react17之前所有的事件都是绑定在document 上的 react 17之后所有的事件都绑定在整个APP上的根节点上,这主要是为了以后页面上可能存在多版本react考虑的。 原因: 第一虚拟DOM render的时候,dom很可能还没有真实地render 到页面上,所以无法绑定事件。 第二react可以屏蔽底层事件的细节,避免浏览器的兼容性问题 同时对于react native 这种不是通过浏览器render 的运行时,也能提供一致的API 浏览器的原生机制事件会从被触发的节点往父节点冒泡,直到根节点,所以根节点其实是可以收到所有的事件的(冒泡模型) 无论事件在哪个节点被触发,React都可以通过事件的srcElement 这个属性,知道它是从哪个节点开始出发的,这样react就可以收集管理所有的事件,然后再以一致的api暴露出来。 自定义事件和原生事件