React Hook学习

React 要做的工作就是每当 Model(state+props) 发生变化时,函数会重新执行,并且生成新的 DOM 树,然后 React 再把新的 DOM 树以最优的方式更新到浏览器。

class 组件并一定是最合适的实现方式,比如说继承和实例两个重要的特性,在 react 中是很少被使用到,毕竟 React 组件之间是不会互相继承的,同时因为所有 UI 都是由状态驱动的,因此很少会在外部去调用一个类实例(即组件)的方法。

因为 React 推崇声明式编程范式,即所有的 UI 都是声明出来的,不用处理细节的变化过程。因此,通过函数去描述一个组件才是最为自然的方式。Hooks 带来的最大好处:逻辑复用,随着 React Hooks 的引入,函数组件变得更加强大和灵活,可以完成类组件的所有功能,并且更易于理解和编写。

useState

函数组件中并没有一个直接的方式在多次渲染之间维持一个状态,因此 Hook 通过 useState 来管理 state ,它可以让函数组件具有维持状态的能力,和类组件中的 setState 非常类似。

js 复制代码
import React, { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

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

useEffect

同样的,函数组件也是没有生命周期的,useEffect 应运而生,它可以接收两个参数

js 复制代码
useEffect(callback, dependencies)

第一个为要执行的函数 callback,第二个是可选的依赖项数组 dependencies。

在函数组件的当次执行过程中,useEffect 中代码的执行是不影响渲染出来的 UI 的。

useEffect 是每次组件 render 完后判断依赖并执行的:

  • 没有依赖项,则每次 render 后都会重新执行,
  • 空数组作为依赖项,则只在首次执行时触发,
js 复制代码
import React, { useState, useEffect } from "react";

function BlogView({ id }) {
  // 设置一个本地 state 用于保存 blog 内容
  const [blogContent, setBlogContent] = useState(null);

  useEffect(() => {
    // useEffect 的 callback 要避免直接的 async 函数,需要封装一下
    const doAsync = async () => {
      // 当 id 发生变化时,将当前内容清楚以保持一致性
      setBlogContent(null);
      // 发起请求获取数据
      const res = await fetch(`/blog-content/${id}`);
      // 将获取的数据放入 state
      setBlogContent(await res.text());
    };
    doAsync();
  }, [id]); // 使用 id 作为依赖项,变化时则执行副作用

  // 如果没有 blogContent 则认为是在 loading 状态
  const isLoading = !blogContent;
  return <div>{isLoading ? "Loading..." : blogContent}</div>;
}

useEffect 还允许返回一个函数,用于在组件销毁的时候做一些清理的操作。比如移除事件的监听。这个机制就几乎等价于类组件中的 componentWillUnmount。举个例子,在组件中,我们需要监听窗口的大小变化,以便做一些布局上的调整

js 复制代码
// 设置一个 size 的 state 用于保存当前窗口尺寸
const [size, setSize] = useState({});
useEffect(() => {
  // 窗口大小变化事件处理函数
  const handler = () => {
    setSize(getSize());
  };
  // 监听 resize 事件
  window.addEventListener('resize', handler);
  
  // 返回一个 callback 在组件销毁时调用
  return () => {
    // 移除 resize 事件
    window.removeEventListener('resize', handler);
  };
}, []);

useCallback

每次组件状态发生变化的时候,函数组件实际上都会重新执行一遍,这个时候,函数组件中的方法都会被重新创建。这样做不仅增加了系统的开销,更重要的是:每次创建新函数的方式会让接收事件处理函数的组件,需要重新渲染。

js 复制代码
useCallback(fn, deps)

这里 fn 是定义的回调函数,deps 是依赖的变量数组。只有当某个依赖变量发生变化时,才会重新声明 fn 这个回调函数。这样就能避免重复创建函数组件中的方法

js 复制代码
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> }

useMemo

如果某个数据是通过其它数据计算得到的,那么只有当用到的数据,也就是依赖的数据发生变化的时候,才应该需要重新计算

js 复制代码
useMemo(fn, deps);

这里的 fn 是产生所需数据的一个计算函数。通常来说,fn 会使用 deps 中声明的一些变量来生成一个结果,用来渲染出最终的 UI。

如果 deps 中声明的一些变量没有变化,那么函数组件重新渲染时,不会重新触发fn这个计算函数。

js 复制代码
//...
// 使用 userMemo 缓存计算的结果
const usersToShow = useMemo(() => {
    if (!users) return null;
    return users.data.filter((user) => {
      return user.first_name.includes(searchKey));
    }
  }, [users, searchKey]);
//...

users、searchKey不变化,就不会重新算usersToShow。除了避免重复计算之外,useMemo 还有一个很重要的好处:避免子组件的重复渲染。

useRef

在类组件中,我们可以定义类的成员变量,以便能在对象上通过成员属性去保存一些数据。但是在函数组件中,是没有这样一个空间去保存数据的。因此,React 让 useRef 这样一个 Hook 来提供这样的功能。

js 复制代码
const myRefContainer = useRef(initialValue);

我们可以把 useRef 看作是在函数组件之外创建的一个容器空间。在这个容器上,我们可以通过唯一的 current 属设置一个值,从而在函数组件的多次渲染之间共享这个值。

js 复制代码
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 的地方。

除了存储跨渲染的数据之外,useRef 还有一个重要的功能,就是保存某个 DOM 节点的引用。

js 复制代码
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>
    </>
  );
}

useContext

React 提供了 Context 这样一个机制,能够让所有在某个组件开始的组件树上创建一个 Context。

js 复制代码
// 创建content
const MyContext = React.createContext(initialValue);
//使用content
const value = useContext(MyContext);

具体地

js 复制代码
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>
  );
}

优点:

  • 能够进行数据的绑定,让 React 应用具备定义全局的响应式数据的能力。
  • 当这个 Context 的数据发生变化时,使用这个数据的组件就能够自动刷新。

缺点:

  • 会让调试变得困难,因为你很难跟踪某个 Context 的变化究竟是如何产生的。
  • 让组件的复用变得困难,因为一个组件如果使用了某个 Context,它就必须确保被用到的地方一定有这个 Context 的 Provider 在其父组件的路径上。

如何创建自定义 Hooks?

自定义 Hooks 在形式上其实非常简单,就是声明一个名字以 use 开头的函数,并且在函数中要用到其它 Hooks。

js 复制代码
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 };
}

使用这个 Hook

js 复制代码
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>
  );
}

refs: b.geekbang.org/member/cour...

相关推荐
潜意识起点12 分钟前
精通 CSS 阴影效果:从基础到高级应用
前端·css
奋斗吧程序媛16 分钟前
删除VSCode上 origin/分支名,但GitLab上实际上不存在的分支
前端·vscode
IT女孩儿26 分钟前
JavaScript--WebAPI查缺补漏(二)
开发语言·前端·javascript·html·ecmascript
m0_748256563 小时前
如何解决前端发送数据到后端为空的问题
前端
请叫我飞哥@3 小时前
HTML5适配手机
前端·html·html5
@解忧杂货铺5 小时前
前端vue如何实现数字框中通过鼠标滚轮上下滚动增减数字
前端·javascript·vue.js
F-2H6 小时前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
gqkmiss7 小时前
Chrome 浏览器插件获取网页 iframe 中的 window 对象
前端·chrome·iframe·postmessage·chrome 插件
m0_748247559 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255029 小时前
前端常用算法集合
前端·算法