loading

react基础(四)

Hook好强大 真香!

# react Hook

  1. Hook是React 16.8的新增特性。它可以让你在不编写class的情况下使用state以及其他的React特性。

  2. Hook是一些可以让你在函数组件里“钩入” React state生命周期等特性的函数。

  3. Hook 不能在 class 组件中使用。

# State Hook

简单示例:

import { useState } from "react";
import ReactDOM from 'react-dom';
function ExampleHook() {
    const [ count, setCount ] = useState(0);
    return (
        <div>
          <p>已点击{count}</p>
          <button onClick={() => setCount(count + 1)}>
            点击
          </button>
        </div>
    );
}

ReactDOM.render(
    <ExampleHook />,
    document.getElementById('root')
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

效果:

这里导入的useState就是一个Hook,通过在函数中调用会给组件添加一些内部state。注意点:

  • useState接收的唯一参数就是初始state,这个初始state参数只有在第一次渲染时会被用到,而且不同于this.state,使用useState时声明的state不一定要是一个对象
  • useState会返回一个当前状态和一个更新状态的函数更新状态的函数,通过数组解构我们调用useState的时候可以给state变量取任意名称,更新函数同理。

与class组件中state变量的区别:

  1. 声明方式不同,class中我们需要在构造函数中设置this.state;在函数组件中,通过调用useStateHook来声明。
  2. 读取方式不同,class中我们通过this.state.count读取我们声明在state中的count值;在函数组件中我可以直接使用count
  3. 更新state状态的方式不同,class中我们需要调用setState()来更新值;在函数组件中,我们需要调用useState时声明的更新函数。

# Effect Hook

Effect Hook 可以让你在函数组件中执行副作用操作,它跟class组件中的componentDidMountcomponentDidUpdatecomponentWillUnmount具有相同的用途,可以看成将它们合并成了一个API。

将上面的例子稍加改造:

import { useState, userEffect } from "react";
import ReactDOM from 'react-dom';
function ExampleHook() {
    const [ count, setCount ] = useState(0);
    useEffect(() => {
        console.log(`按钮被点击${count}`)
    });
    return (
        <div>
          <p>已点击{count}</p>
          <button onClick={() => setCount(count + 1)}>
            点击
          </button>
        </div>
    );
}

ReactDOM.render(
    <ExampleHook />,
    document.getElementById('root')
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

实现效果:

当你调用useEffect时,就是在告诉React在完成对DOM的更改后运行你的“副作用”函数。注意点:

  • 由于副作用函数是在组件内(函数作用域中)声明的,所以它们可以访问到组件的props和state。
  • 默认情况下,React会在每次渲染后调用副作用函数(包括第一次渲染的时候)。

# 无需清除的effect

有时候,我们只想在React更新DOM之后运行一些额外的代码。比如发送网络请求,手动变更DOM,记录日志,这些都是常见的无需清除的操作。上面的例子便是无需清除的。

# 需要清除的effect

我们在组件的操作,有些在组件销毁时是应该被清除的,例如订阅外部数据源,在class组件中,我们一般在componentDidMount生命周期中绑定 ,在componentWillUnmount生命周期内清除它。

在Hook中,由于添加和删除订阅的代码的紧密性,所以 useEffect 的设计是在同一个地方执行。effect函数还可以通过返回一个函数来指定如何“清除”副作用(可以同时清除多个),React将会在执行清除操作时调用它。例如,同样继续改造上面的例子:

import { Component, useState, userEffect } from "react";
import ReactDOM from 'react-dom';
function ExampleHook() {
    const [ count, setCount ] = useState(0);
    useEffect(() => {
        console.log(`按钮被点击${count}`)
        let interval = setInterval(() => {
            console.log('当前时间戳', new Date().getTime())
        }, 1000);
        return () => {
            clearInterval(interval)
        }
    });
    return (
        <div>
          <p>已点击{count}</p>
          <button onClick={() => setCount(count + 1)}>
            点击
          </button>
        </div>
    );
}
class App extends Component {
  constructor() {
    super();
    this.state = {
      hookComponentFlag: true
    };
  };
  changeHookFlag() {
    this.setState(newState => {
      return {
        hookComponentFlag: !newState.hookComponentFlag
      }
    })
  };
  render(){
    return (
      <div>
        <button onClick={this.changeHookFlag.bind(this)}>{ this.state.hookComponentFlag ? '移除' : '添加'}hook组件</button>
        {this.state.hookComponentFlag ? <ExampleHook /> : null}
      </div>
    )
  }
} 

ReactDOM.render(
    <App />,
    document.getElementById('root')
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

在这个示例中,React会在组件销毁时清除定时器,然后在后续渲染时重新执行副作用函数创建定时器。

效果:

# useEffect使用技巧

将useEffect用于一个单一功能

在React Hooks,可以有多个useEffect函数。这样我们可以简洁代码,每一个函数对应一个单一功能,同时将useEffect拆分为单独的用途函数,可以防止意外发生(使用依赖数组时)。 继续改动上面的例子:

import { Component, useState, userEffect } from "react";
function ExampleHook() {
    const [ count, setCount ] = useState(0);
    const [ number, setNumber ] = useState(0);
    useEffect(() => {
        console.log(`按钮被点击${count}`)
    })
    useEffect(() => {
        let interval = setInterval(() => {
            console.log('当前时间戳', new Date().getTime())
        }, 1000);
        return () => {
            clearInterval(interval)
        }
    });
    useEffect(() => {
        let interval = setInterval(() => {
            // setNumber(number + 1)
            setNumber(number => number + 1) //当变量依赖先前状态时建议这样使用
            console.log('当前数量', number)
        }, 1000);
        return () => {
            clearInterval(interval)
        }
    });
    return (
        <div>
          <p>已点击{count}</p>
          <button onClick={() => setCount(count + 1)}>
            点击
          </button>
        </div>
    );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

通过跳过 Effect 进行性能优化

在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevPropsprevState的比较逻辑解决:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    //进行操作
  }
}
1
2
3
4
5

useEffect的Hook API中同样支持,如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:

useEffect(() => {
  document.title = `已点击 ${count}`;
}, [count]); // 仅在 count 更改时更新
1
2
3

这里传入[count]作为第二个参数,当组件重新渲染的时候如果count的值没有改变,则React会跳过这个effect。

加条件执行Effect 有些业务条件下,除了比对值是否改变的情况,还有是否满足某些条件才执行Effect。

我们可以在Effect的头部添加条件返回:

function ExampleHook() {
    const [ number, setNumber ] = useState(0);
    useEffect(() => {
        if(number > 5) return;
        let interval = setInterval(() => {
            // setNumber(number + 1)
            setNumber(number => number + 1) //当变量依赖先前状态时建议这样使用
            console.log('当前数量', number)
        }, 1000);
        return () => {
            clearInterval(interval)
        }
    }, [number]);
    return (
        <div>
          <p>已点击{count}</p>
          <button onClick={() => setCount(count + 1)}>
            点击
          </button>
        </div>
    );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# Ref Hook

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。

# 常规用法

实例代码:

import React, { useState, useEffect, useMemo, useRef } from 'react';

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

  const doubleCount = useMemo(() => {
    return 2 * count;
  }, [count]);

  const couterRef = useRef();

  useEffect(() => {
    document.title = `The value is ${count}`;
    console.log(couterRef.current);
  }, [count]);
  
  return (
    <>
      <button ref={couterRef} onClick={() => {setCount(count + 1)}}>Count: {count}, double: {doubleCount}</button>
    </>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

上述代码使用useRef可以创建一个对象couterRef,将这个对象赋给组件的ref属性,就可以通过该对象的current属性访问DOM元素button

# 跨渲染周期保存数据

一般我们将组件的状态属性放在state中保存,但当我们改变state中的属性时,就会触发组件重新渲染。如果我们有更新数据而不触发组件重新渲染的需求时,就可以使用userRef来跨越渲染周期存储数据。

import React, { useState, useEffect, useMemo, useRef } from 'react';

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

  const doubleCount = useMemo(() => {
    return 2 * count;
  }, [count]);

  const timerID = useRef();
  
  useEffect(() => {
    timerID.current = setInterval(()=>{
        setCount(count => count + 1);
    }, 1000); 
  }, []);
  
  useEffect(()=>{
      if(count > 10){
          clearInterval(timerID.current);
      }
  });
  
  return (
    <>
      <button ref={couterRef} onClick={() => {setCount(count + 1)}}>Count: {count}, double: {doubleCount}</button>
    </>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

这里用ref对象的current属性来存储定时器的ID,这样便可以在多次渲染之后依旧保存定时器ID,从而能正常清除定时器。同时这里出现了一个新的hookuseMemo,我们下面说。

# 其他Hook

# useMemo

这里承接上面代码的useMemo

官网定义:函数返回一个 memoized 值。把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

总结一下就是:这个hook的功能是判断组件中的函数逻辑是否重新执行,用来优化性能。

  //第一个参数是要执行的函数
  //第二个参数是执行函数依赖的变量组成的数据
  //这里只有count发生变化double才会重新计算
const doubleCount = useMemo(() => {
    return 2 * count;
  }, [count]);
1
2
3
4
5
6

上述代码,只有当count的值发生改变了,doubleCount才会重新渲染,感觉有点类似vue中的computed属性。 可以将代码稍微改变一下

const doubleCount = useMemo(() => {
    return 2 * count;
  }, [count === 3]);

1
2
3
4

这样的话只有当count的值为3时,doubleCount才会重新渲染。

综上,只有当满足传入函数中第二个参数(依赖项数组)的条件满足时,这个hook创建的数据才能够被更改,减少频繁渲染来优化性能。

# useContext

可以让你不使用组件嵌套就可以订阅 React 的 Context。

function Example() {
  const locale = useContext(LocaleContext);
  const theme = useContext(ThemeContext);
  // ...
}
1
2
3
4
5

# useReducer

可以让你通过reducer来管理组件本地的复杂state。

function Example() {
  function Todos() {
  const [todos, dispatch] = useReducer(todosReducer);
  // ...
}
1
2
3
4
5

React 内置的 Hook API索引 (opens new window)

# 自定义Hook

自定义Hook是一个函数,其名称以use开头,函数内部可以调用其他的Hook。

示例:

function useVariable(porps) {
    // const value = 
    const [ variable, setVariabl] = useState(null);

     useEffect(() => {
        variable = setVariabl(porps)
    });

    return variable
}

function getValue(porps) {
    const value = useVariable(porps)
    if(value === null) {
        return 'error'
    }
    return value
}

function getResultByVariable(porps) {
    const value = useVariable(porps)
    if(value === null) {
        return 'waiting'
    }
    return (
        <div>
            {`结果为:${value === 'success''成功' : '失败'}`}
        </div>
    )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

上述组件getValuegetResultByVariable都使用了自定义Hook useVariable,但它们并没有共享state,每一次使用自定义Hook时,其中所有的state和副作用都是完全隔离的。

最近更新时间: 2021/10/25 17:21:36
最近更新
01
2021/10/23 00:00:00
02
2021/08/18 17:00:03
03
2021/07/04 15:26:36