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 进阶 #50

Open
SunShinewyf opened this issue Jul 19, 2019 · 2 comments
Open

React Hooks 进阶 #50

SunShinewyf opened this issue Jul 19, 2019 · 2 comments
Labels

Comments

@SunShinewyf
Copy link
Owner

SunShinewyf commented Jul 19, 2019

React Hooks 进阶

前言

上一篇简单地介绍了一下 React Hooks 的背景和 API 的使用,这一篇深入探索一下 React Hooks 的实践和原理。

React Hooks 实践

模拟 Class Component 的生命周期

有的时候还是需要根据不同的生命周期来处理一些逻辑,React Hooks 几乎可以模拟出全部的生命周期。

componentDidMount

使用 useEffect 来实现,如下:

useEffect(()=> {
  	//ComponentDidMount do something
},[]);

useEffect 第二个参数传空数组时,表示只会在执行一次。

componentWillUnMount

同样可以使用 useEffect 来实现,如下:

useEffect(()=> {
	return ()=> {
    // ComponentWillUnMount do something
  }
},[])

componentDidUpdate

componentDidUpdate 生命周期在组件每次更新之后执行,除了初始化 render 的时候不执行,所以可以设置一个标志位来判断是否是第一次 render,使用 useEffect + useRef 配合就可以实现:

const firstRenderRef = useRef(true)

useEffect(()=>{
	if(firstRenderRef.current){
    // 如果是第一次 render,就设置为 false
  	firstRenderRef.current = false;
  } else {
    // componentDidUpdate do something
  }

})

getDerivedStateFromProps

getDeriverdStateFromProps 是 react 新版本中用来替代 componentWillReceiveProps,它可以感知 props 的变化,从而更新组件内部的 state,用 hooks 模拟这个生命周期,可以这样实现:

function Child(props){
	const [count,setCount] = useState(0);
  if(props.count !== count){
  	setCount(props.count);
  }
}

shouldComponentUpdate

React 16.6 引入 React.memo,是用来控制 Function Component 的重新渲染的,类似于 Class Component 的 PureComponent,可以跳过 props 没有变化时的更新,为了支持更加灵活的 props 对比,它还提供了第二个函数参数 areEqual(prevProps, nextProps),和 shouldComponentUpdate 相反的是,当该函数返回 true 时表示不更新函数,返回 false 则重新更新,用法如下:

function Child(props){
    return <h2>{props.count}</h2>
}
// 模拟shouldComponentUpdate
const areEqual = (prevProps, nextProps) => {
   //比较
};

const PureChild = React.memo(Child, areEqual)

除了上面这种方法可以模拟 shouldComponentUpdate 之外,React Hooks 还提供一个 useMemo 用来控制子组件重新渲染的,举一个例子如下:

// Parent 组件
function Parent() {
  	const [count,setCount] = useState(0);
  	const child = useMemo(()=> <Child count={count} />, [count]);
  	return <>{count}</>
}

// Child 组件
function Child(props) {
    return <div>Count:{props.count}</div>
}

在上面的例子中,只有 Parent 组件中的 count state 更新了,Child 才会重新渲染,否则不会。

React Hooks 原理

还记得我们之前讲过的使用 React Hooks 的两条规则吗?

  • 只在 React 函数和 自定义 Hooks 中使用,不要在普通 js 中使用 Hooks
  • 只在顶层使用 Hook,不在循环、条件或者嵌套函数中调用 Hook

现在我们来一一剖析一下为什么会有这个限制?

只能在 React 函数和自定义 Hooks 中使用

翻到 ReactHooks 对应的源码,贴出 Hooks 的定义如下:

// useState
export function useState<S>(initialState: (() => S) | S) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}
// useEffect
export function useEffect(
  create: () => (() => void) | void,
  inputs: Array<mixed> | void | null,
) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, inputs);
}
// useRef
export function useRef<T>(initialValue: T): {current: T} {
  const dispatcher = resolveDispatcher();
  return dispatcher.useRef(initialValue);
}
...
//其他的都类似

所有的 Hooks 基本都调用了这个 resolveDispatcher(),定位到 resolveDispatcher,代码如下:

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  invariant(
    dispatcher !== null,
    'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
      ' one of the following reasons:\n' +
      '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
      '2. You might be breaking the Rules of Hooks\n' +
      '3. You might have more than one copy of React in the same app\n' +
      'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
  );
  return dispatcher;
}

如果 ReactCurrentDispatcher.current 是空的,就会得出我们使用 Hooks 的方式不对,只有在 React 环境中才会给 ReactCurrentDispatcher 的 current 赋值,所以就可以解这个问题。

不在循环、条件或者嵌套函数中调用 Hook

为什么不能在循环、条件或者嵌套函数中调用 Hook,我们还是从源码出发寻找原因:
Hooks 的实现源码在 ReactFiberHooks.js
在这个文件中,定义了 firstWorkInProgressHook 和 workInProgressHook 这两个全局变量,观察所有的 Hooks 实现,发现都执行了 const hook = 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 时的流程:

  • 初始时,组件还未渲染时,firstWorkInProgressHook = workInProgressHook = null;
  • 初次渲染
    • 定义第一个 Hook 时:firstWorkInProgressHook = workInProgressHook = hook;
    • 定义第二个 Hook 时:workInProgressHook = workInProgressHook.next = hook;
    • 定义第三个 Hook 时:workInProgressHook = workInProgressHook.next = hook;

这种结构就是一个链表结构,而每一个 Hook 的结构如下:

type Hook = {
  memoizedState: any,
  baseState: any,
  baseUpdate: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null,
};

type Effect = {
  tag: HookEffectTag,
  create: () => (() => void) | void,
  destroy: (() => void) | void,
  deps: Array<mixed> | null,
  next: Effect,
};

其中 memoizedState 存储当前 Hook 的结果,next 则连接到下一个 Hook,从而将所有 Hook 进行串联起来。这个链表结果存储在 Fiber 对象的 memoizedState 属性中,在 React 中,每个节点都对应一个 Fiber 对象,而 Fiber 的 memoizedState 用来存储该节点在上次渲染中的 state,这个属性是 Class Component 用来存储节点的 state 的,这也就是为什么 Hook 可以拥有 Class Component 功能的原因。
链表结构用图形显示如下:

images

在第二次渲染时,也就是 update 的时候,此时调用的是 Hook 对应的 update 方法,而 update 方法又分别执行了 updateWorkInProgressHook(),先来看看这个方法的实现:

function updateWorkInProgressHook(): Hook {
  if (nextWorkInProgressHook !== null) {
    // There's already a work-in-progress. Reuse it.
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
    nextCurrentHook = currentHook !== null ? currentHook.next : null;
  } else {
    // Clone from the current hook.
    invariant(
      nextCurrentHook !== null,
      'Rendered more hooks than during the previous render.',
    );
    currentHook = nextCurrentHook;

    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      queue: currentHook.queue,
      baseUpdate: currentHook.baseUpdate,

      next: null,
    };

    if (workInProgressHook === null) {
      // This is the first hook in the list.
      workInProgressHook = firstWorkInProgressHook = newHook;
    } else {
      // Append to the end of the list.
      workInProgressHook = workInProgressHook.next = newHook;
    }
    nextCurrentHook = currentHook.next;
  }
  return workInProgressHook;
}

在这个方法中,它会获取渲染时生成的 Hooks,并获取当前 update 的是处于链表的哪个节点,然后返回。
假如在条件语句中使用 Hook,如下:

let condition = true;
const [state1,setState1] = useState(0);
if(condition){
    const [state2,setState2] = useState(1);
    condition = false;
}
const [state3,setState3] = useState(2);

初始渲染时,拿到的是 state1 => hook1,state2 => hook2,state3 => hook3,再次渲染时,condition 条件不满足,那么执行 state3 时拿到的就是 hook2,那整个逻辑就乱套了...

结语

React Hooks 解决了一部分问题,但同时自身也有一定的缺陷,比如要遵守一定规则、组件嵌套层次不明显导致 bug 定位难。所以在实际的开发实践中,还是要评估再选型。

@shenghou
Copy link

👍

@ttthing111
Copy link

我宣布 w女士就是我女神了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants