likes
comments
collection
share

React教程 - Hooks

作者站长头像
站长
· 阅读数 17
import React, { useState } from 'react';
import { Button } from 'antd';
import './Demo.less';

// Hook实现
export default function UseStateDemo() {
    const [num, setNum] = useState(0);

    const handleAdd = () => {
        setNum(num + 10);
    };
    return (
        <div className="demo">
            <span className="num">{num}</span>
            <Button type="primary" size="small" onClick={handleAdd}>
                add
            </Button>
        </div>
    );
}

// 类组件实现
// export default class UseStateDemo extends React.Component {
//     state = {
//         n: 0,
//     };
//     handleAdd = () => {
//         let { n } = this.state;
//         this.setState({
//             n: n + 10,
//         });
//     };
//     render() {
//         let { n } = this.state;
//         return (
//             <div className="demo">
//                 <span className="num">{n}</span>
//                 <Button type="primary" size="small" onClick={this.handleAdd}>
//                     add
//                 </Button>
//             </div>
//         );
//     }
// }
  • 函数组件

    • 不具备“状态、ref、周期函数”等内容,第一次渲染完毕后,无法基于组件内部的操作来控制其更新,因此称之为静态组件!
    • 但是具备属性及插槽,父组件可以控制其重新渲染!
    • 渲染流程简单,渲染速度较快!
    • 基于FP(函数式编程)思想设计,提供更细粒度的逻辑组织和复用!
  • 类组件

    • 具备“状态、ref、周期函数、属性、插槽”等内容,可以灵活的控制组件更新,基于钩子函数也可灵活掌控不同阶段处理不同的事情!
    • 渲染流程繁琐,渲染速度相对较慢!
    • 基于OOP(面向对象编程)思想设计,更方便实现继承等!

React Hooks 组件,就是基于 React 中新提供的 Hook 函数,可以让函数组件动态化

Hook函数概览

Hooks API

useState

作用

在函数组件中使用状态,修改状态值可让函数组件更新,类似于类组件中的setState

语法

// 返回一个 state,以及更新 state 的函数
const [state, setState] = useState(initialState);

函数组件或者Hooks组件不是类组件,所以没有实例的概念【调用组件不再是创建类的实例,而是把函数执行,产生一个私有上下文而已】,因此在函数组件中不涉及this的处理

示例

import React, { useState } from 'react';
import { Button } from 'antd';
import './Demo.less';

// Hook实现
export default function UseStateDemo() {
    const [num, setNum] = useState(0);

    const handleAdd = () => {
        setNum(num + 10);
    };
    return (
        <div className="demo">
            <span className="num">{num}</span>
            <Button type="primary" size="small" onClick={handleAdd}>
                add
            </Button>
        </div>
    );
}

// 类组件实现
// export default class UseStateDemo extends React.Component {
//     state = {
//         n: 0,
//     };
//     handleAdd = () => {
//         let { n } = this.state;
//         this.setState({
//             n: n + 10,
//         });
//     };
//     render() {
//         let { n } = this.state;
//         return (
//             <div className="demo">
//                 <span className="num">{n}</span>
//                 <Button type="primary" size="small" onClick={this.handleAdd}>
//                     add
//                 </Button>
//             </div>
//         );
//     }
// }

处理机制

React教程 - Hooks 函数组件的每一次渲染(或者是更新),都是把函数(重新)执行,产生一个全新的"私有上下文"

  • 内部的代码也需要重新执行
  • 涉及的函数需要重新的构建(这些函数的作用域【函数执行的上级上下文】,是每一次执行DEMO的闭包)
  • 每一次执行DEMO函数,也会把useState重新执行,但是:
    • 执行useState,只有第一次设置的初始值会生效,其余以后再执行,获取的状态都是最新的状态值【而不是初始值】
    • 返回的修改状态的方法,每一次都是返回一个新的

细节处理

示例

import React, { useState } from 'react';
import { Button } from 'antd';
import './Vote.less';

const Vote = function Vote(props) {
    let [num, setNum] = useState({
        surNum: 0,
        oppNum: 0,
    });
    const handleClick = (flag) => {
        switch (flag) {
            case 'sur':
                setNum({
                    ...num,
                    surNum: num.surNum + 1,
                });
                break;
            case 'opp':
                setNum({
                    ...num,
                    oppNum: num.oppNum + 1,
                });
                break;
            default:
                break;
        }
    };
    return (
        <div className="vote-box">
            <div className="header">
                <h2 className="title">{props.title}</h2>
                <span className="num">{num.surNum + num.oppNum}</span>
            </div>
            <div className="main">
                <p>支持人数:{num.surNum}人</p>
                <p>反对人数:{num.oppNum}人</p>
            </div>
            <div className="footer">
                <Button
                    type="primary"
                    onClick={() => {
                        handleClick('sur');
                    }}
                >
                    支持
                </Button>
                <Button
                    type="primary"
                    danger
                    onClick={() => {
                        handleClick('opp');
                    }}
                >
                    反对
                </Button>
            </div>
        </div>
    );
};

export default Vote;

官方建议:需要多个状态,就把useState执行多次即可

import React, { useState } from 'react';
import { Button } from 'antd';
import './Vote.less';

/* 官方建议是:需要多个状态,就把useState执行多次即可 */
const Vote = function Vote(props) {
    let [surNum, setSurNum] = useState(0);
    let [oppNum, setOppNum] = useState(0);

    const handleClick = (flag) => {
        switch (flag) {
            case 'sur':
                setSurNum(surNum + 1);
                break;
            case 'opp':
                setOppNum(oppNum + 1);
                break;
            default:
                break;
        }
    };
    return (
        <div className="vote-box">
            <div className="header">
                <h2 className="title">{props.title}</h2>
                <span className="num">{surNum + oppNum}</span>
            </div>
            <div className="main">
                <p>支持人数:{surNum}人</p>
                <p>反对人数:{oppNum}人</p>
            </div>
            <div className="footer">
                <Button
                    type="primary"
                    onClick={() => {
                        handleClick('sur');
                    }}
                >
                    支持
                </Button>
                <Button
                    type="primary"
                    danger
                    onClick={() => {
                        handleClick('opp');
                    }}
                >
                    反对
                </Button>
            </div>
        </div>
    );
};

export default Vote;

同步异步

在React18中,基于useState创建出来的"修改状态的方法",它们的执行也是异步的;原理等同于类组件中的this.setState【基于异步操作与更新队列 ,实现状态的批处理】;在任何地方修改状态,都是采用异步编程的

异步执行 React教程 - Hooks

React教程 - Hooks

同步执行

React教程 - Hooks

函数更新

  • useState自带了性能优化的机制:
    • 每一次修改状态值的时候,会拿最新要修改的值和之前的状态值做比较【基于 Object.is 作比较】
    • 如果发现两次的值是一样的,则不会修改状态,也不会让视图更新【可以理解为:类似于 PurComponent,在 shouldComponentUpdate 中做了比较和优化】

React教程 - Hooks

函数更新一次,结果为20

React教程 - Hooks

惰性初始state

如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用!

import React, { useState } from "react";

export default function Demo(props) {
    let [num, setNum] = useState(() => {
        let { x, y } = props;
        return x + y;
    });
    return <div>
        <span>{num}</span>
    </div>;
} ;

优化机制

调用 State Hook 的更新函数,并传入当前的 state 时,React 将跳过组件的渲染(原因:React 使用 Object.is 比较算法,来比较新老 state;注意不是因为DOM-DIFF;)!

import React, { useState } from "react";

export default function Demo() {
    console.log('render');
    let [num, setNum] = useState(10);
    return <div>
        <span>{num}</span>
        <button onClick={() => {
            setNum(num);
        }}>处理</button>
    </div>;
};

useEffect

作用

在函数组件中使用生命周期函数

语法

useEffect(callback)

  • 第一次渲染完毕后,执行 callback,等同于 componentDidMount
  • 在组件每一次更新完毕后,也会执行 callback,等同于 componentDidUpdate

useEffect(callback,[])

  • 只有第一次渲染完毕后,才会执行callback,每一次视图更新完毕后,callback不再执行,类似于 componentDidMount

useEffect(callback,[依赖的状态(多个状态)])

  • 第一次渲染完毕会执行callback
  • 当依赖的状态值(或者多个依赖状态中的一个)发生改变,也会触发callback执行
  • 但是依赖的状态如果没有变化,在组件更新的时候,callback是不会执行的

回调函数中嵌套函数

React教程 - Hooks

示例

import React, { useState, useEffect } from 'react';
import { Button } from 'antd';
import './Demo.less';

export default function UseStateDemo() {
    const [num, setNum] = useState(0);

    useEffect(() => {
        console.log('回调函数', num);
    });

    useEffect(() => {
        console.log('空数组', num);
    }, []);

    useEffect(() => {
        console.log('设置依赖', num);
    }, [num]);

    useEffect(() => {
        return () => {
            // num 获取的是上一个状态值
            console.log('回调函数中嵌套函数', num);
        };
    }, [num]);

    const handleAdd = () => {
        setNum(num + 1);
    };
    return (
        <div className="demo">
            <span className="num">{num}</span>
            <Button type="primary" size="small" onClick={handleAdd}>
                add
            </Button>
        </div>
    );
}

底层机制

React教程 - Hooks useEffect 在依赖变化时,执行回调函数。这个变化,是「本次 render 和上次 render 时的依赖比较」;因此我们需要:

  • 存储依赖,上一次 render 的
  • 兼容多次调用
  • 比较依赖,执行回调函数

实现:

const lastDepsBoxs = [];
let index = 0;

const useEffect = (callback, deps) => {
    const lastDeps = lastDepsBoxs[index];
    const changed =
        !lastDeps   // 首次渲染,肯定触发
        || !deps    // deps 不传,次次触发
        || deps.some((dep, index) => dep !== lastDeps[index]);  // 正常比较
        
    if (changed) {
        lastDepsBoxs[index] = deps;
        callback();
    }
    index ++;
};

增加副作用清除

effect 触发后会把清除函数暂存起来,等下一次 effect 触发时执行。

React教程 - Hooks

明确这个顺序就不难实现了

const lastDepsBoxs = [];
const lastClearCallbacks = [];
let index = 0;

const useEffect = (callback, deps) => {
    const lastDeps = lastDepsBoxs[index];
    const changed = !lastDeps || !deps || deps.some((dep, index) => dep !== lastDeps[index]); 
    
    if (changed) {
        lastDepsBoxs[index] = deps;
        if (lastClearCallbacks[index]) {
            lastClearCallbacks[index]();
        }
        lastClearCallbacks[index] = callback();
    }
    index ++;
};

总结

  • 利用闭包,useState / useEffect 的实现并不深奥
  • 巧妙的是对多次调用的组织方式
  • 使用 hooks 要避免 if、for 等嵌套

useLayoutEffect

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。 可以使用它来读取 DOM 布局并同步触发重渲染。 在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。 尽可能使用标准的 useEffect 以避免阻塞视觉更新

import { useState, useLayoutEffect } from 'react';

export default function App() {
    const [count, setCount] = useState(0);

    useLayoutEffect(() => {
        console.log(`useLayoutEffect - count=${count}`);
        // 耗时的操作
        const pre = Date.now();
        while (Date.now() - pre < 500) {}

        if (count === 0) {
            setCount(10 + Math.random() * 200);
        }
    }, [count]);

    return <div onClick={() => setCount(0)}>{count}</div>;
}
  • useLayoutEffect会阻塞浏览器渲染真实DOM,优先执行Effect链表中的callback;
  • useEffect不会阻塞浏览器渲染真实DOM,在渲染真实DOM的同时,去执行Effect链 表中的callback;
    • useLayoutEffect设置的callback要优先于useEffect去执行! !
    • 在两者设置的callback中,依然可以获取DOM元素「原因:真实DOM对象已经创建了,区别只是浏览器是否渲染」
    • 如果在callback函数中又修改了状态值「视图又要更新」
    • useEffect :浏览器肯定是把第一次的真实已经绘制了,再去渲染第二次真实DOM
    • useLayoutEffect :浏览器是把两次真实DOM的渲染,合并在一起渲染的

视图更新的步骤

  • 第一步:基于babel-preset-react-app把JSX编译为createElement格式

  • 第二步:把createElement执行,创建出virtualDOM

  • 第三步:基于root. render方法把virtua LDOM变为真实DOM对象「DOM-DIFF」

    • useLayoutEffect阻塞第四步操作,先去执行Effect链表中的方法「同步操作」

    • useEffect第四步操作和Effect链表中的方法执行,是同时进行的「异步操作」

  • 第四步:浏览器渲染和绘制真实DOM对象

useRef

函数组件中,可以基于 useRef Hook 函数,创建一个ref对象

  • React.createRef 也是创建 ref 对象,既可在类组件中使用,也可以在函数组件中使用
  • useRef 只能在函数组件中使用【所有的ReactHook函数,都只能在函数组件中使用,在类组件中使用会报错】 示例
import React from 'react';
import { useState, useEffect } from 'react';
import { Button } from 'antd';
import '../Demo.less';
import { useRef } from 'react';

export default function UseStateDemo() {
    const [num, setNum] = useState(0);
    let box = useRef(null)

    useEffect(() => {
        console.log(box.current);
    },[]);

    return (
        <div className="demo">
            <span className="num" ref={box}>{num}</span>
            <Button type="primary" size="small">
                add
            </Button>
        </div>
    );
}

React.createRef在函数组件中依然可以使用!

  • createRef 每次渲染都会返回一个新的引用
  • 而 useRef 每次都会返回相同的引用
import React from 'react';
import { useState, useEffect } from 'react';
import { Button } from 'antd';
import '../Demo.less';
import { useRef } from 'react';

let prev1, prev2;
export default function UseStateDemo() {
    const [num, setNum] = useState(0);
    let box1 = useRef(null),
        box2 = React.createRef();

    if (!prev1) {
        prev1 = box1;
        prev2 = box2;
    } else {
        console.log(prev1 === box1);  // true   useRef再每一次组件更新的时候(函数重新执行),再次执行useRef方法的时候,不会创建新的REF对象了,获取到的还是第一次创建的那个REF对象!

        console.log(prev2 === box2);  // false  createRef在每一次组件更 新的时候,都会创建一个全新的REF对象出来,比较浪费性能!
    }

    useEffect(() => {
        console.log(box1.current);
        console.log(box2.current);
    }, []);

    return (
        <div className="demo">
            <span className="num" ref={box1}>
                {num}
            </span>
            <span className="num" ref={box2}>
                {num + 11111}
            </span>
            <Button
                type="primary"
                size="small"
                onClick={() => {
                    setNum(num + 1);
                }}
            >
                add
            </Button>
        </div>
    );
}

useImperativeHandle

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。通常与forwardRef一起使用,暴露之后父组件就可以通过 selectFileModalRef.current?.handleCancel();来调用子组件的暴露方法

语法

useImperativeHandle(ref, createHandle, [deps])
  1. ref 需要被赋值的ref对象。
  2. createHandlecreateHandle函数的返回值作为ref.current的值。
  3. [deps] 依赖数组,依赖发生变化会重新执行createHandle函数。
useImperativeHandle(ref, () => ({
    handleShowModal,
    handleCancel,
}));

useMemo

在前端开发的过程中,我们需要缓存一些内容,以避免在需渲染过程中因大量不必要的耗时计算而导致的性能问题。为此 React 提供了一些方法可以帮助我们去实现数据的缓存,useMemo 就是其中之一!

let xxx = useMemo( callback, [dependencies])

  • 第一次渲染组件的时候,callback会执行
  • 后期只有依赖的状态值发生改变, callback才会再执行
  • 每一次会把callback执行的返回结果赋值给xxx
  • useMemo具 备“计算缓存",在依赖的状态值没有发生改变,callback没有触发执行的时候,xxx获取的是上- -次计算出来的结果

和Vue中的计算属性非常的类似! !

import React, { useState, useMemo } from 'react';
import { Button } from 'antd';
import './Vote.less';

const Vote = function Vote(props) {
    let [surNum, setSurNum] = useState(0);
    let [oppNum, setOppNum] = useState(0);
    let [otherOption, setOtherOption] = useState(0);

    let radio = useMemo(() => {
        console.log('OK');
        let total = surNum + oppNum,
            radio = '--';
        if (total > 0) {
            radio = ((surNum / total) * 100).toFixed(0) + '%';
            return radio
        }
    }, [surNum, oppNum]);

    return (
        <div className="vote-box">
            <div className="header">
                <span className="num">{surNum + oppNum}</span>
            </div>
            <div className="main">
                <p>支持人数:{surNum}人</p>
                <p>反对人数:{oppNum}人</p>
                <p>支持比率:{radio}</p>
                <p>其他操作:{otherOption}</p>
            </div>
            <div className="footer">
                <Button
                    type="primary"
                    onClick={() => {
                        setSurNum(surNum + 1);
                    }}
                >
                    支持
                </Button>
                <Button
                    type="primary"
                    danger
                    onClick={() => {
                        setOppNum(oppNum + 1);
                    }}
                >
                    反对
                </Button>
                <Button
                    type="primary"
                    onClick={() => {
                        setOtherOption(otherOption + 1);
                    }}
                >
                    其他操作
                </Button>
            </div>
        </div>
    );
};

export default Vote;

useCallback

const xxx = useCallback(callback,[dependencies])

  • 组件第一次渲染,useCallback执行,创建一个函数callback,赋值给xxx
  • 组件后续每一次更新,判断依赖的状态值是否改变,如果改变,则重新创建新的函数堆,赋值给xxx;如果依赖的状态值没有更新(或者没有设置依赖“[]”)则xxx获取的一直是第一次创建的函数堆,不会创建新的函数出来!
  • 或者说,基于useCallback,可以始终获取第一次创建函数的堆内存地址(或者说函数的引用)

诉求:当父组件更新的时候,因为传递给子组件的属性仅仅是一个函数「特点:基本应该算是不变的」,所以不想再让子组件也跟着更新了!

  • 第一条:传递给子组件的属性(函数),每一次需要是相同的堆内存地址(是一致的) . 基于useCallback处理!
  • 第二条:在子组件内部也要做一个处理,验证父组件传递的属性是否发生改变,如果没有变化,则让子组件不能更新,有变化才需要更新;
  • 类组件是通过继承React.PureComponent即可「在shouldComponentUpdate中对新老属性做了浅比较」!!
  • 函数组件是基于 React.memo 函数,对新老传递的属性做比较,如果不一致,才会把函数组件执行,如果一致,则不让子组件更新!!

子组件为类组件的情况

React教程 - Hooks

子组件为函数组件的情况

React教程 - Hooks

自定义Hook

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

与组件中一致,请确保只在 自定义 hook 的顶层无条件地调用其他 hook

React教程 - Hooks

自定义 Hooks 的核心是共享组件之间的逻辑。使用自定义 Hooks 能够减少重复的逻辑,更重要的是,自定义 Hooks 内部的代码描述了它们想做什么,而不是如何做。当你将逻辑提取到自定义Hooks 中时,你可以隐藏如何处理某些"外部系统"或浏览器 API 的调用的细节,组件的代码表达的是你的意图,而不是实现细节