Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

浅谈 React Hooks(二) #6

Open
sliwey-zz opened this issue Mar 3, 2019 · 0 comments
Open

浅谈 React Hooks(二) #6

sliwey-zz opened this issue Mar 3, 2019 · 0 comments

Comments

@sliwey-zz
Copy link
Owner

上一篇文章中,我们谈到 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,具体代码可以看这里:https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberHooks.js#L974

我们一点点来看,先看下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来处理。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant