【React】精选5题

1. 说说你对 useMemo 的理解

useMemo 是 React 提供的一个 Hook,用于在函数组件中进行性能优化,避免不必要的计算和渲染。

在 React 中,组件的渲染是由组件的 props 和状态(state)决定的。当组件的 props 或状态发生变化时,组件会重新渲染。而有时候,某些计算操作可能是比较耗时的,如果在每次渲染时都执行这些计算,会导致性能下降。

这时,可以使用 useMemo 来优化性能。useMemo 接收一个计算函数和一个依赖数组,它会记住计算函数的返回值,并且只在依赖数组发生变化时重新计算。当依赖数组不变时,会直接返回上一次缓存的值,避免重复计算。

useMemo 的使用步骤如下:

  1. 定义计算函数:创建一个计算函数,用于执行需要优化的计算操作。例如:
function calculateExpensiveValue(a, b) {
  // 执行耗时的计算操作
  // ...
  return result;
}
  1. 使用 useMemo:在函数组件中使用 useMemo,传入计算函数和依赖数组。例如:
function MyComponent() {
  const a = 5;
  const b = 10;
  
  const result = useMemo(() => calculateExpensiveValue(a, b), [a, b]);

  return <div>{result}</div>;
}

在上面的例子中,当 a 或 b 发生变化时,useMemo 会执行计算函数 calculateExpensiveValue,并将结果缓存起来。如果 a 和 b 的值在下一次渲染时没有发生变化,useMemo 会直接返回上一次缓存的结果。

需要注意的是,依赖数组是可选的。如果不传入依赖数组,那么 useMemo 的计算函数将在每次渲染时都执行。传入空数组([])作为依赖数组,可以使 useMemo 只在组件的初始渲染时执行一次。

总结来说,useMemo 是 React 提供的一个用于性能优化的 Hook,它会记住计算函数的返回值,并且只在依赖数组发生变化时重新计算。通过使用 useMemo,我们可以避免不必要的计算和渲染,提升应用的性能。

useMemo和useCallback区别:

虽然 useMemo 和 useCallback 都是 React 提供的用于性能优化的 Hook,但它们在使用场景和作用上有一些区别。

useMemo 的主要作用是缓存一个计算结果,并在依赖项发生变化时重新计算。它接收一个计算函数和一个依赖数组作为参数,并返回计算函数的结果。在组件重新渲染时,如果依赖项未发生变化,则 useMemo 会直接返回上一次缓存的结果,避免重复计算。适合用于缓存昂贵的计算操作的结果。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useCallback 的主要作用是缓存一个函数,以便在子组件中使用时,子组件不会重新创建该函数。它接收一个函数和一个依赖数组作为参数,并返回一个记忆化后的函数。在依赖项未发生变化的情况下,useCallback 会返回同一个函数实例,避免了不必要的函数创建和传递给子组件。适合用于将回调函数传递给子组件时,避免子组件在每次渲染时都创建新的回调函数。

const memoizedCallback = useCallback(() => {
  // do something
}, [a, b]);

总结区别:

useMemo 缓存一个计算结果,适合用于缓存昂贵的计算操作的结果。
useCallback 缓存一个函数,适合用于避免子组件在每次渲染时都创建新的回调函数。
需要注意的是,虽然 useMemo 和 useCallback 都可以在依赖项未发生变化时避免重新计算,但它们并不能完全代替彼此。具体使用哪个 Hook 取决于具体的优化需求和场景。

2. 说说你对自定义hook的理解

自定义 Hook 是一种在 React 中共享逻辑的机制。它可以让你将组件逻辑提取到可重用的函数中,并在函数组件中使用。

自定义 Hook 按照一定的命名规则命名,以 “use” 开头,例如 useFetchData 或 useTheme。它们可以使用其他的 React Hook,也可以是纯 JavaScript 函数。

自定义 Hook 的步骤如下:

  1. 命名和定义:为你的自定义 Hook 选择一个有意义的名称,并定义一个函数,遵循 use 开头的命名规则。例如:
function useFetchData(url) {
  // ...具体的逻辑
}
  1. 提取逻辑:在自定义 Hook 函数中,将需要重用的逻辑逻辑提取出来。可以使用其他 React Hook、状态管理库或其他 JavaScript 函数,根据你的需求来决定提取的逻辑。例如,我们可以使用 useEffect 和 fetch API 来获取数据:
function useFetchData(url) {
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(url);
      const data = await response.json();
      setData(data);
    };

    fetchData();
  }, [url]);

  return data;
}
  1. 使用自定义 Hook:在函数组件中使用自定义 Hook。注意,自定义 Hook 的名称必须以 “use” 开头,以便 React 在内部识别它作为一个 Hook。例如:
function MyComponent() {
  const data = useFetchData("https://api.example.com/data");

  return (
    <div>
      {data ? (
        <ul>
          {data.map((item) => (
            <li key={item.id}>{item.name}</li>
          ))}
        </ul>
      ) : (
        <div>Loading...</div>
      )}
    </div>
  );
}

通过上述步骤,我们可以将一些通用的逻辑封装到自定义 Hook 中,并在需要的地方重用它。这样可以提高代码的重用性和可维护性,使组件更加关注视图层的展示。

需要注意的是,自定义 Hook 只是一种约定,它并不会引入额外的 React 渲染或状态管理的机制。它仅仅是一种将逻辑提取到可重用函数中的方式。

3. 在 React 中可以做哪些性能优化?

  • 使用 shouldComponentUpdate 避免不需要的渲染,但是如果对 props 和 state 做深比较,代价很大,所以需要根据业务进行些取舍;在有子组件的情况下,为了避免子组件的重复渲染,可以通过父组件来判断子组件是否需要 PureRender。

  • 将 props 设置为数组或对象:每次调用 React 组件都会创建新组件,就算传入的数组或对象的值没有改变,他们的引用地址也会发生改变,比如,如果按照如下的写法,那么每次渲染时 style 都是一个新对象

// 不推荐
<button style={{ color: 'red' }} />

// 推荐
const style = { color: 'red' }
<button style={style} />

// 不推荐
<button style={this.props.style || {} } />  

// 推荐
const defaultStyle = {}
<button style={this.props.style || defaultStyle } />   
  • 将函数的绑定移动到构造函数内:可以避免每次都绑定事件。
  • 使用 immutable 不可变数据,在我们项目中使用引用类型时,为了避免对原始数据的影响,一般建议使用 shallowCopy 和 deepCopy 对数据进行处理,但是这样会造成 CPU 和 内存的浪费,所以推荐使用 immutable,优点如下:
  1. 降低了“可变”带来的复杂度
  2. 节省内存,immutable 使用结构共享尽量复用内存,没有被引用的对象会被垃圾回收
  3. 可以更好的做撤销/重做,复制/粘贴,时间旅行
  4. 不会有并发问题(因为数据本身就是不可变的)
  5. 拥抱函数式编程
  • 给子组件设置一个唯一的 key,因为在 diff 算法中,会用 key 作为唯一标识优化渲染

在 React 中使用不可变(immutable)数据有多种方式,以下是其中几种常用的方法:

  1. 使用 JavaScript 中的不可变数据结构:使用不可变数据结构(如数组的 concat() 和 slice() 方法,对象的 Object.assign() 方法等)来创建新的不可变数据,而不直接修改原始数据。
// 使用 concat() 方法创建新的不可变数组
const newArray = oldArray.concat(newValue);

// 使用 Object.assign() 方法创建新的不可变对象
const newObject = Object.assign({}, oldObject, { key: newValue });
  1. 使用第三方库:使用第三方库(如 Immutable.js、Immer 等)来创建和操作不可变数据。这些库提供了更丰富的 API 和更高级的不可变数据结构,使得处理不可变数据更加简单和高效。
import { Map, List } from 'immutable';

// 使用 Immutable.js 创建新的不可变 Map
const newMap = Map({ key: value });

// 使用 Immer 创建新的不可变对象
const newObject = produce(oldObject, (draft) => {
  draft.key = newValue;
});
  1. 使用 React 的 useState 和 useReducer Hooks 的更新函数:在使用 useState 或 useReducer Hook 时,可以使用回调函数的形式来更新状态,以确保不直接修改原始数据。
// 使用 useState Hook 更新不可变数组
const [array, setArray] = useState([]);

const addToArray = (newValue) => {
  setArray((prevArray) => [...prevArray, newValue]);
};

// 使用 useReducer Hook 更新不可变对象
const initialState = { key: value };
const reducer = (state, action) => {
  switch (action.type) {
    case 'UPDATE':
      return { ...state, key: action.payload };
    default:
      return state;
  }
};

const [state, dispatch] = useReducer(reducer, initialState);

const updateState = (newValue) => {
  dispatch({ type: 'UPDATE', payload: newValue });
};

使用不可变数据的好处是,它可以帮助我们避免直接修改原始数据,从而避免出现不必要的副作用和错误。不可变数据也有助于优化 React 的性能,因为它可以帮助 React 更准确地进行 Virtual DOM 的比较和渲染。

4. 如何让 useEffect 支持 async/await?

在 useEffect 中直接使用 async/await 是不支持的,因为 useEffect 函数必须返回一个清除函数或者是 undefined。

然而,你可以创建一个内部的异步函数,并在 useEffect 中调用这个函数来实现类似的效果。下面是一个使用 async/await 的示例:

useEffect(() => {
  const fetchData = async () => {
    try {
      const response = await fetch('https://api.example.com/data');
      const data = await response.json();
      // 处理数据
    } catch (error) {
      // 处理错误
    }
  };

  fetchData();
}, []);

在上面的例子中,我们定义了一个内部的异步函数 fetchData,并在 useEffect 中调用它。这样可以使用 async/await 来处理异步操作,同时仍然符合 useEffect 的要求。

需要注意的是,由于 fetchData 是在 useEffect 中定义的,它可以访问 useEffect 中的变量和 props。如果你的异步操作依赖于 useEffect 中的某些值,请确保将这些值添加到依赖数组中,以便在它们发生变化时重新运行 useEffect。

另外,如果你希望在组件卸载时取消异步操作,可以在 fetchData 函数中添加取消逻辑,并在清除函数中调用它。

总结来说,虽然 useEffect 不直接支持 async/await,但你可以通过在 useEffect 中调用内部的异步函数来实现类似的效果。这样可以使用 async/await 来处理异步操作,并在组件挂载和卸载时进行清理。

也可以自定义hooks使其支持 支持 async/await

import { useEffect } from 'react';

function useAsyncEffect(effect, dependencies) {
  useEffect(() => {
    const cleanupFunction = effect();
    
    return () => {
      if (typeof cleanupFunction === 'function') {
        cleanupFunction();
      }
    };
  }, dependencies);
}

export default useAsyncEffect;

在上述代码中,我们定义了一个名为 useAsyncEffect 的自定义 Hook。它接收一个异步函数 effect 和一个依赖数组 dependencies,并在 useEffect 中执行这个异步函数。

使用这个自定义 Hook 的示例:

import useAsyncEffect from './useAsyncEffect';

function MyComponent() {
  useAsyncEffect(async () => {
    try {
      const response = await fetch('https://api.example.com/data');
      const data = await response.json();
      // 处理数据
    } catch (error) {
      // 处理错误
    }
  }, []);

  // 组件的其他逻辑

  return <div>...</div>;
}

在这个示例中,我们使用了自定义 Hook useAsyncEffect,将异步操作的逻辑封装到了其中。这样,我们可以在函数组件中像使用普通的 useEffect 一样使用这个支持异步的效果。

需要注意的是,自定义 Hook useAsyncEffect 内部仍然是使用了 useEffect,因此它也遵循 useEffect 的规则和限制。在使用自定义 Hook 时,也需要将需要的值添加到依赖数组 dependencies 中,以确保在这些依赖发生变化时重新运行异步函数。

总结来说,通过封装一个自定义 Hook,我们可以实现支持异步操作的 useEffect。这样可以更加方便地在函数组件中处理异步逻辑,同时符合 React Hook 的规范和要求。

5. 你常用的 React Hooks 有哪些?

1. useState

1.1 类组件中

在类组件中,可以用this.state来定义类组件的状态,可以看下以下代码实现

import React from 'react'

class StateClass extends React.Component{
    constructor(){
        super()
        this.state = {
            name: '类'
        }
    }

    render() {
        return (
            <div onClick={ this.setName }>
                这是一个类组件————{ this.state.name }
            </div>
        )
    }

    setName = () => {
        this.setState({
            name: '我通过类组件方法变成这样了'
        })
    }
}

export default StateClass

1.2 函数组件中

在函数组件中,可以使用useState来定义函数组件的状态。使用useState来创建状态

  • 1.引入
  • 2.接收一个参数作为初始值
  • 3.返回一个数组,第一个值为状态,第二个值为改变状态的函数
    看一下同样的功能实现,函数组件的useState的代码实现如下。相同的功能,用函数组件的useState的写法是不是方便多了呢?接下来我们继续学习另外的hooks
import React,{ useState } from 'react'

function StateFunction () {
    const [name, setName] = useState('函数')
    //     类名,修改函数名            初始值

    return (
        <div onClick={ () => setName('我使用hooks变成这样了') }>
        //	setName也可以写入方法,如setName( val => val+'xxxx' )
            这是一个函数式组件————{name}
        </div>
    )
}

export default StateFunction

2. useEffect

2.1 简单介绍

useEffect又称副作用hooks。作用:给没有生命周期的组件,添加结束渲染的信号。执行时机:在渲染结束之后执行

  1. 什么是副作用?
    副作用 ( side effect ): 数据获取,数据订阅,以及手动更改 React 组件中的 DOM 都属于副作用
    因为我们渲染出的页面都是静态的,任何在其之后的操作都会对他产生影响,所以称之为副作用
  2. 使用:
  • 1.第一个参数,接收一个函数作为参数
  • 2.第二个参数,接收【依赖列表】,只有依赖更新时,才会执行函数
  • 3.返回一个函数,先执行返回函数,再执行参数函数

2.2 不接受第二个参数的情况下

如果不接受第二个参数,那么在第一次渲染完成之后和每次更新渲染页面的时候,都会调用useEffect的回调函数,所以你要考虑好使用场景。

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

function StateFunction () {
    const [num, setNum] = useState(0)
    
    useEffect( () => {
        console.log('2222函数式组件结束渲染')
    })
    
    return (
        <div onClick={ () => setNum( num => num+1 ) }>
            这是一个函数式组件————{num}
        </div>
    )
}

2.3 接受第二个参数的情况下

useEffect( () => {
    console.log('2222函数式组件结束渲染')
},[])
//	改变useEffect第二个参数,其余代码同上

在这,我们可以对第二个参数传入一个数组,这个数组表示的是更新执行所依赖的列表,只有依赖列表改变时(当数组中的任意一项变化的时候,useEffect会被重新执行 ),才会触发回调函数

  • 传入的为空数组[],那么即告诉useEffect不依赖于state、props中的任意值,useEffect就只会运行一次,常用场景为页面获取数据的方法可以写入此处进行调用,以获取页面初始数据
  • 传入一个值构建的数组、或者多个值构建的数组,如[num]、[num,val],上述代码变更为如下。那么此时只有当数组中的值(任意一项即可)改变时,才会重新触发回调函数
useEffect( () => {  
    console.log('2222函数式组件结束渲染')  
},[num])  
useEffect( () => {  
    console.log('2222函数式组件结束渲染')  
},[num,val])  
//	改变useEffect第二个参数,其余代码同上 

2.4 清除副作用

上面写的都是一些不需要清除的副作用,只是回调触发一些简单的方法,但是有一些副作用是需要清除的。例如绑定一些DOM事件,在这种情况下,清除工作是非常重要的,可以防止引起内存泄露,例如下面给出的代码对比

  • ①未清除副作用的情况下。此时第一次点击正常输出一次打印当前位置,而后每一次useEffect调用都会新绑定一个updateMouse方法,那么点击一次所触发绑定的方法越来越多,那么之后点击一次就会疯狂打印打印当前位置,这也就造成了页面性能、内存泄露等问题
const [positions,setPositions] = useState({ x:0,y:0 })  
useEffect( () => {  
    console.log('2222函数式组件结束渲染')  
    const updateMouse = (e) => {  
        console.log('打印当前位置')  
        setPositions({ x:e.clientX, y:e.clientY })  
    }  
    document.addEventListener('click',updateMouse)  
})  
return (  
    <div>  
        <p>x:{ positions.x }</p>  
        <p>y:{ positions.y }</p>  
    </div>  
)  
  • ②清除副作用的情况下(仅修改部分代码,其它代码同上)。例如示例代码
    1. 首次刷新或进入页面会先执行除return以外的内容,也就是会执行一个绑定的方法,然后将updateMouse方法绑定到click事件上
    2. 并将该次useEffect中的事件清除返回出去,但是此时是并没有执行return中的内容的(重点注意)
  1. 然后当你点击第一次的时候,就会打印设置当前鼠标页面坐标,然后先执行上一次return返回出去的内容,注意这里是执行上一次return中的清除事件绑定方法,然后执行该清除事件绑定方法,当然清除也是清除的上一个useEffect中的绑定事件
  2. 然后再开始执行新的useEffect中的绑定事件方法,并再次将改次useEffect清除事件绑定的方法return返回出去,如此就形成了一个链式一样的过程
  3. 当页面卸载的时候,会执行最后一次return返回出来的清除事件绑定的方法,这样也就保证了页面卸载的时候,移除了绑定添加的DOM事件方法
    (上述写的执行过程并没有从原理出发去分析的,只是简单的描述。可能稍微有点乱,如果你理解不了,可以多看几遍并动手执行示例代码结合进行理解)
useEffect( () => {  
    console.log('2222函数式组件结束渲染')  
    const updateMouse = (e) => {  
        console.log('打印当前位置')  
        setPositions({ x:e.clientX, y:e.clientY })  
    }  
    document.addEventListener('click',updateMouse) //  添加绑定方法事件(要修改依赖,绑定到依赖上)  
    return () => {  
        //  在每次执行useEffect之前都会执行上一次return中内容  
        document.removeEventListener('click',updateMouse)  
        //  移除绑定方法事件(要修改依赖,绑定到依赖上)  
        console.log('1111销毁')  
    }  
})  

2.5 useEffect中的异步

每个effect函数都属于一次特定的渲染:

  • ①useEffect调度不会阻塞浏览器更新屏幕<异步>
  • ②每次重新渲染都会生成新的effect,替换掉之前的,确保effect中获取的值是最新的,不用担心过期。如下,设置的3000毫秒内连续点击三次,那么将会一共打印4次,分别是0、1、2、3,0是第一次渲染结束之后自动触发的,剩下1、2、3则是点击三次每次触发时的count值
function Counter() {  
  const [count, setCount] = useState(0);  
  useEffect(() => {  
    setTimeout(() => {  
      console.log(`${count}`);  
    }, 3000);  
  });  
  return (  
    <div>  
      <p>你点击了{count}</p>  
      <button onClick={() => setCount(count + 1)}>  
        点击我  
      </button>  
    </div>  
  );  
} 
  • 与类组件进行对比:如果放到类组件中,则是打印设置的时间内改变的最终值,在类组件中同等代码如下(仅给出关键代码)。打印设置的时间内改变的最终值是什么意思呢?如果你设置3000毫秒,那么渲染结束的瞬间开始计时,3000毫秒内连续点击三次,那么最终就会打印4次3,如果先等第一次componentDidMount中设置的定时器结束,再突然3000毫秒内连续点击三次,那么就会先打印第一次的0,再打印三次3,因为类写法中是共用的同一个num状态值。(如果你将时间设置为0毫秒,那么其实你连续点击三次,会跟useEffect一样,也是先打印一次0,再接着打印1、2、3,因为这个变化反应很快,你感觉不到差异,可以自己动手试试哦。)
this.state = {  
  	num:0  
}  
componentDidMount(){  
    setTimeout(() => {  
        console.log(this.state.num)  
    },3000)  
}  
componentDidUpdate(){  
    setTimeout(() => {  
        console.log(this.state.num)  
    },3000)  
}  
render() {  
    return (  
        <div onClick={ this.setNum }>  
            这是一个类组件————{ this.state.num }  
        </div>  
    )  
}  
setNum = () => {  
    this.setState({  
        num: this.state.num+1  
    })  
}  

3. useLayoutEffect

一般将useLayoutEffect称为有DOM操作的副作用hooks。作用是在DOM更新完成之后执行某个操作。执行时机:在DOM更新之后执行

与useEffect对比

相同点

1.第一个参数,接收一个函数作为参数
2.第二个参数,接收【依赖列表】,只有依赖更新时,才会执行函数
3.返回一个函数,先执行返回函数,再执行参数函数
(所以说执行过程的流程是一样的)

不同点

执行时机不同。useLayoutEffect在DOM更新之后执行;useEffect在render渲染结束后执行。执行示例代码会发现useLayoutEffect永远比useEffect先执行,这是因为DOM更新之后,渲染才结束或者渲染还会结束

const [num, setNum] = useState(0)
//在类组件中用componentWillMount生命周期来实现
useLayoutEffect( () => {
    console.log('useLayoutEfffect')
	//	也可以在此进行事件绑定
    return () => {
    	//	也可以在此进行事件绑定移除
        console.log(1)
    }
},[num])

useEffect( () => {
    console.log('useEffect')
},[num])

return (
    <div onClick={ () => setNum( num => num+1 ) }>
        这是一个函数式组件————{num}
    </div>
)

4. useMemo

使用useMemo可以传递一个创建函数和依赖项,创建函数会需要返回一个值,只有在依赖项发生改变的时候,才会重新调用此函数,返回一个新的值。简单来说,作用是让组件中的函数跟随状态更新(即优化函数组件中的功能函数)。

  • 使用:
    1.接收一个函数作为参数
    2.同样接收第二个参数作为依赖列表(可以与useEffect、useLayoutEffect进行对比学习)
    3.返回的是一个值。返回值可以是任何,函数、对象等都可以

4.1 复杂计算逻辑优化使用场景

  • 未优化前代码如下。当我们点击div区域时,此时触发的setAge,改变的是age,跟getDoubleNum方法其实是不相关的,但是如果你看下控制台,能看到打印了多次获取双倍Num,说明该方法不断被触发,其实是没必要触发的。如果方法内计算量大、对性能是有一定影响的,所以需要进行优化
const [num, setNum] = useState(1)  
const [age, setAge] = useState(18)  
function getDoubleNum () {  
    console.log(`获取双倍Num${num}`)  
    return 2 * num  //	假设为复杂计算逻辑  
}  
return (  
  <div onClick={ () => { setAge( age => age+1 )} }>  
      <br></br>  
      这是一个函数式组件————{  getDoubleNum() }  
      <br></br>  
      age的值为————{ age }  
      <br></br>  
  </div>  
)  
  • 使用useMemo优化后代码如下。此时getDoubleNum方法是接收一个返回的值,所以要注意注释里所写的,括号是去掉了的。使用useMemo后,再点击div区域改变age的值,此时执行返回的return 2*num以及打印只有在num更新时才会去执行,然后返回值给到getDoubleNum再渲染到视图上,这样就减少了不必要的计算达到优化的作用
const [num, setNum] = useState(1)  
const [age, setAge] = useState(18)  
const getDoubleNum = useMemo( () => {  
    console.log(`获取双倍Num${num}`)  
    return 2 * num  //	假设为复杂计算逻辑  
},[num] )  
return (  
    <div onClick={ () => { setAge( age => age+1 ) }  }>  
        <br></br>  
        这是一个函数式组件————num:{  getDoubleNum }  //  注意这里没括号,因为是返回值  
        <br></br>  
        age的值为————{ age }  
        <br></br>  
    </div>  
)  

4.2 父子组件重复渲染问题优化使用场景

  • 未优化前代码如下。子组件包裹一个memo,但是包裹了还是会重新渲染, 为什么呢?因为我们定义的info是const定义的一个局部变量,每次重新渲染都是重新定义一个新的info,然后子组件进行浅层比较时候,info永远是不一样的,所以就会重新渲染(可以按照例子点击按钮,会发现子组件不断打印我是子组件)。如果子组件比较复杂的情况下,那么就会对页面性能产生影响
const Child = memo( () => {  
    console.log('我是子组件')  
    return <p>我是子组件</p>  
})  
function Parent() {  
    const [show,setShow] = useState(true)  
    const info = {  
        name: 'Even',  
        age: 22  
    }  
    return(  
        <div>  
            <Child info={ info } />  
            <button onClick={ () => setShow(!show) }>点击更新状态</button>  
        </div>  
    )  
}  
  • 使用useMemo后代码如下(只给出修改代码,其它代码同上例子)。这样子优化后,子组件只会在初始化状态时渲染一次,当我们点击按钮时,因为info其包裹的useMemo依赖并没有改变,返回值是同一个值,所以不会造成子组件重新渲染。
const info = useMemo( () => {  
    return {  
        name: 'Even',  
        age: 22  
    }  
},[])  

5. useCallback

useMemo讲完我们来讲一个跟其很相似的叫useCallback,作用也是让某些操作、方法跟随状态的更新而去执行。

与useMemo对比。
可以简单这样看作,useMemo(() => Fn,deps)相当于useCallback(Fn,deps)

  1. 不同点:
    useCallback是对传过来的回调函数优化,返回的是一个函数;useMemo返回值可以是任何,函数,对象等都可以

  2. 相同点:
    在使用方法上,useMemo与useCallback相同。接收一个函数作为参数,也同样接收第二个参数作为依赖列表

5.1 为何说useCallback缓存的是一个函数(重要区别)

useCallback虽然与useMemo相似,但其返回及缓存的是一个函数,对比以下示例代码。先说①②③三种情况的对比(可以复制代码然后分别注释①②③代码对比)

  • 当①情况时,只会打印一次获取双倍Num1,也就是首次渲染的打印,之后再点击div区域改变age的值都与其无关,所以不会执行。因为getDoubleNum已经获得了useMemo中传入的函数执行后返回的值了,获取之后,便将其缓存下来了
  • 当②情况时,首次渲染会打印一次获取双倍Num1,然后每点击一次都会打印获取双倍Num1,这是为什么呢?不是说useCallback也有缓存的功能吗?这是因为我们前面提到的,useCallback返回的是一个函数。因为useCallback中的函数是在当前组件内定义的,组件重新渲染,它自然也会重新渲染,这又会有同学说了,可是这也不能说明它缓存的是一个函数啊。那么你可以先看看③的依赖为[]情况时,那么你就能明白了。所以说复杂计算逻辑的场景不适合使用useCallback来缓存,因为传入的函数内容会不断执行。
  • 当③情况时,我们结合②③处标记代码,set只能存入唯一值,我们观察打印的set的长度
    当useCallback依赖为空[]时,我们连续多次点击div区域,虽然useCallback中的内容会不断执行,但是我们可以看到打印出来的set的长度一直都是2,这就是因为它不断将同一个函数添加进set,所以set的长度不变
    而当useCallback的依赖为[num]时,我们连续多次点击div区域,可以看到打印出来的set在不断累加,1、2、3、4、5、6…。因为num在改变,所以每一次缓存的函数都是一个新的函数,所以添加进set的函数是不一样的,所以set的长度点一次加一次
const set = new Set()  
export default function StateFunction () {  
    const [num, setNum] = useState(1)  
    const [age, setAge] = useState(18)  
    const getDoubleNum = useMemo( () => {  
        console.log(`获取双倍Num${num}`)  
        return 2 * num  //	①假设为复杂计算逻辑  
    },[] )  
    const getDoubleNum = useCallback( () => {  
        console.log(`获取双倍Num${num}`)  
        return 2 * num  //	②假设为复杂计算逻辑  
    },[] )  
    set.add(getDoubleNum())  //	③注意set打印的长度变化(设置Callback的依赖为[]、[num]进行对比)  
    console.log('set.size:',set.size)  
    return (  
        <div onClick={ () => { setNum( num => num+1 ) }  }>  
            <br></br>  
            这是一个函数式组件————num:{  getDoubleNum } //①useMemo情况下  
            这是一个函数式组件————num:{  getDoubleNum() } //②useCallback情况下  
            <br></br>  
            age的值为————{ age }  
            <br></br>  
        </div>  
    )  
}  

5.2 useCallback适用场景

可以对父子组件传参渲染的问题进行优化。简单来说就是,父组件的传入函数不更新,就不会触发子组件的函数重新执行

  • 通常而言,父组件更新了,那么子组件也会更新。但是如果父组件传入子组件的内容不变,那么子组件某些操作(某些操作是指需要跟随传入内容的改变而同步进行的操作)是没必要执行的,这会影响页面性能,所以我们可以对这情况进行优化。
  • 例如示例代码,我们将getDoubleNum传入子组件,此时点击div区域改变的是num的值,我们使用父组件useCallback配合子组件的useEffect来优化,只有当父组件的num改变导致传入子组件的getDoubleNum改变的时候,我们才会执行子组件某些需要更新的操作(即注释标注处代码),这样就可以避免子组件一些没必要的更新操作反复执行而影响页面性能
function Parent () {

    const [num, setNum] = useState(1)
    const [age, setAge] = useState(18)

    const getDoubleNum = useCallback( () => {
        console.log(`获取双倍Num${num}`)
        return 2 * num
    },[num] )

    return (
        <div onClick={ () => {setNum( num => num+1 )} }>
            这是一个函数式组件————num:{  getDoubleNum() }
            <br></br>
            age的值为————age:{ age }
            <br></br>
            set.size:{set.size}
            <Child callback={ getDoubleNum() }></Child>
        </div>
    )
}

function Child(props) {
    useEffect( () => {
        console.log('callback更新了') //这里代表的是需要跟随传入内容的改变而同步进行的操作
    },[props.callback])

    return (
        <div>
            子组件的getDoubleNum{props.callback}
        </div>
    )
}

5.3 总结

简单总结使用场景判断:

  • 在子组件不需要父组件的值和函数的情况下,只需要使用memo函数包裹子组件即可
  • 如果有函数传递给子组件,使用useCallback
  • 缓存一个组件内的复杂计算逻辑需要返回值时,使用useMemo
  • 如果有值传递给子组件,使用useMemo

6. useRef

简单来说useRef就是返回一个子元素索引,此索引在整个生命周期中保持不变。作用也就是:长久保存数据。注意事项,保存的对象发生改变,不通知。属性变更不会重新渲染

  • 未使用useRef,如果我们有这样一个需求如下,需要当某个定时器自增的值达到限制条件后就清除该定时器,如下代码。此时以下的代码其实是没有办法完成给出的需求的,当num大于10后,会发现不停的打印大于10了,清除定时器,而其实是定时器没有清除掉的,所以会一直执行这两个打印内容,但是会发现打印出来的timer显示undefined,这是为什么呢?因为我们每次渲染都是通过setInterval重新返回的timer,timer也在更新,也就丢失了timer这个数据,导致无法准确清除某个需要清除的定时器
const [num, setNum] = useState(0)  
let timer  
useEffect( () => {  
    timer = setInterval( () => {  
        setNum( num => num+1 )  
    },400 )  
},[] )  
useEffect( () => {  
    if(num > 10){  
        console.log('大于10了,清除定时器')  
        console.log('timer:',timer)  
        //  因为每一个timer都是独立render的,所以获取不到  
        clearTimeout(timer)  
    }  
},[num] )  
return (  
    <div>  
        这是一个函数式组件————num:{  num }  
    </div>  
)  

使用useRef后,代码如下。我们可以看到num自增到11后就打印了一次大于10了,清除定时器以及ref.current 1,然后就停止自增了,因为定时器被清除了。ref是一个对象,ref.current存储了该定时器在整个生命周期中的id值,所以当清除定时器的时候,可以准确清除这个定时器.
保存一个值,在整个生命周期中维持不变

const [num, setNum] = useState(0)  
const ref = useRef()  
useEffect( () => {  
    ref.current = setInterval( () => {  
        setNum( num => num+1 )  
    },400 )  
    // ref.current = '111'  
},[] )  
useEffect( () => {  
    if(num > 10){  
        console.log('大于10了,清除定时器')  
        console.log('ref.current',ref.current)  
        clearTimeout(ref.current)  
    }  
},[num] )  
return (  
    <div>  
        这是一个函数式组件————num:{  num }  
    </div>  
)  
  • 重新赋值ref.current不会主动触发页面重新渲染。当我们将代码修改成下面这样,会在控制台打印发现ref.current的值打印为111,但是页面视图上显示的还是空,这是因为ref保存的对象发生改变,不会主动通知,属性变更不会重新渲染
const [num, setNum] = useState(0)  
const ref = useRef()  
useEffect( () => {  
    ref.current = '111'  
    console.log('ref.current',ref.current)  
},[] )  
return (  
    <div>  
    	  这是ref.current的值——ref.current:{ ref.current }  
        <br></br>  
        这是一个函数式组件————num:{  num }  
    </div>  
)  

7. useContext

useContext是让子组件之间共享父组件传入的状态的。作用通俗地说是带着子组件去流浪。

  • 未使用useContext,我们有下列这样一个场景,我们父组件有传入一个值到不同的子组件中,示例给出的代码是2个这样的子组件,但是如果我需要添加的子组件特别多呢?总不能总是一个一个这样添加写入吧,而且如果传入的同一个变量名如果发生改变,还得一个个去改,所以我们可以用useContext优化一下代码
function StateFunction () {  
    const [num, setNum] = useState(1)  
    return (  
        <div>  
            <button onClick={ ()=> setNum(num => num+1) }>增加num的值+1</button>  
            <br></br>  
            这是一个函数式组件——num:{  num }  
            <Item1 num={num}></Item1>  
            <Item2 num={num}></Item2>  
            //	......  
        </div>  
    )  
}  
function Item1 (props) {  
    return (  
        <div>  
            子组件1 num:{ props.num }  
        </div>  
    )  
}  
function Item2 (props) {  
    return (  
        <div>  
            子组件2 num:{ props.num }  
        </div>  
    )  
}  
  • 使用useContext优化后,代码如下,这样我们只需要在子组件中使用useContext(Context句柄)来获取数据即可,添加同类子组件时不需要再关注父组件中子组件定义时的props传入值,使用方法如下:
  1. 需要引入useContetx,createContext两个内容
  2. 通过createContext创建一个context句柄
  3. Context.Provider来确定数据共享范围
  4. 通过value来分发内容
  5. 在子组件中,通过useContext(Context句柄)来获取数据
    注意事项:上层数据发生改变,肯定会触发重新渲染(点击button按钮触发父组件更新传入的num值能看到子组件重新渲染)
const Context = createContext(null)  
function StateFunction () {  
    const [num, setNum] = useState(1)  
    return (  
        <div>  
            <button onClick={ ()=> setNum(num => num+1) }>增加num的值+1</button>  
            <br></br>  
            这是一个函数式组件——num:{  num }  
            <Context.Provider value={num}>  
                <Item3></Item3>  
                <Item4></Item4>  
            </Context.Provider>  
        </div>  
    )  
}  
function Item3 () {  
    const num = useContext(Context)  
    return (  
        <div>  
            子组件3: { num }  
        </div>  
    )  
}  
function Item4 () {  
    const num = useContext(Context)  
    return (  
        <div>  
            子组件4: { num+2 }  
        </div>  
    )  
}  

8. useReducer

以前是只能在类组件中使用Redux,现在我们可以通过useReducer在函数式组件中使用Redux。作用是可以从状态管理的工具中获取到想要的状态。

  • 如何使用useReducer。Redux必须要有的内容就是仓库store和管理者reducer。而useReducer也是一样的,需要创建数据仓库store和管理者reducer,即示例代码注释处。然后我们就可以通过①处的定义一个数组获取状态和改变状态的动作,触发动作的时候需要传入type类型判断要触发reducer哪个动作,然后进行数据的修改。需要注意的地方是,在reducer中return的对象中,需要将state解构,否则状态就剩下一个num值了
const store = {  
    age:18,  
    num:1  
}	//	数据仓库  
const reducer = (state, action) => {  
    switch(action.type){  
        case 'add':  
            return {  
                ...state,  
                num: action.num+1  
            }  
        default:  
            return {  
                ...state  
            }  
    }  
} //	管理者  
function StateFunction () {  
    const [state,dispacth] = useReducer(reducer,store)  //	①  
    return (  
        <div>  
            <button onClick={ () => {  
                dispacth({  
                    type: 'add',  
                    num: state.num  
                })  
            } }>  
                增加num的值+1  
            </button>  
            <br></br>  
            这是一个函数式组件——num:{  state.num }  
        </div>  
    )  
}  

你可能感兴趣的:(react,react.js,前端,前端框架)