useCallback与useMemo源码浅析

5,388 阅读5分钟

useCallback

基本示例

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

返回一个memoized的函数,内联回调函数及依赖项数组作为参数传入useCallback,该回调函数只有在依赖项改变的时候才会更新,避免非必要的渲染。我在实际工作中因为用了eslint的一个配置,依赖项自动给加上。

react-hooks/exhaustive-deps: 'error'/'warn'

然后也不具体看依赖项,几次导致循环调用(后面我给出发生循坏调用的代码),所以后面我们就去掉了这个eslint-rule

Memoization

Memoization这里很有必要提下这个。理解为缓存,看了下hooks的源码,基本上都用了Memoization这个概念。

简易版的useCallback

let hookStates = [];
let hookIndex = 0;
function useCallbacks(callback, dependencies) {
  if (hookStates[hookIndex]) {   // 说明不是第一次渲染
    let [lastCallback, lastDependencies] = hookStates[hookIndex];
    let same = dependencies.every((item, index) => item === lastDependencies);
    if (same) {
      hookIndex++;
      return lastCallback;
    } 
  }
   // 第一次渲染 或者 不是第一次但是依赖项相同,都返回新的
  hookStates[hookIndex++] = [callback, dependencies];
  return callback;
}

源码

export function useCallback<T>(
  callback: T,
  inputs: Array<mixed> | void | null,
): T {
  currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
  workInProgressHook = createWorkInProgressHook();
  // 以上两句几乎在hooks中都用到了

  const nextInputs =
    inputs !== undefined && inputs !== null ? inputs : [callback];  // 新的依赖项,如果为undefined或者null的话, 则用callback,否则用依赖项

  const prevState = workInProgressHook.memoizedState;
  if (prevState !== null) {
    const prevInputs = prevState[1];  // 依赖项
    if (areHookInputsEqual(nextInputs, prevInputs)) {  //对比依赖项,相同从memoizedState获取
      return prevState[0];  // 返回上一个memoizedState的callback
    }
  }
  workInProgressHook.memoizedState = [callback, nextInputs]; // 首次或非首次不相同存入memoizedState
  return callback;  // 返回callback
}

useMemo

基本用法

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

两个参数。一个回调,一个依赖项,与useCallback没什么不同,返回一个memoized的函数。甚至示例也给出了一个隐藏的提示,昂贵的计算时才用。

简易版的useMemo

  
function useMemos(nextCreate, dependencies) {
  if (hookStates[hookIndex]) {   // 说明不是第一次渲染
    let [lastMemo, lastDependencies] = hookStates[hookIndex];
    let same = dependencies.every((item, index) => item === lastDependencies);
    if (same) {
      hookIndex++;
      return lastMemo;
    }
  }
  const nextValue = nextCreate();  // 此处有点不用
    // 第一次渲染 或者 不是第一次但是依赖项相同,都返回新的
    hookStates[hookIndex++] = [nextValue, dependencies];
    return nextValue;
  }

源码

export function useMemo<T>(
  nextCreate: () => T,
  inputs: Array<mixed> | void | null,
): T {
  currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
  workInProgressHook = createWorkInProgressHook();

  const nextInputs =
    inputs !== undefined && inputs !== null ? inputs : [nextCreate];
  const prevState = workInProgressHook.memoizedState;
  if (prevState !== null) {
    const prevInputs = prevState[1];
    if (areHookInputsEqual(nextInputs, prevInputs)) {
      return prevState[0];
    }
  }

  const nextValue = nextCreate();  // 除了这句,其他的和useCallback一模一样,不能你从否从这句看出useMemo与useCallback分别适用于什么场景
  workInProgressHook.memoizedState = [nextValue, nextInputs];
  return nextValue;
}

使用场景

源码的区别

有趣的是在源码中,两个hooks在第一个入参的时候声明的类型有所区别,不知道你没有注意的

useCallback

 export function useCallback<T>(
  callback: T,
  inputs: Array<mixed> | void | null,
): T {
}

useMemo

export function useMemo<T>(
  nextCreate: () => T,
  inputs: Array<mixed> | void | null,
): T {
}

感觉useMemo用来专门处理函数,虽然useCallback也可以做到,但我看了不少例子。useMemo这样子用,这好像形成了一个约定

useMemo

const initialCandies = React.useMemo(
  () => ['snickers', 'skittles', 'twix', 'milky way'],
  [],
 )

useCallback

const dispense = candy => {
    setCandies(allCandies => allCandies.filter(c => c !== candy))
  }
  const dispenseCallback = React.useCallback(dispense, [])

究竟何时用

推荐阅读usememo-and-usecallback,这篇是译文。原作者是Kent C. Dodds

引用相等

理解为依赖项引用(通常表现为非原始类型,如对象、数组、函数。 他们看来类型和属性都是一样的,但是引用却不同)相等的情况下,可以用useCallback 或者useMemo,如

function Foo({bar, baz}) {
  const options = {bar, baz}
  React.useEffect(() => {
    buzz(options)
  }, [options]) // we want this to re-run if bar or baz change
  return <div>foobar</div>
}

function Blub() {
  return <Foo bar="bar value" baz={3} />
}

能看出这段代码有问题吗。 每次都会调用useEffect的回调。为了更好的测试,我将代码稍微改变了下

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      bar: ['1'],
      baz: ['1'],
    };
  }

  handleChangeBazInput = (e) => {
    this.setState({
      baz: [e.target.value],
    });
  };

  handleChangeBarInput = (e) => {
    this.setState({
      bar: [e.target.value],
    });
  };

  render() {
    return (
      <div>
        <input type="text" onBlur={this.handleChangeBazInput}></input>
        <input type="text" onBlur={this.handleChangeBarInput}></input>
        <Foo baz={this.state.baz} bar={this.state.bar}></Foo>
      </div>
    );
  }
}

function Foo({ bar, baz }) {
  const options = { bar, baz };
  React.useEffect(() => {
    console.log(options);  // 不管 bar, baz有没有发生变化,这里都会打印出来
  }, [options]); // 即使改成改成[baz,bar]也没有用,因为bar、baz是数组
  return (
    <div>
      <div> {baz}</div> 
      <div> {bar}</div>
    </div>
  );
}
 ReactDOM.render(<App />, document.getElementById("root"));

但是如果bar、baz是一般类型的值,如string,就不会啦

this.state = {
      bar: '1',
      baz: '1',
    };
  }

  handleChangeBazInput = (e) => {
    this.setState({
      baz: e.target.value,
    });
  };

  handleChangeBarInput = (e) => {
    this.setState({
      bar: e.target.value,
    });
  };
  
  React.useEffect(() => {
    console.log(options); 
  }, [bar,baz]);  // 注意这里不是options

这是作者给出的实例,我觉得没有很具体

before

function Foo({bar, baz}) {
  React.useEffect(() => {
    const options = {bar, baz}
    buzz(options)
  }, [bar, baz]) // we want this to re-run if bar or baz change
  return <div>foobar</div>
}
function Blub() {
  const bar = () => {}
  const baz = [1, 2, 3]
  return <Foo bar={bar} baz={baz} />
}

after

function Foo({bar, baz}) {
  React.useEffect(() => {
    const options = {bar, baz}
    buzz(options)
  }, [bar, baz])
  return <div>foobar</div>
}

function Blub() {
  const bar = React.useCallback(() => {}, [])
  const baz = React.useMemo(() => [1, 2, 3], [])
  return <Foo bar={bar} baz={baz} />
}

我自己尝试用usecallback包了一层,发现和之前以前。当bar、baz是数组时,永远都会打印

function App() {
  const [bar, setBar] = useState(["1"]);
  const [baz, setBaz] = useState(["1"]);

  const handleChangeBazInput = useCallback((e) => {
    setBaz([e.target.value]);
  }, []);

  const handleChangeBarInput = useCallback((e) => {
    setBar([e.target.value]);
  }, []);

  return (
    <div>
      <input type="text" onBlur={(e) => handleChangeBazInput(e)}></input>
      <input type="text" onBlur={(e) => handleChangeBarInput(e)}></input>
      <Foo baz={baz} bar={bar}></Foo>
    </div>
  );
}

function Foo({ bar, baz }) {
  const options = { bar, baz };
  React.useEffect(() => {
    console.log(options);  // 这样还是会打印
  }, [bar, baz]); 
  return (
    <div>
      {" "}
      <div>{baz}</div> <div> {bar}</div>
    </div>
  );
}

不知道我理解是不是有问题,如果你知道的话可以告诉我

巨大开销

这个没啥说的,因为我们工作中很少会碰到巨大的计算,压根用不着

before

function RenderPrimes({iterations, multiplier}) {
  const primes = calculatePrimes(iterations, multiplier)
  return <div>Primes! {primes}</div>
}

after

function RenderPrimes({iterations, multiplier}) {
  const primes = React.useMemo(() => calculatePrimes(iterations, multiplier), [
    iterations,
    multiplier,
  ])
  return <div>Primes! {primes}</div>
}

产生死循环的代码


  const searchList = useCallback(() => {
   fetchMyVisitList({
      page: {
        pageNo: 1,
        pageSize: 20
      },
      params: {}
    }).then(res => {
      if (res.success) {
       // do something
      }
    })
  }, [list])

  useEffect(() => {
    searchList()
  }, [searchList])
// 所有的依赖是自动加上的

总结

首先说了useCallbackuseMemo的基本用法,然后写了个简易版的,再从源码分析。思路都是一样的,都用了Memoization这个概念。接着说了使用的场景,我问了一些人,他们平时很少用这两个hooks,诚如Kent C. Dodds所言,性能优化都需要成本,当你不需要性能优化时,你根本不需要用,用的话反而效果更不好。最后我写了个关于用useCallback和不用useCallback的例子,发现根本没区别,不知道是不是哪里没有理解到位,如果你知道的话可以告诉我