React04-Hooks 详解

一、Hooks

1. Hooks 简介

Hooks,可以翻译成钩子。

在类组件中钩子函数就是生命周期函数,Hooks 主要用在函数组件。

在 react 中定义组件有2种方式:class 定义的类组件和 function 定义的函数组件。

在类组件中,钩子函数可以给组件增加额外的功能。

类组件的不足:

  • 在一个钩子里可能有很多业务代码。
  • 一个业务很可能出现在多个钩子里。
  • class 组件中的 this 指向问题

在函数组件中,函数在执行完毕之后,会自动销毁内存,存储在函数中的状态无法保留。为了增加函数组件的功能,我们需要引入 Hooks。

类组件通过在构造器中直接使用 this.state = {} 给组件设置状态值,而函数组件不行。因为函数执行完后会销毁内容,在函数内声明的变量就会自定销毁,所以函数组件无法设置状态,于是 react 提供了 Hooks。

2. Hooks 的分类

Hook 分为2种,基础 Hook 和额外的 Hook。

基础 Hook:

  • useState
  • useEffect
  • useContext

额外的 Hook:

  • useReducer
  • useCallback
  • useMemo
  • useRef

所有 Hook 都以 use 开头。

3. Hook 的使用

 所有 Hook 都在 react 模块下,使用时需进行引入。

import { useState } from 'react';

二、useState

1. 使用 useState 的注意事项

(1) useState 向组件引入新的状态,这个状态会被保留在 react 中。

(2) useState 只有一个参数,这个参数是初始状态值,它可以是任意类型。

(3) useState 可以多次调用,意味着可以为组件传入多个状态。

(4) useState 返回值是一个数组,第一个元素就是我们定义的 state,第二个元素就是修改这个 state 的方法。接收 useState 的返回值使用数组结构语法,我们可以随意为 state 起名字。修改 state 的方法必须是 set + 状态名首字母大写构成,不按照约定写就会报错。

(5) useState 的参数也可以是一个函数,这个函数的返回值就是我们的初始状态。

2. 计数器案例

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h3>{count}</h3>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

在这个案例中,我们定义了一个状态 count,传入的参数为函数 () => 1,则 count 的初始值为函数的返回值1。

当点击按钮时,调用修改方法 setCount 将 count 的值自增1,页面上计数器也会同步自增1。

3. 修改状态方法

修改状态的方法 set + 状态变量名:

  • 这个方法的参数可以是值(替换方法),也可以是一个函数。如果是函数,那么这个函数的参数就是初始状态值,这个方法就是更新方法。
  • 这个方法是异步的。

看如下案例:

function Counter() {
  const [count, setCount] = useState(() => 1);
  function handleCount(a) {
    setCount(a + 1);
    document.title = a;
  }
  return (
    <div>
      <h3>{count}</h3>
      <button onClick={() => handleCount(count)}>+</button>
    </div>
  );
}

在点击按钮后,计数器显示数字变为了2,但页面的标题还是1。每次点击按钮后,页面的标题都比计数器显示数字少1。因此执行代码先执行了第5行的设置页面标题,才执行第4行的设置 count 值,看过上篇文章就很容易理解。

三、useEffect

1. useEffect 简介

useEffect 这个 Hook 函数的主要作用就是将副作用代码添加到函数组件内。所谓的副作用代码就是 dom 更改、计时器、数据请求等。

使用场景:

  • useEffect(() => {}) 这种写法代表两个生命周期函数 componentDidMount 和 componentDidUpdate。
  • useEffect(() => {}, []) 这种写法代表生命周期函数 componentDidMount。
  • useEffect(() => () => {}) 这种写法代表组件卸载之前 componentWillUnmount 和 componentDidUpdate 两个生命周期函数。
import { useEffect } from 'react';

2. useEffect(() => {})

useEffect 钩子函数传入一个函数作为参数,代表组件在加载完成后和数据更新时都会调用传入的函数。

function Counter() {
  const [count, setCount] = useState(() => 1);
  function handleCount(a) {
    setCount(a + 1);
    document.title = a;
  }
  useEffect(() => {
    console.log('hahaha');
  });
  return (
    <div>
      <h3>{count}</h3>
      <button onClick={() => handleCount(count)}>+</button>
    </div>
  );
}

在页面加载完成和每次点击按钮让计数器自增后,控制台都会打印 hahaha。即 useEffect Hook 实现了生命周期函数 componentDidMount 和 componentDidUpdate 的功能。

3. useEffect(() => {}, [])

useEffect 钩子函数传入一个函数和一个数组作为参数,代表组件在加载完成后会调用传入的函数。

function Counter() {
  const [count, setCount] = useState(() => 1);
  function handleCount(a) {
    setCount(a + 1);
    document.title = a;
  }
  useEffect(() => {
    console.log('hahaha');
  }, []);
  return (
    <div>
      <h3>{count}</h3>
      <button onClick={() => handleCount(count)}>+</button>
    </div>
  );
}

这个案例和上一个案例不同之处在于 useEffect 钩子函数传入了一个空数组作为第2个参数,当页面加载完成时控制台会打印 hahaha,点击按钮更新计数器后不再打印,即实现了生命周期函数 componentDidMount 的功能。

4. useEffect(() => () => {})

useEffect 钩子函数传入一个返回函数的函数作为参数,代表组件在数据更新时和卸载时会调用返回的函数。

function Counter(props) {
  const [count, setCount] = useState(() => 1);
  function handleCount(a) {
    setCount(a + 1);
    document.title = a;
  }
  useEffect(() => () => {
    console.log('hahaha');
  }, []);
  return (
    <div>
      <h3>{count}</h3>
      <button onClick={() => handleCount(count)}>+</button>
      <button onClick={() => props.root.unmount()}>卸载组件</button>
    </div>
  );
}

这个案例中,点击按钮更新计数器和点击卸载组件时控制台都会打印 hahaha,即实现了生命周期函数 componentWillUnmount 和 componentDidUpdate 的功能。

5. useEffect 第二个参数的使用

useEffect 钩子函数的第二个参数,正常添加空数组,代表的生命周期是 componentDidMount。即使我们修改了 state,useEffect 也只会调用一次。

如果我们想让某个 state 发生改变的时候,继续调用 useEffect,就需要把这个状态添加到第二个参数的数组中。

看下面的案例:

function Counter() {
  const [count, setCount] = useState(() => 1);
  const [person, setPerson] = useState({ name: 'zhangsan' });
  function handleCount(a) {
    setCount(a + 1);
    document.title = a;
  }
  useEffect(() => {
    console.log('计数器改变');
  }, [count]); // count 一旦发生改变,就会执行 useEffect
  return (
    <div>
      <h3>{count}</h3>
      <h3>{person.name}</h3>
      <button onClick={() => handleCount(count)}>+</button>
      <button onClick={() => setPerson({ name: 'lisi' })}>更改person</button>
    </div>
  );
}

useEffect 第二个参数传递了 count,那么将会在 count 状态更新时才会执行传入的函数。

6. useEffect 的异步处理

看下面的案例:

function Counter() {
  function asyncFn() {
    setTimeout(() => {
      console.log('hahaha');
    }, 1000);
  }
  useEffect(() => {
    asyncFn();
  });
  return (
    <div>
    </div>
  );
}

我们声明了一个异步函数,然后在 useEffect 中调用,预览正常,在页面加载完成1秒后打印 hahaha。

当我们使用 async/await 时,像下面这样:

function Counter() {
  function asyncFn() {
    setTimeout(() => {
      console.log('hahaha');
    }, 1000);
  }
  useEffect(async () => {
    await asyncFn();
  });
  return (
    <div>
    </div>
  );
}

这时控制台就会报一个警告:

在 useEffect 中如果使用了异步函数,那就需要定义一个自调用函数。如:

function Counter() {
  function asyncFn() {
    setTimeout(() => {
      console.log('hahaha');
    }, 1000);
  }
  useEffect(() => {
    (async () => {
      await asyncFn();
    })();
  });
  return (
    <div>
    </div>
  );
}

这时控制台就不报警告了。

遇到异步函数,我们需要在 useEffect 中添加一个自调用函数。

四、useContext

useContext 用于父组件向子孙组件传递数据,不需要再使用通过 props 从父组件向子组件逐级传递数据。

如果组件多层嵌套,使用 props 来传值显得极其复杂,这时就需要使用 useContext。

1. 引入 useContext

要使用 useContext,需要引入 useContext 和 createContext 两个函数。

import { useContext, createContext } from 'react';

2. 使用方法

首先定义一个 context 变量,用于存放当前上下文对象。将上下文对象的 Provider 作为父组件,通过 value 属性将要传递的值传给子孙组件。在子孙组件中就可以通过 useContext 获取到要传递的值。

const myContext = createContext(); // 当前上下文对象

function App() {
  const value = useContext(myContext);
  return (
    <div>{value}</div>
  );
}

export default function () {
  return (
    <myContext.Provider value={100}>
      <div>hello world</div>
      <App />
    </myContext.Provider>
  );
}

在 App 组件中我们获取到了父组件通过 context 传递来的 value 值。

五、useReducer

1. useReducer 简介

useReducer 是 useState 的替代方案。

对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序可能会令人不知所措。对于这种情况,我们可以将组件的所有状态更新逻辑整合到一个外部函数中,这个函数叫作 reducer。

Reducer 是处理状态的另一种方式。可以通过三个步骤将 useState 迁移到 useReducer:

(1) 将设置状态的逻辑修改成 dispatch 的一个 action;
(2) 编写 一个 reducer 函数;
(3) 在组件中 使用 reducer。

使用 reducers 管理状态与直接设置状态略有不同。它不是通过设置状态来告诉 react “要做什么”,而是通过事件处理程序 dispatch 一个 “action” 来指明 “用户刚刚做了什么”。(而状态更新逻辑则保存在其他地方!)因此,我们不再通过事件处理器直接 “设置 task”,而是 dispatch 一个 “添加/修改/删除任务” 的 action。这更加符合用户的思维。

action 对象可以有多种结构。按照惯例,我们通常会添加一个字符串类型的 type 字段来描述发生了什么,并通过其它字段传递额外的信息。type 是特定于组件的,选一个能描述清楚发生的事件的名字!

import { useReducer } from 'react';

2. 计数器案例

在这个案例中,要实现一个可以增加和减少数字的计数器,效果如下:

点击+号按钮自增,点击-号按钮自减。

function Counter() {
  const initState = { count: 0 };
  function reducer(state, action) {
    switch (action.type) {
      case 'increment':
        return { count: state.count + 1 };
      case 'decrement':
        return { count: state.count - 1 };
      default:
        return { count: state };
    }
  }
  const [state, dispatch] = useReducer(reducer, initState);
  return (
    <div>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <span>{state.count}</span>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </div>
  );
}

在这个案例中,我们定义了一个 reducer 函数处理不同操作需要更新的状态。在点击按钮时,调用 useReducer 返回的 dispatch 方法,传递操作类型给 reducer 函数,然后按对应方法更新状态。

reducer 函数就是我们放置状态逻辑的地方。它接受两个参数,分别为当前 state 和 action 对象,并且返回的是更新后的 state,react 会将状态设置为从 reducer 返回的状态。

在 reducers 中使用 switch 语句是一种惯例,和使用 if/else 的结果是相同的,但 switch 语句读起来一目了然。

六、useMemo

1. useMemo 简介

useMemo 是一种性能优化的手段,主要用途就是对状态 state 的记忆,并且还具有缓存功能,类似于 vue 中的计算属性。还有一个作用,避免在代码中参与大量运算。

useMemo 接收2个参数,第1个参数为执行运算的函数,第2个参数为要监控的状态。

import { useMemo } from 'react';

2. 使用 useMemo

还是计数器案例,使用 useMemo 通过计数器当前值计算出一个新的值展示在页面上。

function Counter() {
  const [count, setCount] = useState(0);
  const value = useMemo(function () {
    return count * 10;
  }, [count]); // 数组中的元素就是 useMemo 监控的状态
  return (
    <div>
      <h3>{count}</h3>
      <h3>{value}</h3>
      <button onClick={() => setCount(count + 1)}>按钮</button>
    </div>
  );
}

 在这个案例中,我们使用 useState 定义了一个计数器。使用 useMemo 定义了一个 value 状态,代表 useMemo 中计算结果的值。

useMemo 会监控第2个参数数组中的状态,当对应状态更新时,才会执行 useMemo。

七、useRef

1. useRef 简介

useRef 用于获取组件中的 dom 对象。

import { useRef } from 'react';

2. useRef 使用

在组件的属性中加入 ref 属性为存储 useRef 的返回值的变量,就可以获取到这个组件的 dom 对象。

function App() {
  const refObj = useRef();
  console.log(refOjb);
  function getRef() {
    console.log(refObj);
  }
  return (
    <div>
      <div ref={refObj}>hello</div>
      <button onClick={getRef}>按钮</button>
    </div>
  );
}

current 属性为获取到的 dom 对象。

第一次打印出的 refObj 的 current 属性为 undefined,因为这时页面元素还未挂载。当点击按钮时,就会获取到 div 的 dom 对象了。

八、memo

1. memo 简介

memo 是一个高阶组件(参数又是一个组件),功能是对组件进行记忆。

import { memo } from 'react';

2. memo 使用

先来看一个案例:

function App() {
  const [count, setCount] = useState(0);
  const fn = function () {
    console.log('hahaha');
  };
  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1)}>增加</button>
      <Heads fn={fn}></Heads>
    </div>
  );
}

function Heads(props) {
  console.log('我被渲染了');
  return <button>按钮</button>;
}

在这个案例中,我们定义了一个父组件 App 和一个子组件 Heads。接下来查看在 count 变化时,子组件是否重新被渲染,答案是肯定的,子组件被渲染了多次。

但这个过程中子组件没有改变,重新渲染多次显然不好。

当前组件内的视图没有发生改变,但被重渲染了,这时就需要借助 memo。

function App() {
  const [count, setCount] = useState(0);
  const fn = function () {
    console.log('hahaha');
  };
  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1)}>增加</button>
      <Heads fn={fn}></Heads>
    </div>
  );
}

const Heads = memo(function (props) {
  console.log('我被渲染了');
  return <button>按钮</button>;
});

memo 的参数是一个组件,返回值为一个高阶组件,可以对传入的组件进行记忆,不修改就不会重新渲染。

但上述案例中,点击按钮时子组件还是会重新渲染。原因在于父组件给子组件传递了一个 fn,当点击按钮时,父组件重新渲染导致 fn 被赋值,fn 修改就会导致子组件重新渲染。

修改代码如下:

function App() {
  const [count, setCount] = useState(0);
  const fn = function () {
    console.log('hahaha');
  };
  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1)}>增加</button>
      <Heads></Heads>
    </div>
  );
}

const Heads = memo(function (props) {
  console.log('我被渲染了');
  return <button>按钮</button>;
});

删除给子组件传入的 fn,这时再点击按钮,子组件就不会重新渲染了。

要实现前面的案例传入 fn 不让子组件重新渲染,需要使用 useCallback Hook。

九、useCallback

1. useCallback 简介

useCallback 是一个允许我们在多次渲染中缓存函数的 React Hook,它返回一个 memoized 回调函数。

useMemo 是对数据的记忆,useCallback 是对函数的记忆。

useCallback 有2个参数,第1个参数为要缓存的函数,第2个参数是一个数组,表示在哪些响应值(包括 props 、state 和所有在组件内部直接声明的变量和函数)变化时更新函数。

import { useCallback } from 'react';

2. useCallback 使用

将上面的案例用 useCallback 改写:

function App() {
  const [count, setCount] = useState(1);
  const fn = useCallback(function () {
    return count;
  }, []);
  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1)}>增加</button>
      <Heads fn={fn}></Heads>
    </div>
  );
}

const Heads = memo(function (props) {
  return <button onClick={() => console.log(`我被渲染了${props.fn()}次`)}>按钮</button>;
});

这里我们使用 useCallback 将函数 fn 进行缓存,这时再去点击增加按钮,将不会再重新渲染子组件。

接下来看 useCallback 第2个参数如何使用:

function App() {
  const [count, setCount] = useState(1);
  const fn = useCallback(function () {
    return count;
  }, [count]);
  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1)}>增加</button>
      <Heads fn={fn}></Heads>
    </div>
  );
}

const Heads = memo(function (props) {
  return <button onClick={() => console.log(`我被渲染了${props.fn()}次`)}>按钮</button>;
});

这里我们在 useCallback 中加入了第二个参数,数组中有一个元素 count,表示在 count 变化时更新函数 fn。这时在点击增加按钮,就会重新渲染子组件。

十、自定义 Hook

自定义 Hook 就是自己封装的函数功能和 react 中内置的 Hook 进行结合,用于组件间共享逻辑

自定义 Hook 必须以 use 开头。

下面来封装一个 axios get 请求的自定义 Hook:

const useGet = function ({ path, params }) {
  const [data, setData] = useState({});
  useEffect(() => {
    axios.get(path, params).then(res => {
      setData(res.data);
    });
  }, []);
  return data;
};

我们自定义了一个 Hook useGet,作用是使用 axios 的get 请求方式请求接口数据。

function App() {
  const data = useGet({ path: 'https://conduit.productionready.io/api/articles', params: {}});
  if (!data.articles) {
    return <div>
      请求中...
    </div>;
  }
  return (
    <div>
      {data.articles.map(item => <p>{item.title}</p>)}
    </div>
  );
}

使用时和使用内置 Hook 一样的形式使用自定义 Hook。这里我们通过 useGet 获取到接口中的文章数据,并将它们的标题展示在页面上。

十一、useImperativeHandle

1. useImperativeHandle 简介

useImperativeHandle 是 React 中的一个 Hook,它能让我们自定义由 ref 暴露出来的句柄(句柄就是程序、数据或地址表等的入口地址),可应用于父组件访问子组件的场景。

useImperativeHandle 有3个参数,第1个参数为从父组件传来的 ref,第2个参数是一个函数,它无需参数,返回要暴露的句柄组成的对象,第3个参数参数是一个数组,表示在哪些响应值(包括 props 、state 和所有在组件内部直接声明的变量和函数)变化时更新暴露的句柄。

import { useImperativeHandle } from "react";

2. 父子组件计数器案例

来做一个父组件访问子组件的计数器案例,本案例中需要点击父组件中的按钮,修改子组件中计数器的值。

我们首先想到可以使用 useRef Hook 来获取子组件的 ref,然后访问子组件的数据。

function App() {
  const counterRef = useRef();
  function click() {
    const { count, setCount } = counterRef.current || {};
    setCount(count + 1);
  }
  return (
    <>
      <Counter ref={counterRef}></Counter>
      <button onClick={() => click()}>按钮</button>
    </>
  );
}

function Counter() {
  const [count, setCount] = useState(0);
  return <>
    {count}
  </>;
}

上述代码中,App 为父组件,Counter 为子组件。我们在子组件中定义了一个状态变量 count,并将其展示到页面上。在父组件中使用 useref Hook 获取子组件的引用,点击按钮时调用子组件的 setCount 方法修改 count 的状态,实现计数器的功能。

但点击按钮时并不起作用,报错 setCount 不是一个函数。

这是因为函数组件在函数执行完毕后销毁,没有组件实例,因此无法获取 ref。要想将一些句柄以 ref 的形式暴露给父组件,需要使用 forwardRef。

3. forwardRef

forwardRef 是 react 的一个方法,允许组件使用 ref 将一个 DOM 节点暴露给父组件。

它有一个参数是 render 函数,组件的渲染函数。react 会调用该函数并传入父组件传递来的参数和 ref。返回的 JSX 将成为组件的输出。简单理解,render 函数就是一个函数组件的定义,它除了 props 参数,还有一个 ref 参数。

const Component = forwardRef(render);

props 就是从父组件传来的数据,ref 为父组件中使用 useRef Hook 定义的 ref。

可以将此 ref 转发给其他组件,或者将其传递给 useImperativeHandle。

接下来修改上面的计数器案例:

function App() {
  const counterRef = useRef();
  function click() {
    const { count, setCount } = counterRef.current || {};
    setCount(count + 1);
  }
  return (
    <>
      <Counter ref={counterRef}></Counter>
      <button onClick={click}>按钮</button>
    </>
  );
}

const Counter = forwardRef((props, ref) => {
  const [count, setCount] = useState(0);
  useImperativeHandle(ref, () => ({ count, setCount }), [count]);
  return <>
    {count}
  </>;
});

这里我们使用 forwardRef 来定义子组件,使用 useImperativeHandle 将 count 和 setCount 暴露给 ref,在父组件中就可以使用 ref 来访问子组件暴露的句柄。点击父组件中的按钮,子组件的 count 将自增。

4. 父子组件传值整理

到这里我已经讲解到多种父子组件传值的方法,这里统一整理一下。

4.1 父传子

(1) 使用 props。

(2) 使用 useContext。

4.2 子传父

(1) 使用父组件传来的 props 事件,类似 vue 中的 $emit 发射事件。

(2) 父组件访问子组件,使用 useImperativeHandle,类似 vue 中的 $refs。

  • 8
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晴雪月乔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值