React教程 – Hooks

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>

//         );

//     }

// }

处理机制

QQ20230408-143715@2x.png函数组件的每一次渲染(或者是更新),都是把函数(重新)执行,产生一个全新的”私有上下文”

  • 内部的代码也需要重新执行
  • 涉及的函数需要重新的构建(这些函数的作用域【函数执行的上级上下文】,是每一次执行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【基于异步操作与更新队列 ,实现状态的批处理】;在任何地方修改状态,都是采用异步编程的

异步执行
image.png

image.png

同步执行

image.png

函数更新

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

image.png

函数更新一次,结果为20

image.png

惰性初始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是不会执行的

回调函数中嵌套函数

image.png

示例

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>
    );
}

底层机制

useEffect的处理机制.jpg
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 触发时执行。

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

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. createHandle
    createHandle函数的返回值作为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 函数,对新老传递的属性做比较,如果不一致,才会把函数组件执行,如果一致,则不让子组件更新!!

子组件为类组件的情况

carbon (1).png

子组件为函数组件的情况

carbon.png

自定义Hook

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

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

carbon.png

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

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYNkrf0b' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片