React hooks

242 阅读6分钟

React组件的挂载和更新顺序

挂载

import React, { useEffect } from "react";

const GrandFather: React.FC = ({ children }) => {
    useEffect(() => {
        console.log("爷爷挂载了");
    }, []);

    return (
        <div>
            我是爷爷
            {children}
        </div>
    );
};

const Father: React.FC = ({ children }) => {
    useEffect(() => {
        console.log("爸爸挂载了");
    }, []);

    return (
        <div>
            我是爸爸
            {children}
        </div>
    );
};

const Me: React.FC = () => {
    useEffect(() => {
        console.log("孙子挂载了");
    }, []);

    return <div>我是孙子</div>;
};

const Demo: React.FC = () => {
    useEffect(() => {
        console.log("老祖宗挂载了");
    }, []);

    return (
        <div>
            <GrandFather>
                <Father>
                    <Me />
                </Father>
            </GrandFather>
        </div>
    );
};

export default Demo;

结果:

image.png

结论:React组件的挂载顺序为: 子孙组件 -> 当前组件 -> 根组件

更新

import React, { useEffect, useState } from "react";

const GrandFather: React.FC<{ random: number }> = ({ children, random }) => {
    console.log("我是爷爷");

    useEffect(() => {
        console.log("%c爷爷更新了", "color: red");
    }, [random]);

    return (
        <div>
            我是爷爷, {random}
            {children}
        </div>
    );
};

const Father: React.FC<{ random: number }> = ({ children, random }) => {
    console.log("我是爸爸");

    useEffect(() => {
        console.log("%c爸爸更新了", "color: red");
    }, [random]);

    return (
        <div>
            我是爸爸, {random}
            {children}
        </div>
    );
};

const Me: React.FC = () => {
    console.log("我是孙子");
    console.log("\n");

    useEffect(() => {
        console.log("孙子更新了");
    }, []);

    return <div>我是孙子</div>;
};

const Demo: React.FC = () => {
    console.log("我是老祖宗");

    const [randNum, setRandNum] = useState(Math.random());

    useEffect(() => {
        console.log("老祖宗挂载了");
    }, []);

    return (
        <div>
            <button
                type="button"
                onClick={() => {
                    setRandNum(Math.random());
                }}
            >
                点我
            </button>
            <GrandFather random={randNum}>
                <Father random={randNum}>
                    <Me />
                </Father>
            </GrandFather>
        </div>
    );
};

export default Demo;

结果:

image.png

结论:组件的更新顺序同组件的挂载顺序,也为 子孙组件 -> 当前组件,组件触发更新,则该组件及以下的所有组件将会触发更新

问题: 没有状态或属性变更的孙子组件也被调用了

React有哪些框架内提供的hook?

  • useState
  • useEffect
  • useContext
  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImperativeHadle
  • useLayoutEffect
  • useDebugValue

useState

  • 什么是? 函数组件中使用状态的一种方式
  • 何时用? 在编写需要状态渲染的函数组件时使用
  • 怎么用? 第一种:直接传入初始值
const [state, setState] = useState(initialState);

第二种:传入一个初始值初始化的函数(惰性初始化)

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

setState的两种方式:

  1. 直接传入更新值
  2. 在使用到依赖旧值得出新值时传入一个函数,返回新值
  • 和class组件有啥不同?

情景一:class组件与函数组件更新状态时传入相同的值

import React from "react";

export default class Demo extends React.Component<any, { random: number }> {
    constructor(props: any) {
        super(props);

        this.state = {
            random: Math.random(),
        };
    }

    changeState = () => {
        this.setState({
            random: 1,
        });
    };

    render() {
        console.log("%c rendered...", "color: red");
        const { random } = this.state;

        return (
            <div>
                <button type="button" onClick={this.changeState}>
                    点我
                </button>
                <p>{random}</p>
            </div>
        );
    }
}

结果>>:

image.png

import React from "react";

export default () => {
    console.log("%c rendered...", "color: red");
    const [num, setNum] = React.useState(Math.random());

    return (
        <div>
            <button type="button" onClick={() => setNum(1)}>
                点我
            </button>
            <div>{num}</div>
        </div>
    );
};

结果>>:

image.png

为什么一摸一样的组件,会产生这样的差异?

原因: image.png

结论:class组件的this.setState每次都会触发更新,即使是相同的state。而hooks的setState会经过比较算法判断,相同的state不会触发更新

知道这个差异对我们的工作有什么指导意义?

import React from "react";

export default () => {
    console.log("%c rendered...", "color: red");
    const [num, setNum] = React.useState({ a: Math.random() });

    return (
        <div>
            <button
                type="button"
                onClick={() => {
                    num.a = Math.random();
                    setNum(num);
                }}
            >
                点我
            </button>
            <div>{num.a}</div>
        </div>
    );
};

image.png

import React from "react";

export default class Demo extends React.Component<any, { num: { a: number } }> {
    constructor(props: any) {
        super(props);

        this.state = {
            num: {
                a: Math.random(),
            },
        };
    }

    changeState = () => {
        const { num } = this.state;
        num.a = Math.random();
        this.setState({
            num,
        });
    };

    render() {
        console.log("%c rendered...", "color: red");
        const { num } = this.state;

        return (
            <div>
                <button type="button" onClick={this.changeState}>
                    点我
                </button>
                <p>{num.a}</p>
            </div>
        );
    }
}

image.png

情景二:更新状态时传入状态对象的部分属性

import React from "react";

interface IState {
    a: number;
    b: number;
    c: number;
}

export default class Demo extends React.Component<any, IState> {
    constructor(props: any) {
        super(props);

        this.state = {
            a: 1,
            b: 2,
            c: 3,
        };
    }

    render() {
        console.log("rendered...");
        return (
            <div>
                <button
                    type="button"
                    onClick={() => {
                        this.setState({ a: 4 });
                        this.setState({ b: 5 });
                        this.setState({ c: 6 });
                    }}
                >
                    点我
                </button>
                <p>state keys: </p>
                <pre>{JSON.stringify(this.state, null, 2)}</pre>
            </div>
        );
    }
}

image.png

import React, { useState } from "react";

interface IState {
    a: number;
    b: number;
    c: number;
}

export default () => {
    console.log("rendered...");
    const [state, setState] = useState<Partial<IState>>({
        a: 1,
        b: 2,
        c: 3,
    });

    return (
        <div>
            <button
                type="button"
                onClick={() => {
                    setState({ a: 4 });
                    setState({ b: 5 });
                    setState({ c: 6 });
                }}
            >
                点我
            </button>
            <p>state keys: </p>
            <pre>{JSON.stringify(state, null, 2)}</pre>
        </div>
    );
};

image.png

结论:class组件setState会自动合并更新对象,而hook setState不会,每次传入的都作为一个新的state

知道这个差异对我们的工作有什么指导意义?

直接影响到编码逻辑的实现

useEffect

import React, { useEffect, useState } from "react";

export default () => {
    const [num, setNum] = useState(Math.random());

    useEffect(() => {
        console.log("加载或更新了");

        return () => {
            console.log("清除副作用");
        };
    });

    return (
        <div>
            <button type="button" onClick={() => setNum(Math.random())}>
                点我
            </button>
            <p>{num}</p>
        </div>
    );
};

image.png

effect返回的副作用清除函数,在没有设置依赖或依赖不为空且发生变化时时,每次更新后下一次effect执行前会被执行;在设置依赖为空时,副作用清除函数会在组件被卸载时被执行。

useContext

使用频率相对较低,分享一个使用context默认值和属性值覆盖的写法

import React from "react";

const CustomContext = React.createContext('defaultContextValue');

const Demo: React.FC<{customValue: any}> = ({children, customValue}) => {
    return (
        <CustomContext.Consumer>
            {(value) => <CustomContext.Provider value={customValue || value}>{children}</CustomContext.Provider>}
        </CustomContext.Consumer>
    );
}

export default Demo;

useReducer

需要熟悉redux, 除useState外,另一种使用state的方式

import React, { useReducer } from "react";

export default () => {
    const [count, dispatch] = useReducer((x) => x + 1, 0);

    return (
        <div>
            <button type="button" onClick={dispatch}>
                点我
            </button>
            <p>{count}</p>
        </div>
    );
};

useCallback

函数组件中函数属性的两种写法:

  • 写法一
import React, { useReducer } from "react";

const Child: React.FC<{ onClick: () => void }> = ({ onClick }) => {
    console.log("rendered...");

    return (
        <button type="button" onClick={onClick}>
            点我
        </button>
    );
};

export default () => {
    const [num, dispatch] = useReducer((x) => x + 1, 0);

    const btnHandle = () => {
        console.log(111);
    };

    return (
        <div>
            Parent, {num}
            <button type="button" onClick={dispatch}>
                点我呀
            </button>
            <Child
                onClick={btnHandle}
            />
        </div>
    );
};
  • 写法二
import React, { useReducer } from "react";

const Child: React.FC<{ onClick: () => void }> = React.memo(({ onClick }) => {
    console.log("rendered...");

    return (
        <button type="button" onClick={onClick}>
            点我
        </button>
    );
});

export default () => {
    const [num, dispatch] = useReducer((x) => x + 1, 0);

    return (
        <div>
            Parent, {num}
            <button type="button" onClick={dispatch}>
                点我呀
            </button>
            <Child
                onClick={() => {
                    console.log(num);
                }}
            />
        </div>
    );
};

两种写法无本质区别,都会在组件更新时重新生成一个新的函数

在某些场景下会引起不必要的渲染,useCallback就是用来生成一个不变的函数

import React, { useReducer, useCallback } from "react";

const Child: React.FC<{ onClick: () => void }> = React.memo(({ onClick }) => {
    console.log("rendered...");

    return (
        <button type="button" onClick={onClick}>
            点我
        </button>
    );
});

export default () => {
    const [num, dispatch] = useReducer((x) => x + 1, 0);

    return (
        <div>
            Parent, {num}
            <button type="button" onClick={dispatch}>
                点我呀
            </button>
            <Child
                onClick={useCallback(() => {
                    console.log(num);
                }, [])}
            />
        </div>
    );
};

什么是useCallback的依赖?

image.png

useCallback的依赖是指在函数中用到的组件的state

那是不是应该把所有函数属性都用useCallback进行包裹呢?

答案显示是否定的,我们可以参考不要过度使用useCallback()一文的分析

image.png

useMemo

import React, { useState } from "react";

const User: React.FC<{ info: { name: string; age: number } }> = React.memo(
    ({ info }) => {
        console.log("rendered...");
        return (
            <div>
                {info.name}的年纪是{info.age}
            </div>
        );
    }
);

const Demo: React.FC = () => {
    const [count] = useState(0);
    const [value, setValue] = useState(0);
    const userInfo = {
        name: "Jack",
        age: count,
    };

    return (
        <div>
            <div>
                <User info={userInfo} />
            </div>
            <div>
                {value}
                <button
                    type="button"
                    onClick={() => {
                        setValue(value + 1);
                    }}
                >
                    点我
                </button>
            </div>
        </div>
    );
};

export default Demo;

image.png

子组件使用了memo,没有依赖value,只是依赖了count,但是结果每次父组件修改了value的值后,虽然子组件没有依赖value,而且使用了memo包裹,还是每次都重新渲染了

使用useMemo后:

const userInfo = React.useMemo(
    () => ({
        name: "Jack",
        age: count,
    }),
    [count]
);

image.png

useMemo用于在渲染期间高开销的计算,减少不必要的组件调和,从而实现性能优化的目的

useRef

useRef是非常有用的hook,除了可以将其返回值传递给ref属性获取一个class组件实例或者一个DOM元素外,由于其current属性可以保存具有记忆能力的任意值,我们可以利用到实现很多功能。

比如,获取一个变量的新旧值:

import React, { useState, useEffect, useRef } from "react";

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

    const prevCountRef = useRef(0);
    useEffect(() => {
        prevCountRef.current = count;
    });
    const prevCount = prevCountRef.current;

    return (
        <div>
            <button type="button" onClick={() => setCount((c) => c + 1)}>
                点我
            </button>
            <h1>
                Now: {count}, before: {prevCount}
            </h1>
        </div>
    );
};

image.png

useImperativeHandle

import React, {
    forwardRef,
    useImperativeHandle,
    useRef,
    useEffect,
} from "react";

const Input = (props: any, ref: any) => {
    const inputRef: React.LegacyRef<HTMLInputElement> = useRef(null);
    useImperativeHandle(ref, () => ({
        focus: () => {
            inputRef.current?.focus();
        },
    }));

    return <input type="text" ref={inputRef} />;
};

const FancyInput = forwardRef<HTMLInputElement, {}>(Input);

export default () => {
    const inputRef = useRef(null);

    useEffect(() => {
        if (inputRef.current) {
            // @ts-ignore
            inputRef.current.focus();
        }
    }, []);

    return <FancyInput ref={inputRef} />;
};

使用useImperativeHandle,我们可以定义一些api给父组件调用,这些api相当于函数组件对外的接口

自定义hook

什么是?

自定义hook就是使用了react hooks的函数

何时用?

当我们在项目开发过程中使用了react hooks,并在多处重复的逻辑即可抽取为一个自定义hook

例子:

function usePrevious(value) { 
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);  
  return <h1>Now: {count}, before: {prevCount}</h1>;
}