React hooks之useCallback的使用与性能分析

使用useCallback优化代码

useCallback是对传过来的回调函数优化,返回的是一个函数;useMemo返回值可以是任何,函数,对象等都可以。
简单来说就是返回一个函数,只有在依赖项发生变化的时候才会更新(返回一个新的函数)。

1.原理分析

useCallback是React Hooks中的一个函数,用于优化函数组件的性能。它的作用是返回一个memoized(记忆化的)函数,这个函数只有在依赖项发生变化时才会重新计算,否则会直接返回上一次计算的结果。
useCallback是对传过来的回调函数优化,返回的是一个函数;useMemo返回值可以是任何,函数,对象等都可以。
简单来说就是返回一个函数,只有在依赖项发生变化的时候才会更新(返回一个新的函数)。

2.案例分析:

父组件定义一个请求函数fetchData,和一个状态query,将query当作fetchData的参数,将该函数传递进子组件,当父组件query发生变化时,让子组件调用该函数发起请求。

class Parent extends Component {
  state = {
    query: 'react'
  };
  fetchData = () => {    
    const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query;    // ... Fetch data and do something ...  };  render() {
    return <Child fetchData={this.fetchData} />;
  }
}

class Child extends Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.props.fetchData();
  }
  componentDidUpdate(prevProps) {
    //  This condition will never be true
    if (this.props.fetchData !== prevProps.fetchData) {
      this.props.fetchData();
    }
  }
  render() {
    // ...
  }
}

在本代码中,fetchData是一个class方法!(或者你也可以说是class属性)它不会因为状态的改变而不同,所以this.props.fetchData和 prevProps.fetchData始终相等,因此不会重新请求。

2.1旧思维–优化该案例:

子组件使用:

componentDidUpdate(prevProps) {
    this.props.fetchData();
}

这样可以发起请求,但是会在每次渲染后都去请求。
或者改变父组件:

render() {
    return <Child fetchData={this.fetchData.bind(this, this.state.query)} />;
}

但这样一来,this.props.fetchData !== prevProps.fetchData 表达式永远是true,即使query并未改变。这会导致我们总是去请求。(bind() 方法会创建一个新的函数对象)
唯一现实可行的办法是把query本身传入 Child 组件。 Child 虽然实际并没有直接使用这个query的值,但能在它改变的时候触发一次重新请求:

class Parent extends Component {
  state = {
    query: 'react'
  };
  fetchData = () => {
    const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query;
    // ... Fetch data and do something ...
  };
  render() {
    return <Child fetchData={this.fetchData} query={this.state.query} />;  }
}

class Child extends Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.props.fetchData();
  }
  componentDidUpdate(prevProps) {
    if (this.props.query !== prevProps.query) {      this.props.fetchData();    }  }
  render() {
    // ...
  }
}

在class组件中,函数属性本身并不是数据流的一部分。使用useCallback,函数完全可以参与到数据流中。我们可以说如果一个函数的输入改变了,这个函数就改变了。如果没有,函数也不会改变。

2.2开始使用hooks:

场景一: 使用函数组件
但父组件不使用useCallback处理函数

import React, { useCallback, useState, useEffect } from 'react';
import './App.css';

function App() {
  const [query, setQuery] = useState(1);
  const [queryOther, setQueryOther] = useState(1);
  const fecthData = () => {
    console.log('新的fetch');
    return query;
  }
  const add = () => {
    console.log('点击add');
    setQuery(query + 1);
  }
  const addOther = () => {
    console.log('点击addOther');
    setQueryOther(queryOther + 1);
  }
  return (
    <>
      <Child fecthData={fecthData} />
      <button onClick={add}>+1</button>
      <button onClick={addOther}>other+1</button>
      <div>{ query }</div>
    </>
  );
}

function Child({ fecthData }: { fecthData: any }) {
  console.log('子组件相关内容');
  useEffect(() => {
    const querN = fecthData();
    console.log('子组件调用该函数获取到相关内容', querN);
  }, [fecthData])
  return <div>
    123
  </div>
}

export default App;

初始化的时候:
React hooks之useCallback的使用与性能分析_第1张图片
点击按钮:

React hooks之useCallback的使用与性能分析_第2张图片
但是从图里面可以看到,点击addOther时,并没有使得query发生变化,但是子组件仍然调用了该函数发起请求。可以看到这种方法需求可以使得子组件在父组件的状态query发生变化时,成功发起了请求,但是还是存在副作用。
问题的原因在于状态queryOther的改变,使得父组件重新渲染,重新生成了fecthData函数,并返回了该函数新的地址,导致子组件刷新。

场景二:父组件使用useCallback处理函数

import React, { useCallback, useState, useEffect } from 'react';
import './App.css';

function App() {
  const [query, setQuery] = useState(1);
  const [queryOther, setQueryOther] = useState(1);
  const fecthData = useCallback(() => {
    console.log('新的fetch');
    return query;
  }, [query])
  const add = () => {
    console.log('点击add');
    setQuery(query + 1);
  }
  const addOther = () => {
    console.log('点击addOther');
    setQueryOther(queryOther + 1);
  }
  return (
    <>
      <Child fecthData={fecthData} />
      <button onClick={add}>+1</button>
      <button onClick={addOther}>other+1</button>
      <div>{ query }</div>
    </>
  );
}

function Child({ fecthData }: { fecthData: any }) {
  console.log('子组件相关内容');
  useEffect(() => {
    const querN = fecthData();
    console.log('子组件调用该函数获取到相关内容', querN);
  }, [fecthData])
  return <div>
    123
  </div>
}

export default App;

初始状态
React hooks之useCallback的使用与性能分析_第3张图片
点击按钮:
React hooks之useCallback的使用与性能分析_第4张图片

可以看到只有点击+1按钮改变query才会使得子组件发起请求,点击other+1已经没有处罚上文副作用。

原因分析:
使用了useCallback,useCallback的工作原理是什么?useCallBack的本质工作不是在依赖不变的情况下阻止函数创建,而是在依赖不变的情况下不返回新的函数地址而返回旧的函数地址。不论是否使用useCallBack都无法阻止组件render时函数的重新创建。在本例子中点击按钮other+1并没有使得query发生变化,所以并没有返回新的fetchData函数地址,又因为在子组件中使用useEffect对fetchData监听时,所以子组件不会发起请求。但是,点击按钮other+1时,子组件虽然没发起请求,但是还是刷新了,这是什么原因呢?这是因为子组件直接在父组件中挂载,没有做过任何优化,当父组件重新渲染时,会导致子组件也跟着渲染。所以单纯的使用useCallback可以监听到相应变化,使得子组件做出变化,但是并不能优化性能。所以当我们不用监听某个状态使得函数发生改变时,不要轻易使用useCallback,因为使用 useCallBack后每次执行到这里内部比对是否变化,还有存一下之前的函数,消耗更大了。

场景三:优化上述问题,搭配React.memo使用

import React, { useCallback, useState, useEffect } from 'react';
import './App.css';

function App() {
  const [query, setQuery] = useState(1);
  const [queryOther, setQueryOther] = useState(1);
  const fecthData = useCallback(() => {
    console.log('新的fetch');
    return query;
  }, [query])
  const add = () => {
    console.log('点击add');
    setQuery(query + 1);
  }
  const addOther = () => {
    console.log('点击addOther');
    setQueryOther(queryOther + 1);
  }
  return (
    <>
      <Child fecthData={fecthData} />
      <button onClick={add}>+1</button>
      <button onClick={addOther}>other+1</button>
      <div>{ query }</div>  ,,m

const Child = React.memo(({ fecthData }: { fecthData: any }) => {
  console.log('子组件相关内容');
  useEffect(() => {
    const querN = fecthData();
    console.log('子组件调用该函数获取到相关内容', querN);
  }, [fecthData])
  return <div>
    123
  </div>
})


export default App;

初始状态:
React hooks之useCallback的使用与性能分析_第5张图片
点击按钮:
React hooks之useCallback的使用与性能分析_第6张图片
一切问题都解决了。点击other+1按钮,没有使得子组件发起请求,也没有使得子组件因为这个无关变量的变化,导致重新渲染。

原因分析:

  • 使用useCallback使得无关变量变化时,阻止了新创建的fetchData的新地址返回,传给子组件的还是原本的函数地址(useCallBack的本质工作不是在依赖不变的情况下阻止函数创建,而是在依赖不变的情况下不返回新的函数地址而返回旧的函数地址)
  • React.memo 这个方法,此方法内会对 props 做一个浅层比较,如果如果 props 没有发生改变(useCallback的存在使得props没变化),则不会重新渲染此组件。

场景四:单纯使用React.memo会发生什么

import React, { useCallback, useState, useEffect } from 'react';
import './App.css';

function App() {
  const [query, setQuery] = useState(1);
  const [queryOther, setQueryOther] = useState(1);
  const fecthData = () => {
    console.log('新的fetch');
    return query;
  }
  const add = () => {
    console.log('点击add');
    setQuery(query + 1);
  }
  const addOther = () => {
    console.log('点击addOther');
    setQueryOther(queryOther + 1);
  }
  return (
    <>
      <Child fecthData={fecthData} />
      <button onClick={add}>+1</button>
      <button onClick={addOther}>other+1</button>
      <div>{ query }</div>
    </>
  );
}

const Child = React.memo(({ fecthData }: { fecthData: any }) => {
  console.log('子组件相关内容');
  useEffect(() => {
    const querN = fecthData();
    console.log('子组件调用该函数获取到相关内容', querN);
  }, [fecthData])
  return <div>
    123
  </div>
})


export default App;

初始状态:
React hooks之useCallback的使用与性能分析_第7张图片
点击按钮
React hooks之useCallback的使用与性能分析_第8张图片
React.memo检测的是props中数据的栈地址是否改变。而父组件重新构建的时候,会重新构建父组件中的所有函数(旧函数销毁,新函数创建,等于更新了函数地址),新的函数地址传入到子组件中被props检测到栈地址更新。也就引发了子组件的重新渲染。所以,在上面的代码示例里面,子组件是要被重新渲染的。上文中的fetchData因为失去了useCallback的保护使得子组件的props发生了变化,从而React.memo也失去了作用,而且因为fetchData因为失去了useCallback的保护,使得点击other+1按钮改变无关的变量时,子组件也调用了请求函数。

3.useCallback使用总结:

  • 可以使用useCallback可以监听到相应状态变化,使得父/子组件做出响应。
  • 但是滥用useCallback会影响性能,需搭配React.memo进行使用,否则适得其反。

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