Reat Hooks 【进阶2】

328 阅读7分钟

useState

设置初始值时应该注意的问题

useState 设置初始值时,如果初始值是个值,可以直接设置,如果是个函数返回值,建议使用回调函数的方式设置

  • 初始值直接设置为函数返回值
const initCount = c => {
  console.log('initCount 执行');
  return c * 2;
}; 
function Counter() { 
  const [count, setCount] = useState(initCount(0)); 
  return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}

存在问题 即便 Counter 组件重新渲染时没有再给 count 重新赋初始值,但是 initCount 函数却会重复执行

  • 🌹 初始值设置为函数函数回调
const initCount = ...

function Counter() { 
  const [count, setCount] = useState(()=>initCount(0)); 
  
  ...
}

initCount 函数只会在 Counter 组件初始化的时候执行,之后无论组件如何渲染,initCount 函数都不会再执行

与 setState的区别

  1. state改变时
    • useState 修改 state 时,同一个 useState 声明的值会被 覆盖处理,多个 useState 声明的值会触发 多次渲染
    • setState 修改 state 时,多次 setState 的对象会被 合并处理
    • useState 修改 state 时,设置相同的值,函数组件不会重新渲染,而继承 Component 的类组件,即便 setState 相同的值,也会触发渲染

与 useReducer 的区别

  1. 修改状态时
    • useState 修改状态时,同一个 useState 声明的状态会被覆盖处理
    • useReducer 修改状态时,多次 dispatch 会按顺序执行,依次对组件进行渲染
    const [count, setCount] = useState(0);

    return (
        <p
            onClick={() => {
                setCount(count + 1);
                setCount(count + 2);
            }}
        >
            clicked {count} times
        </p>
    );
}

image.png

function Counter() {
   const [count, dispatch] = useReducer((x, payload) => x + payload, 0);        return (
      <p 
         onClick={() => { 
           dispatch(1); 
           dispatch(2); 
         }} 
      >
         clicked {count} times
      </p> 
   ); 
}

image.png

effect 的依赖频繁变化,该怎么办?

  1. 使用 setState 的函数式更新形式
function Counter() {
 const [count, setCount] = useState(0);

 useEffect(() => {
   const id = setInterval(() => {
     setCount(count + 1); // 这个 effect 依赖于 `count` state    }, 1000);
   return () => clearInterval(id);
 }, []); // 🔴 Bug: `count` 没有被指定为依赖
 return <h1>{count}</h1>;
}

传入空的依赖数组 [], hook 只在组件挂载时运行一次,并非重新渲染时。在 setInterval 的回调中,count 的值不会发生变化。因为当 effect 执行时,我们会创建一个闭包,并将 count 的值被保存在该闭包当中,且初值为 0。每隔一秒,回调就会执行 setCount(0 + 1)count是1。

指定 [count] 作为依赖列表能修复这个 Bug,但会导致每次改变发生时定时器都被重置。事实上,每个 setInterval 在被清除前(类似于 setTimeout)都会调用一次。但这并不是我们想要的。

要解决这个问题,我们可以使用 setState 的函数式更新形式。它允许我们指定 state 该 如何 改变而不用引用 当前 state:

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量    }, 1000);
    return () => clearInterval(id);
  }, []); // ✅ 我们的 effect 不适用组件作用域中的任何变量
  1. 在一些更加复杂的场景中(比如一个 state 依赖于另一个 state)用 useReducer  把 state 更新逻辑移到 effect 之外。
  2. 使用 ref来保存一个可变的变量(类似 class 中的 this 的功能),对它进行读写。
    不建议,因为依赖于变更会使得组件更难以预测。

useRef、createRef的区别及使用,及useRef妙用

useRef基本用法: 在函数组件中操作DOM (比如获取子组件的state/方法)

  • 不使用 useRef 的情况下,每一帧里的 state 值是如何打印的
function Counter() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        const handleClick = function() {
            console.log('count: ', count);
        }
        window.addEventListener('click', handleClick, false)
    });

    return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
复制代码

打印结果:

image.png

保证在函数组件的每一帧里访问到的 state 值是相同的

  • 使用 useRef 之后,每一帧里的 ref 值是如何打印的
function Counter() {
    const [count, setCount] = useState(0);
    const countRef = useRef(count);

    useEffect(() => {
        // 将最新 state 设置给 countRef.current
        countRef.current = count;
        const handleClick = function () {
            console.log('count: ', countRef.current);
        };
        window.addEventListener('click', handleClick, false);
    });

    return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}

打印结果

image.png

——>

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

    countRef.current = count;
    const handleClick = function () {
        console.log('count: ', countRef.current);
    };

    useEffect(() => {
        window.addEventListener('click', handleClick, false);
    }, []);

    return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}

image.png

改进版 提取

const useStab = (fn) => {
    const ref = useRef({});
    ref.current = fn;
    return useCallback(() => {
        ref.current();
    }, []);
};

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

    const add = () => {
        setCount(count + 1);
    };

    const hanldeClick = useStab(() => {
        console.log("count", count);
    });

    useEffect(() => {
        window.addEventListener("click", hanldeClick);
    }, []);

    return (
        <div className="App">
            <h1>{count}</h1>
            <button onClick={add}>Add</button>
        </div>
    );
}

image.png

保存住函数组件实例的属性

函数组件是没有实例的,因此属性也无法挂载到 this 上。那如果我们想创建一个非 stateprops 变量,能够跟随函数组件进行创建销毁,该如何操作呢?

同样的,还是可以通过 useRefuseRef 不仅可以作用在 DOM 上,还可以将普通变量转化成带有 current 属性的对象

比如,我们希望设置一个 Model 的实例,在组件创建时,生成 model 实例,组件销毁后,重新创建,会自动生成新的 model 实例

class Model {
    constructor() {
        console.log('创建 Model');
        this.data = [];
    }
}

function Counter() {
    const [count, setCount] = useState(0);
    const countRef = useRef(new Model());

    return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
复制代码

按照这种写法,可以实现在函数组件创建时,生成 Model 的实例,挂载到 countRefcurrent 属性上。重新渲染时,不会再给 countRef 重新赋值。

也就意味着在组件卸载之前使用的都是同一个 Model 实例,在卸载之后,当前 model 实例也会随之销毁。

仔细观察控制台的输出,会发现虽然 countRef 没有被重新赋值,但是在组件在重新渲染时,Model 的构造函数却依然会多次执行

所以此时我们可以借用 useState 的特性,改写一下。

class Model {
    constructor() {
        console.log('创建 Model');
        this.data = [];
    }
}

function Counter() {
    const [count, setCount] = useState(0);
    const [model] = useState(() => new Model());
    const countRef = useRef(model);

    return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
复制代码

这样使用,可以在不修改 state 的情况下,使用 model 实例中的一些属性,可以使 flag,可以是数据源,甚至可以作为 Mobxstore 进行使用。

class 组件中createRef

// 父组件
class ParentComponent extends React.Component {
    constructor(props) {
        super(props);
        this.myRef = React.createRef(null);
    }
    render() {
        return (
            <div>
                <ChildComponent ref={this.myRef}></ChildComponent>
                <button
                    onClick={() => {
                        alert(this.myRef.current.state.childName);
                    }}
                >
                    get child childName from parent
                </button>
                <button
                    onClick={() => {
                        this.myRef.current.sayHello();
                    }}
                >
                    get child function from parent
                </button>
            </div>
        );
    }
}

// 子组件
class ChildComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            childName: "mongo",
        };
    }
    sayHello() {
        alert("hello! " + this.state.childName);
    }
    render() {
        return (
            <div>
                <button
                    onClick={() => {
                        this.sayHello();
                    }}
                >
                    alert function by child
                </button>
            </div>
        );
    }
}

useRef

const ParentComponentByUseRef = () => {
    const myRef = useRef();
    return (
        <div>
            <ChildComponentByUseRef ref={myRef}></ChildComponentByUseRef>
            <button
                onClick={() => {
                    alert(myRef.current.childName);
                }}
            >
                get childName by parent
            </button>
            <button
                onClick={() => {
                    myRef.current.sayHello();
                }}
            >
                get function by parent
            </button>
        </div>
    );
};

const ChildComponentByUseRef = forwardRef((props, ref) => {
    // 暴露function、state和DOM结构给父元素
    useImperativeHandle(ref, () => ({
        sayHello,
        childName,
        ref
    }));
    const [childName, setChildName] = useState("mongo");
    const sayHello = () => {
        alert("hello! " + childName);
    };
    return (
        <div ref={ref}>
            <button
                onClick={() => {
                    sayHello();
                }}
            >
                get function by child
            </button>
            <button
                onClick={() => {
                    setChildName("mongowoo");
                }}
            >
                change childName
            </button>
        </div>
    );
});

export default forwardRef(ParentComponentByUseRef);

hooks中如果要进行refs转发,要配合fowardRef和useImperativeHandle使用

  • forwardRef React.forwardRef 会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。

使用场景
1.转发 refs 到 DOM 组件
2.在高阶组件中转发 refs

  • useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值

竞态

执行更早但返回更晚的情况会错误的对状态值进行覆盖useEffect 中,可能会有进行网络请求的场景,我们会根据父组件传入的 id,去发起网络请求,id 变化时,会重新进行请求。

function App() {
    const [id, setId] = useState(0);

    useEffect(() => {
        setId(10);
    }, []);

    // 传递 id 属性
    return <Counter id={id} />;
}


// 模拟网络请求
const fetchData = id =>
    new Promise(resolve => {
        setTimeout(() => {
            const result = `id 为${id} 的请求结果`;
            resolve(result);
        }, Math.random() * 1000 + 1000);
    });


function Counter({ id }) {
    const [data, setData] = useState('请求中。。。');

    useEffect(() => {
        // 发送网络请求,修改界面展示信息
        const getData = async () => {
            const result = await fetchData(id);
            setData(result);
        };
        getData();
    }, [id]);

    return <p>result: {data}</p>;
}
复制代码

展示结果:

竞态问题

上面的实例,多次刷新页面,可以看到最终结果有时展示的是 id 为 0 的请求结果,有时是 id 为 10 的结果。 正确的结果应该是 ‘id 为 10 的请求结果’。这个就是竞态带来的问题。

解决办法:

  • 取消异步操作 image.png
  • 设置布尔值进行追踪

image.png

useCallback

如题,当依赖频繁变更时,如何避免 useCallback 频繁执行呢?

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

    const handleClick = useCallback(() => {
        setCount(count + 1);
    }, [count]);

    return <p onClick={handleClick}>clicked {count} times</p>;
}
复制代码

click 事件提取出来,使用 useCallback 包裹,但其实并没有起到很好的效果。

因为 Counter 组件重新渲染目前只依赖 count 的变化,所以这里的 useCallback 用与不用没什么区别。

使用 useReducer 替代 useState

可以使用 useReducer 进行替代。

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

    const handleClick = useCallback(() => {
        dispatch();
    }, []);

    return <p onClick={handleClick}>clicked {count} times</p>;
}
复制代码

useReducer 返回的 dispatch 函数是自带了 memoize 的,不会在多次渲染时改变。因此在 useCallback 中不需要将 dispatch 作为依赖项。

setState 中传递函数

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

    const handleClick = useCallback(() => {
        setCount(c => c + 1);
    }, []);

    return <p onClick={handleClick}>clicked {count} times</p>;
}
复制代码

setCount 中使用函数作为参数时,接收到的值是最新的 state 值,因此可以通过这个值执行操作。

通过 useRef 进行闭包穿透

function Counter() {
    const [count, setCount] = useState(0);
    const countRef = useRef(count);
    
    countRef.current = count;
    const handleClick = useCallback(() => {
        setCount(countRef.current + 1);
    }, []);

    return <p onClick={handleClick}>clicked {count} times</p>;
}

这种方式也可以实现同样的效果。但是不推荐使用,不仅要编写更多的代码,而且可能会产生出乎预料的问题。

useMemo VS useCallback

  • useMemo 的返回值是一个值,可以是属性,可以是函数(包括组件)
  • useCallback 的返回值只能是函数

因此,useMemo 一定程度上可以替代 useCallback,等价条件:useCallback(fn, deps) => useMemo(() => fn, deps)

useMemo VS React.memo 

  • useMemo 是对组件内部的一些数据进行优化和缓存,惰性处理。
  • React.memo 是对函数组件进行包裹,对组件内部的 state 、 props 进行浅比较,判断是否需要进行渲染。

useCallback 和 useMemo 什么时候使用

不建议频繁使用

  • useCallback 和 useMemo 其实在函数组件中是作为函数进行调用,那么第一个参数就是我们传递的回调函数,无论是否使用 useCallback 和 useMemo,这个回调函数都会被创建,所以起不到降低函数创建成本的作用

  • 不仅无法降低创建成本,使用 useCallback 和 useMemo 后,第二个参数依赖项在每次 render 的时候还需要进行一次浅比较,无形中增加了数据对比的成本

useCallback 的使用场景

  1. 场景一:需要对子组件进行性能优化 state变化时,父组件重新 render,而子组件却不需要重新 render
    import React, { useCallback, useState } from 'react';
    import Foo from './Foo';

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

        const fooClick = useCallback(() => {
            console.log('点击了 Foo 组件的按钮');
        }, []);

        return (
            <div style={{ padding: 50 }}>
                <Foo onClick={fooClick} />
                <p>{count}</p>
                <button onClick={() => setCount(count + 1)}>click</button>
            </div>
        );
    }

    export default App;

Foo.js 中使用 React.memo 对组件进行包裹(类组件的话继承 PureComponent 是同样的效果)

    import React from 'react';

    const Foo = ({ onClick }) => {

        console.log('Foo 组件: render');
        return <button onClick={onClick}>Foo 组件中的 button</button>;

    };

    export default React.memo(Foo);

此时再点击按钮,父组件更新,但是子组件不会重新 render

场景二:需要作为其他 hooks 的依赖

这个例子中,会根据状态 page 的变化去重新请求网络数据,当 page 发生变化,我们希望能触发 useEffect 调用网络请求,而 useEffect 中调用了 getDetail 函数,为了用到最新的 page,所以在 useEffect 中需要依赖 getDetail 函数,用以调用最新的 getDetail

使用 useCallback 处理前的代码

App.js

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

    const request = (p) =>
        new Promise(resolve => setTimeout(() => resolve({ content: `第 ${p} 页数据` }), 300));

    function App() {
        const [page, setPage] = useState(1);
        const [detail, setDetail] = useState('');

        const getDetail = () => {
            request(page).then(res => setDetail(res));
        };

        useEffect(() => {
            getDetail();
        }, [getDetail]);

        console.log('App 组件:render');

        return (
            <div style={{ padding: 50 }}>
                <p>Detail: {detail.content}</p>
                <p>Current page: {page}</p>
                <button onClick={() => setPage(page + 1)}>page increment</button>
            </div>
        );
    }

    export default App;

按照上面的写法,会导致 App 组件无限循环进行 render,此时就需要用到 useCallback 进行处理

使用 useCallback 处理后的代码

App.js

    import React, { useEffect, useState, useCallback } from 'react';

    const request = (p) =>
        new Promise(resolve => setTimeout(() => resolve({ content: `第 ${p} 页数据` }), 300));

    function App() {
        const [page, setPage] = useState(1);
        const [detail, setDetail] = useState('');

        const getDetail = useCallback(() => {
            request(page).then(res => setDetail(res));
        }, [page]);

        useEffect(() => {
            getDetail();
        }, [getDetail]);

        console.log('App 组件:render');

        return (
            <div style={{ padding: 50 }}>
                <p>Detail: {detail.content}</p>
                <p>Current page: {page}</p>
                <button onClick={() => setPage(page + 1)}>page increment</button>
            </div>
        );
    }

    export default App;

App组件可以正常的进行 render 了。

  1. useCallback 使用场景总结:

    1. 向子组件传递函数属性,并且子组件需要进行优化时,需要对函数属性进行 useCallback 包裹
    2. 函数作为其他 hooks 的依赖项时,需要对函数进行 useCallback 包裹

useMemo 的使用场景

  1. 向子组件传递 引用类型 属性,并且子组件需要进行优化时,需要对属性进行 useMemo 包裹
  2. 引用类型值,作为其他 hooks 的依赖项时,需要使用 useMemo 包裹,返回属性值
  3. 需要进行大量或者复杂运算时,为了提高性能,可以使用 useMemo 进行数据缓存,节约计算成本

所以,在 useCallback 和 useMemo 使用过程中,如非必要,无需使用,频繁使用反而可能会增加依赖对比的成本,降低性能。