阅读 966

浅谈 React Hooks(二)

上一篇文章中,我们谈到 Hooks 给 React 带来的一些在开发体验上的改变,如果你已经开始尝试 React Hooks,也许你会跟我一样碰到一个令人疑惑的地方,如果没有的话,那就再好不过啦,我就权当做个记录,以便他人之需。

如何绑定事件?

我们先以官方的例子开始:

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

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

看到onClick绑定的那个匿名函数了吗?这样写的话,每次 render 的时候都会重新生成一个新的函数。这在之前可能不需要太在意,因为我们一般只是拿 Function Component 来实现一些展示型组件,在其之下不会有太多的子组件。但是如果我们拥抱 Hooks 之后,那么就不可控了。

虽然说在一般情况下,这并不会造成太大的性能问题,而且 Function Component 本身的性能就要比 Class Component 更好一点,但是难免会碰到需要优化的时候,比方说在重构原来的 Class Component 的时候,其中有个子组件是个PureComponent,便会使子组件的这个优化失效 ,那么怎么解决呢?

使用useCallbackuseMemo来保存函数的引用,避免重复生成新的函数

function Counter() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => {
    setCount(count => count + 1)
  }, []);
  
  // 或者用useMemo
  // const handleClick = useMemo(() => () => {setCount(count => count + 1)}, []);
   
  return (
    <div>
      <p>count: {count}</p>
	  {/* Child为PureComponent */}
      <Child callback={handleClick} />
    </div>
  )
}
复制代码

可见useCallback(fn, inputs)等同于useMemo(() => fn, inputs),那么这两个 Hook 具体是怎么做到的呢?我们可以从源码中一窥究竟,我们以useCallback为例(useMemo大体上都是一样的,就返回值不同,后面会提到)。

首先,在第一次执行useCallback时,React内部会调用ReactFiberHooks中的mountCallback,之后再次执行时调用的都是updateCallback,具体代码可以看这里:github.com/facebook/re…

我们一点点来看,先看下mountCallback:

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
复制代码

发现核心在于mountWorkInProgressHook这个方法

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    queue: null,
    baseUpdate: null,

    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
复制代码

代码比较简单,就不一一解释了,从上面的代码我们可以得知 Hooks 的本体:

const hook = {
  memoizedState: null,
  baseState: null,
  queue: null,
  baseUpdate: null,
  next: null,
}
复制代码

我们主要关注memoizedStatenextmemoizedState在不同的 Hook 中存放的值会有所不同,在useCallback中存的就是入参的值[callback, deps]next的值就是下一个 hook,也就是说 Hooks 其实就是一个单向链表,这也就解释了为什么 Hooks 需要在顶层调用,不能在循环、条件语句、嵌套函数中使用,因为需要保证每次调用的顺序一致。

再来看之后的updateCallback:

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // 这个hook就是第一次mount的hook
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 所以这里的memoizedState就是mount时候存着的[callback, deps]
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      // 比较两次的deps,相同的话就直接返回之前存的callback,而不是新传进来的callback
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
复制代码

useMemo的实现与useCallback类似,大概看一下:

function mountMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  
  // 与useCallback不同的地方就是memoizedState中存的是nextCreate执行之后的结果
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
    
  // 返回执行结果
  return nextValue;
}

function updateMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  
  // 这里也一样,存的是nextCreate执行之后的结果
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  
    // 返回执行结果
  return nextValue;
}
复制代码

由以上代码便可以看出useCallbackuseMemo在用法上的区别了。

除了这两个方法以外,还可以通过context来传递由useReducer生成的dispatch方法,来避免直接传递callback,因为dispatch是不变的。这个方法跟前面两种有本质上的区别,它从源头上就阻止了callback的传递,所以也就不会有前面提到的性能方面的顾虑,这也是官方推荐的方法,特别是组件树很大的情况下。所以上面的代码如果通过这种方式来写的话,就会是下面这样,有点像Redux

import React, { useReducer, useContext } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    default:
      throw new Error();
  }
}

const TodosDispatch = React.createContext(null);

function Counter() {
  const [state, dispatch] = useReducer(reducer, {count: 0});

  return (
    <div>
      <p>count: {state.count}</p>
      <TodosDispatch.Provider value={dispatch}>
        <Child />
      </TodosDispatch.Provider>
    </div>
  )
}

function Child() {
  const dispatch = useContext(TodosDispatch);
  return (
    <button onClick={() => dispatch({type: 'increment'})}>
      click
    </button>
  )
}
复制代码

总结

  • 一般情况下,事件绑定可以直接通过箭头函数处理,不会有明显的性能问题,写起来也方便。
  • 如有需求,可以通过useCallbackuseMemo来优化。
  • 如果组件树比较大,传递callback的层级可能会很深,可以通过useReducer配合context来处理。

以上只是我个人的一些想法,如有不对之处,欢迎指正~~

原文链接:浅谈 React Hooks(二)