【万字长文】React Hooks的黑魔法

React 的 logo 是一个原子图案, 原子组成了物质的表现。相似的, React 就像原子般构成了页面的表现; 而 Hooks 就如夸克, 更接近 React 的本质, 可是直到 4 年后的今天才被设计出来。 —— Dan in React Conf(2018)html

正片开始

React在18年推出了hooks这一思想,以一种新的思惟模式去构建web App。咱们都知道,React认为,UI视图是数据的一种视觉映射,即UI = F(data),F须要负责对输入数据进行加工、并对数据的变动作出响应。 React给UI的复用提供了极大的便利,可是对于逻辑的复用,在Class Component中并非那么方便。在有Hooks以前,逻辑的复用一般是使用HOC来实现,使用HOC会带来一些问题:前端

  • 嵌套地狱,每一次HOC调用都会产生一个组件实例
  • 可使用类装饰器缓解组件嵌套带来的可维护性问题,但装饰器本质上仍是HOC

在有Hooks以前,Function Component只能单纯地接收props、事件,而后返回jsx,自己是无状态的组件,依赖props来响应数据(状态)的变动,而上面提到的依赖都是从Class Component传入的,因此在有Hooks以前Function Component是要彻底依赖Class Component存在的。可是这上面这些在Hooks出现以后所有都被打破。 本文不会介绍hooks的使用方式,详见 官方文档react

在使用了Hooks以后,你确定会对Hooks的原理很是感兴趣,本文要讲述的就是React Hooks的“黑魔法”,主要内容有:web

  1. Hooks的结构
  2. Hooks是如何区分是首次渲染(mount)仍是更新(update)的
  3. useState是如何使Function Component有状态的?
    • useState的原理
    • useState如何触发更新
  4. useEffect、useCallback、useMemo、useRef的实现
  5. 使用hooks的注意事项

Hooks的结构

一个Hook结构以下:数组

export type Hook = {
  memoizedState: any,

  baseState: any,
  baseUpdate: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,

  next: Hook | null,
};
复制代码

咱们都知道,在React v16版本推出了Fiber架构,每个节点都对应一个Fiber结构。一个Function Component中能够有不少个Hooks执行,最终造成的结构以下:缓存

上面这个图到这里还看不懂不要紧,下面让咱们开始深刻Hooks的原理。

Hooks如何区分Mount和Update

要知道这个问题的答案,首先须要了解React的Fiber架构。React定义了Fiber结构来描述一个节点,经过Fiber节点上的child、return、sibling指针构成整个App的结构。Fiber类型定义以下:bash

export type Fiber = {|
  tag: WorkTag,
  key: null | string,
  elementType: any,
  type: any,
  stateNode: any,
  return: Fiber | null,
  child: Fiber | null,
  sibling: Fiber | null,
  index: number,
  ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,
  pendingProps: any, // This type will be more specific once we overload the tag.
  memoizedProps: any, // The props used to create the output.
  updateQueue: UpdateQueue<any> | null,
  memoizedState: any,
  dependencies: Dependencies | null,
  mode: TypeOfMode,
  effectTag: SideEffectTag,
  nextEffect: Fiber | null,
  firstEffect: Fiber | null,
  lastEffect: Fiber | null,
  expirationTime: ExpirationTime,
  childExpirationTime: ExpirationTime,
  alternate: Fiber | null,
  // ...
|};
复制代码

用React Conf上的例子简单说明一下 Fiber 结构:List组件下面有四个子节点:一个button和三个Item。闭包

这个组件转换成Fiber tree结构以下:

一个 Function Component 最终也会生成一个 Fiber 节点挂载到 Fiber tree 中。React 还使用了 Double Buffer 的思想,在 React 内部会维护两棵 Fiber tree,一棵叫 Current,一棵叫 WorkInProgress,current 是最终展现在用户界面上的结构,workInProgress 用于后台进行diff更新操做。两棵树在更新结束的时候会互相调换,即 workInProgress 在更新以后会变为 current 展现在用户界面上,current 会变成 workInProgress 用于下次 update。两棵树之间对应的节点是经过Fiber结构上的 alternat e属性连接的,即 workInProgress.alternate = current,current.alternate = workInProgress架构

那这和Hooks有什么关系?其实React在初始渲染的时候,只会生成一棵workInProgress树,当整棵树构建完成以后,由workInProgress变为current,在下一次更新的时候才会生成第二棵树。因此当Function Component对应的Fiber节点发现本身的alternate属性为null,说明是第一次渲染。在React的源码中就是renderWithHooks函数中的这句(Function Component的mount和update过程会执行renderWithHooks):ide

export function renderWithHooks( // 判断是mount仍是update:fiber.memoizedState是否有值
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  props: any,
  refOrContext: any,
  nextRenderExpirationTime: ExpirationTime,
): any {
  // ...
  nextCurrentHook = current !== null ? current.memoizedState : null;

  ReactCurrentDispatcher.current =
    nextCurrentHook === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;

  // ...
}
复制代码

从代码中能看到current 等于 null的状况下,nextCurrentHook = null,致使下面的dispatcher取得是HooksDispatcherOnMount,这个HooksDispatcherOnMount就是在初始渲染Mount阶段对应的Hooks,HooksDispatcherOnUpdate显然就是在更新阶段时该调用的Hooks。

useState让函数组件有状态

上面知道了Hooks是如何区分Mount和Update以后,接下来分析useState的实现原理。

useState如何触发更新

state在Function Component中就是一个普通的常量,不存在诸如数据绑定之类的逻辑,更新都是经过useStatedispatch(useState返回的数组的第二个元素),触发了组件rerender,进而更新组件。

dispatch方法接收到最新的state后(就是第三个参数action),生成一个update,添加到queue的最后,用last指针指向这最新的一次更新,而后调用scheduleWork方法,这个scheduleWork就是触发react更新的起始方法,在Class Component中调用this.setState时最终也是执行了这个方法开始更新。

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A, // 最新的state
) {

  const alternate = fiber.alternate;
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // ...
  } else {
    const currentTime = requestCurrentTime();
    const suspenseConfig = requestCurrentSuspenseConfig();
    const expirationTime = computeExpirationForFiber(
      currentTime,
      fiber,
      suspenseConfig,
    );

    const update: Update<S, A> = {
      expirationTime,
      suspenseConfig,
      action,
      eagerReducer: null,
      eagerState: null,
      next: null,
    };

    // Append the update to the end of the list.
    const last = queue.last;
    if (last === null) {
      // This is the first update. Create a circular list.
      update.next = update;
    } else {
      const first = last.next;
      if (first !== null) {
        // Still circular.
        update.next = first;
      }
      last.next = update;
    }
    queue.last = update;

    // ...

    scheduleWork(fiber, expirationTime);
  }
}
复制代码

useState的原理

上面提到了Hooks的结构,每一次Hook的执行都会生成一个Hook结构,首次渲染的时候执行useState,会将传过来的initialState挂在Hook的memoizedState属性上,后续再获取状态就是从memoizedState属性上获取了。

mount阶段的useState:mountState

useState最终会返回一个有两个元素的数组,第一个元素是state,第二个元素是 修改state的方法 dispatch。 这里dispatch方法也须要关注一下:queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber, queue ),dispatch是dispatchAction方法经过bind传入当前的 Fiber 和 queue属性做为前两个参数生成的,因此每一个useState都有本身的dispatch方法,这个dispatch方法是固定做用在指定的Fiber上的(经过闭包锁定了前两个参数)

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<BasicStateAction<S>> = (queue.dispatch = (dispatchAction.bind(
    null, currentlyRenderingFiber, queue ));
  return [hook.memoizedState, dispatch];
}
复制代码

queue属性用一个Update结构记录了每次调用dispatch时候传过来的state,造成了一个链表结构,其last属性指向最新的一个state Update结构。Update结构以下,咱们须要关注的有:

  • action:最新传过来的state值
  • next:指向后面生成的Update结构

其余属性涉及到的内容不在本次讨论范围

update阶段的useState:updateReducer

update阶段,React首先会从Hooks上获取到 last、baseState、baseUpdate 属性,各个值的含义以下:

  • last:queue上挂载的最新一次的Update,里面包含了最新的state,在dispatch方法执行的时候挂载到queue上的
  • baseState:上一次更新后的state值。当dispatch传入的是一个函数的时候,这个值就是函数执行时传入的参数
  • baseUpdate:上一次更新生成的Update结构,做用是找到本次rerender的第一个为处理的Update节点(baseUpdate.next),即下面代码中的first表明第一个未处理的update
const hook = updateWorkInProgressHook();
const queue = hook.queue;

// The last update in the entire queue
const last = queue.last; // 新的值
// The last update that is part of the base state.
const baseUpdate = hook.baseUpdate; // 旧的值,next属性指向新的Update
const baseState = hook.baseState; // 旧的值

// Find the first unprocessed update.
let first;
if (baseUpdate !== null) {
  if (last !== null) {
    last.next = null;
  }
  first = baseUpdate.next;
} else {
  first = last !== null ? last.next : null;
}
复制代码

找到第一个未处理的update以后就须要循环对全部新增update进行处理,这里的变量newState虽然名字叫newState,在每次执行reducer以前的值都是 就的state值,因此当useState传入的值为一个函数的时候,咱们能够获取到上一次的state,由于旧的state值是有缓存的。 当处理完全部update以后就更新hooks对应的baseState、baseUpdate、memoizedState的值。

if (first !== null) {
  let newState = baseState;
  let newBaseState = null;
  let newBaseUpdate = null;
  let prevUpdate = baseUpdate;
  let update = first;
  let didSkip = false;
  do {
      // ...
    const action = update.action;

    newState = reducer(newState, action);

    prevUpdate = update;
    update = update.next;
  } while (update !== null && update !== first);

  if (!didSkip) {
    newBaseUpdate = prevUpdate;
    newBaseState = newState;
  }

  if (!is(newState, hook.memoizedState)) {
    markWorkInProgressReceivedUpdate();
  }

  hook.memoizedState = newState;
  hook.baseUpdate = newBaseUpdate;
  hook.baseState = newBaseState;

  queue.lastRenderedState = newState;
}

// reducer函数:
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === 'function' ? action(state) : action;
}
复制代码

最终仍然返回的是一个数组结构,包含了最新的state和dispatch方法

const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
复制代码

这样Function Component中获取到的state就是最新的值,Function Component的更新实际上就是从新执行一次函数,获得 jsx,剩下 reconcile 和 commit 阶段的就与 class Component是同样的了

useEffect的实现

咱们知道useEffect传入的函数是在绘制以后才执行的,因此当执行function component执行的时候确定不是 useEffect 的第一个函数参数执行的时候,那在执行useEffect的时候都作了什么呢?

执行useEffect时是在为后面作准备。useEffect会生成一个Effect结构,Effect结构以下:

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

create就是咱们传入的参数,咱们若是想取消一个反作用的话是经过create执行返回的结果(仍然是一个函数),React会在内部调用这个函数。由于create函数是在 绘制以后执行的,因此这个时候Effect的destory是null,在后面真正执行create的时候会赋值destory

生成了Effect结构以后就要将其挂载到Hooks的memorizedState上,React不光将Effect挂载到了Hook结构上,也将其直接和Fiber挂钩:

React会将全部的effect造成一个环形链表,保存在FunctionComponentUpdateQueue上,其lastEffect指向最新生成的effect。 为何要作一个环形链表保存全部的effect? 我认为主要是:

  • 环形链表能够很方便的找到头结点(第一个effect),能够处理全部的effect
  • 保存全部的effect是由于effect可能有须要unmount的时候销毁的操做,保存以前的effect能够调用其destory方法销毁,避免内存泄露

最终处理上面保存的effects的函数在commit阶段的commitHookEffectList函数中,代码以下,主要工做就是:循环处理全部的effect,判断须要销毁仍是须要执行,循环终止条件就是从新回到环形链表的第一个节点。删减后的代码以下:

// ReactFiberCommitWork.js
function commitHookEffectList(
  unmountTag: number,
  mountTag: number,
  finishedWork: Fiber,
) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & unmountTag) !== NoHookEffect) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          destroy();
        }
      }
      if ((effect.tag & mountTag) !== NoHookEffect) {
        // Mount
        const create = effect.create;
        effect.destroy = create();

      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
复制代码

useCallback和useMemo的实现

useCallbackuseMemo是十分类似(useCallback能够经过useMemo实现),因此这里咱们看一下useMemo的实现: useMemo传入一个函数fn和一个数组dep,当dep中的值没发生变化的时候,就会一直返回以前函数fn执行后返回的值,因此咱们首先执行函数fn,将返回的结果和依赖数组dep保存起来就行了, React 内部是这么处理的:

const nextValue = nextCreate(); // 传入的函数Fn
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
复制代码

当触发re-render的时候再次执行 useMemo,React会从 hook.memoizedState 上面取出以前保存的dep,也就是hook.memoizedState的第二个元素。比较以前的dep和新传入的dep的每一个元素是否相同(浅比较),若是相同则返回原来保存的值(hook.memoizedState的第一个元素),不相同则从新执行 函数Fn 从新生成返回值并保存。

const prevState = hook.memoizedState;
if (prevState !== null) {
  if (nextDeps !== null) {
    const prevDeps: Array<mixed> | null = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      return prevState[0];
    }
  }
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
复制代码

useCallback原理相同,只不过保存的值不一样,一个是函数,一个是函数的执行结果。

useRef的实现

useRef的实现是最简单的了,咱们先回顾一下useRef的做用:使用useRef生成一个对象以后能够经过 current 属性获取到一个值,这个是不会随着re-render而改变,只能咱们本身手动改变。固然也能够传递给组件的 ref 属性使用。常常被类比为 Class Component的实例属性,好比 this.xxx = xxx

在开头部分咱们知道 Hooks 是基于 fiber 结构的,fiber 是 react 内部维护的结构,会在整个react生命周期中存在,因此useRef最简单的实现就是 挂载到 fiber 上,做为fiber的一个属性存在,这样经过 fiber 就一直能获取到值。

可是真正设计的时候确定不能这样来,由于 Hooks 是有本身的结构的,因此就把 useRef 的值挂载到 Hook 结构的 memorizedState上 就能够了,因此你看到的 useRef 的结构是这样的:

将 useRef的值挂载到 对象的 current属性上,从 current属性上能获取到值

使用hooks的注意事项

使用React.memo减小没必要要的组件渲染

React.memo is equivalent to PureComponent, but it only compares props. (You can also add a second argument to specify a custom comparison function that takes the old and new props. If it returns true, the update is skipped.)

使用 React.memo包裹组件以后,当父组件传过来的props不变时,子组件不会re-render。举个例子:

const ChildComponent = React.memo((props) => {
    return (
        <div>{props.name}</div>
    )
}, compare)
复制代码

React.memo是对新旧Props进行浅比较,也能够自定义compare函数比较nextPropsprevProps,浅比较就会带来问题:每次Function Component执行内部的对象都会从新生成,这个时候若是传给子组件的是一个对象的话,其实仍是会形成刷新。例:

function ParentComponent() {
    const [state, setState] = useState(0)
    function handleClickButton() {
       console.log(state)
        // balabalabala
    }
    const someProps = {
        name: 'xxx'
    }
    return (
        <div>
            <input />
            <ChildExpensiveComponent onPress={handleClickButton} someProps={someProps}/>
        </div>
    )
}
复制代码

因此这里咱们想到用 useMemouseCallback 来保存值,这样只要依赖不变值就不变。因此下面代码这种改变以后确实不会触发没必要要的刷新了

function ParentComponent() {
    const [state, setState] = useState(0)
    const handleClickButton  = useCallback(() => {
        // balabalabala
        console.log(state) // 0
    }, [])
    const memoProps = useMemo(() => {
        return {
            name: 'xxx'
        }
    }, [])
    return (
        <div>
            <p>{state}</p>
            <ChildExpensiveComponent onPress={handleClickButton} someProps={memoProps}/>
        </div>
    )
}
复制代码

可是Hooks是经过closure实现的,除useRef以外,其余的Hooks都会存在capture values的特色。上面例子中handleClickButton每次执行,不管state如何变化,打印的state值将一直是0。 由于useCallback经过闭包保存了一开始的state的值,这个值不会像Class Component同样每次都会取到最新的值。

那咱们是否是给handleClick加个dependence就好了,像这样:

const handleClickButton  = useCallback(() => {
    // balabalabala
    console.log(state) // 0
}, [state])
复制代码

可是当你的state频繁发生变化的时候,handleClickButton其实会频繁改变,这样的话你的子组件经过React.memo实现的优化就失效了。

因此当依赖常常变更时,盲目使用useCallbackuseMemo可能会致使性能不升反降。 上面咱们已经了解了,在react内部,useCallback执行会生成一个Hook结构,将函数和deps保存在这个Hook结构的memoizedState上,在每次rerender的时候,react会去比较prevDepsnextDeps,相等会返回保存的值/函数,不相等会从新执行,因此当deps频繁改变的时候,会多了一个比较deps是否改变的操做,也会浪费性能。这里有一个来自React官网的例子:

function Form() {
  const [text, updateText] = useState('');

  const handleSubmit = useCallback(() => {
    console.log(text);
  }, [text]); // 每次 text 变化时 handleSubmit 都会变

  return (
    <>
      <input value={text} onChange={(e) => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} /> // 很重的组件
    </>
  );
}
复制代码

解决这种问题React官方也给了一种方式,就是使用useRef

function Form() {
  const [text, updateText] = useState('');
  const textRef = useRef();

  useEffect(() => {
    textRef.current = text; // Write it to the ref  });

  const handleSubmit = useCallback(() => {
    const currentText = textRef.current; // Read it from the ref   
    alert(currentText);
  }, [textRef]); // Don't recreate handleSubmit like [text] would do return ( <> <input value={text} onChange={e => updateText(e.target.value)} /> <ExpensiveTree onSubmit={handleSubmit} /> </> ); } 复制代码

当使用Hooks时不要把思惟仍然局限在class Component的定式中,useRef一般给人的感受是和class Component的createRef相似的做用,但 useRef 除了在 ref 使用以外,还能够用来保存值,上面这个例子是一个很是好的例子,帮咱们更好的去使用Hooks避免一些性能的浪费。

推荐阅读

reactjs.org/docs/hooks-…

reactjs.org/docs/hooks-…

zhuanlan.zhihu.com/p/142735113

juejin.im/post/5c9827…

medium.com/@dan_abramo…

dev.to/tylermcginn…

欢迎关注个人我的技术公众号,不按期分享各类前端技术~

相关文章
相关标签/搜索