React传-3

659 阅读9分钟

原文地址

本节是Hook专题,将从 preact 借鉴 Hook 的底层原理实现,虽然实际上 preact 与 react 的 实现有所差异,但是胜在简单,了解了解思路逻辑也是可以的嘛。

Hooks

目前react内置了13种hooks

import {
  useCallback, // ---- 缓存函数
  useMemo,    // ---- 缓存函数
  useContext,  // ---- 上下文共享状态 hook
  useEffect,   // ---- 副作用
  useLayoutEffect, // ---- 副作用(阻塞)
  useImperativeHandle,// ---- 暴露子组件命令句柄
  useDebugValue, // ---- 调试hooks
  useReducer, // ---- action hook
  useRef,     // ---- ref引用
  useState,   // ---- state Hook
  useResponder,
  useTransition,
  useDeferredValue,
} from './ReactHooks';
import {withSuspenseConfig} from './ReactBatchConfig';

if (exposeConcurrentModeAPIs /* false */) {
  React.useTransition = useTransition;
  React.useDeferredValue = useDeferredValue;
  React.SuspenseList = REACT_SUSPENSE_LIST_TYPE;
  React.unstable_withSuspenseConfig = withSuspenseConfig;
}

if (enableFlareAPI/* false */) {
  React.unstable_useResponder = useResponder;
  React.unstable_createResponder = createResponder;
}

最后3个 Hook 尚处于 unstable ,需要等到支持conCurrentMode,这里就不去赘述。

export function useState<S>(initialState: (() => S) | S) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

export function useEffect(
  create: () => (() => void) | void,
  inputs: Array<mixed> | void | null,
) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, inputs);
}
export function useRef<T>(initialValue: T): {current: T} {
  const dispatcher = resolveDispatcher();
  return dispatcher.useRef(initialValue);
}

从最常用的useState、useEffect、useRef源码,可以看到几乎都和 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;
}

都知道 Hooks 的三条铁则,这些方法只会在拿到节点实例的时候触发执行,为了适配多平台ReactCurrentDispatcher 实际上需要等到 react-dom 渲染的时候才能拿到。

/**
 * Keeps track of the current dispatcher.
 */
const ReactCurrentDispatcher = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: (null: null | Dispatcher),
};

光看这些得不到什么比较有效的信息,但本质上是将节点实例返回后调用该节点实例上的对应方法。

原理探究

function Foo() {
  const [str, setStr] = useState('');
  const change = useCallback((e)=>{
    setStr(e.target.value)
  },[])
  
  useEffect(()=>{
    console.log('effect')
    return () => {
      console.log('effect clean')
    };
  },[Math.random()])

  useLayoutEffect(() => {
    console.log('layoutEffect')
    return () => {
      console.log('layoutEffect clean')
    };
  }, [str])

  return (
    <input value={str} onChange={change} />
  )
}

一个简单的Hook组件,可能会有个疑问,Hooks 是针对 Function Component 设计的Api,从而赋予 Function Component 拥有与类组件同样保存状态的能力。为什么不会被实例化还能够拥有状态,是怎么做到的?

其实Hook都依赖了闭包,而hook之间依靠单向链表的方式串联,从而拥有了“状态”,这也是之所以为什么Hooks必须在函数作用域的最顶层声明且不能嵌套在块级作用域内,如果在某个循环或者是表达式内跳过执行,那么上一次的Hook“链表”和本次update的链表某个指针指向错误,将会得到意料之外的结果。

可以借鉴下preact的实现,与React不同,preact使用的是下标索引。

// 初始化时,只有一个catchError属性
import { options } from 'preact';

let currentIndex; // 当前hook索引
let currentComponent; // 当前组件
let afterPaintEffects = []; 

// 保存旧方法,初始为 undefined
let oldBeforeRender = options._render;
let oldAfterDiff = options.diffed;
let oldCommit = options._commit;
let oldBeforeUnmount = options.unmount;

/**
 * currentComponent get hook state
 * @param {number} index The index of the hook to get
 * @returns {import('./internal').HookState}
 */
function getHookState(index) {
    if (options._hook) options._hook(currentComponent);

    const hooks = currentComponent.__hooks ||  (currentComponent.__hooks = {
        _list: [], // 放置effect的状态
        _pendingEffects: [], // 渲染下一帧后要调用的effect队列
      });
    // 新建effect
    if (index >= hooks._list.length) {
      hooks._list.push({});
    }
    return hooks._list[index];
}

通过 getHookState 收集管理Effect,即便没有实例化其本质上是函数每次都会重新执行。通过比较依赖值结果来决定逻辑更新,从这点上看getHookState是一个组件的核心管理器。需要注意的是 _pendingEffect 放入的是不阻塞页面渲染的 effect 操作,也就是useEffect。

export interface ComponentHooks {
  _list: HookState[];
  _pendingEffects: EffectHookState[];
}

export interface Component extends PreactComponent<any, any> {
  __hooks?: ComponentHooks;
}

Hook组件与类组件差不多,只不过多了一个__hooks属性 —— hooks管理器。

useState 与 useReducer

匆匆一瞥:

/**
 * @param {import('./index'). StateUpdater<any>} initialState
 */
export function useState(initialState) {
  return useReducer(invokeOrReturn, initialState);
}
/**
 * @param {import('./index').Reducer<any, any>} reducer
 * @param {import('./index').StateUpdater<any>} initialState
 * @param {(initialState: any) => void} [init]
 * @returns {[ any, (state: any) => void ]}
 */
export function useReducer(reducer, initialState, init) {
  /** @type {import('./internal').ReducerHookState} */
  const hookState = getHookState(currentIndex++);
  if (!hookState._component) {
    hookState._component = currentComponent;

    hookState._value = [
      !init ? invokeOrReturn(undefined, initialState) : init(initialState),

      action => {
        const nextValue = reducer(hookState._value[0], action);
        if (hookState._value[0] !== nextValue) {
          hookState._value[0] = nextValue;
          hookState._component.setState({});
        }
      }
    ];
  }

  return hookState._value;
}

在 preact 里 useState 与useReducer是一码事。也可以使用useState定义useReducer。

function useMyReducer(reducer, initialState, init) {
  const compatible = init ? init(initialState) : initialState;
  const [state, setState] = useState(compatible);
  function dispatch(action) {
    const nextState = reducer(state, action);
    setState(nextState);
  }

  return [state, dispatch];
}

在Foo组件effect收集阶段,useState调用useReducer传入加工函数invokeOrReturn作为reducer传入。

function invokeOrReturn(arg, f) {
  return typeof f === 'function' ? f(arg) : f;
}

通过getHookState在当前组件申明一个新的hooks,放入currentComponent.__hooks._list然后将其返回。hookState暂时只是个空对象,当它没有关联组件时需要对其进行当前组件的关联。

export function useReducer(reducer, initialState, init) {
  const hookState = getHookState(currentIndex++);// 创建hook
  if (!hookState._component) {
    hookState._component = currentComponent; // 关联到当前组件

    hookState._value = [
      !init ? invokeOrReturn(undefined, initialState) : init(initialState),// 初始值

      action => {
        // action 即setStr更新器的参数
        const nextValue = reducer(hookState._value[0], action);
        if (hookState._value[0] !== nextValue) {
          hookState._value[0] = nextValue;
          hookState._component.setState({});// 再通过类组件的setState去通知更新
        }
      }
    ];
  }

  return hookState._value;
}
// ./internal.ts
export interface ReducerHookState {
  _value?: any; // 值与更新器
  _component?: Component; // 关联组件
}

hookState._value 返回的即是平常所用的 const [str, setStr] = useState('');,值与更新器。 hookState._component 就是一个简单的无状态组件,但是React底层仍然是通过调用setState触发enqueueRender进行diff更新。

这些后面再写...因为确实很难简短描述。

useEffect 与 useLayoutEffect

/**
 * @param {any[]} oldArgs
 * @param {any[]} newArgs
 */
function argsChanged(oldArgs, newArgs) { // 比对新旧依赖
  return !oldArgs || newArgs.some((arg, index) => arg !== oldArgs[index]);
}

/**
 * @param {import('./internal').Effect} callback
 * @param {any[]} args 依赖
 */
export function useEffect(callback, args) {
  /** @type {import('./internal').EffectHookState} */
  const state = getHookState(currentIndex++);
  if (argsChanged(state._args, args)) { // 比对依赖决定是否执行
    state._value = callback;
    state._args = args;
    // 推入 effect 队列
    currentComponent.__hooks._pendingEffects.push(state);
  }
}

/**
 * @param {import('./internal').Effect} callback
 * @param {any[]} args
 */
export function useLayoutEffect(callback, args) {
  /** @type {import('./internal').EffectHookState} */
  const state = getHookState(currentIndex++);
  if (argsChanged(state._args, args)) {
    state._value = callback;
    state._args = args;
    // 推入组件render回调队列
    currentComponent._renderCallbacks.push(state);
  }
}

// ./internal.ts
export interface EffectHookState {
  _value?: Effect; // 回调函数
  _args?: any[]; // 依赖项
  _cleanup?: Cleanup; // 清理函数
}

useEffect 与 useLayoutEffect 唯一不同的是在于推入的队列以及执行的时机,前面讲到过,__hooks._pendingEffects 队列执行的时机是下一帧绘制前执行(本次render后,下次render前),不阻塞本次的浏览器渲染。而 _renderCallbacks 则在组件commit钩子内执行

组件render的流程是怎样的?还有是怎么进行比对和派发更新的。

在 Function Component中 除去 vnode 阶段外,组件自身有四个钩子阶段,也就是 render=>diffed=>commit=>unmount

options._render = vnode => {
  if (oldBeforeRender) oldBeforeRender(vnode);

  currentComponent = vnode._component; // 当前组件
  currentIndex = 0;

  if (currentComponent.__hooks) {
    // 先执行清理函数
    currentComponent.__hooks._pendingEffects.forEach(invokeCleanup);
    // 清空上次渲染未处理的Effect(useEffect)
    currentComponent.__hooks._pendingEffects.forEach(invokeEffect);
    currentComponent.__hooks._pendingEffects = [];
  }
};

options.diffed = vnode => {
  if (oldAfterDiff) oldAfterDiff(vnode);

  const c = vnode._component;
  if (!c) return;

  const hooks = c.__hooks;
  if (hooks) {
    // vnode 的 diff 完成之后,将当前的_pendingEffects推进执行队列
    if (hooks._pendingEffects.length) {
       // afterPaint 本次帧绘完——下一帧开始前执行
      afterPaint(afterPaintEffects.push(c));
    }
  }
};

options._commit = (vnode, commitQueue) => {
  commitQueue.some(component => {
    // 执行阻塞渲染任务内的清理函数
    component._renderCallbacks.forEach(invokeCleanup);
    // 更新清理函数
    component._renderCallbacks = component._renderCallbacks.filter(cb =>
      cb._value ? invokeEffect(cb) : true
    );
  });

  if (oldCommit) oldCommit(vnode, commitQueue);
};

options.unmount = vnode => {
  if (oldBeforeUnmount) oldBeforeUnmount(vnode);

  const c = vnode._component;
  if (!c) return;

  const hooks = c.__hooks;
  if (hooks) {
    // 组件卸载直接执行清理函数
    hooks._list.forEach(hook => hook._cleanup && hook._cleanup());
  }
};

/**
 * @param {import('./internal').EffectHookState} hook
 */
function invokeCleanup(hook) { // 执行清理函数
    if (hook._cleanup) hook._cleanup();
}
/**
 * Invoke a Hook's effect
 * @param {import('./internal').EffectHookState} hook
 */
function invokeEffect(hook) { // 执行回调函数
    const result = hook._value();
    if (typeof result === 'function') hook._cleanup = result;
}

最后有两个函数,invokeCleanupinvokeEffect 用来执行清理函数和回调函数.

前面三个钩子在render函数内被同步调用。

export function render(vnode, parentDom, replaceNode) {
  if (options._root) options._root(vnode, parentDom);

  let isHydrating = replaceNode === IS_HYDRATE;
  let oldVNode = isHydrating  ? null 
    : (replaceNode && replaceNode._children) || parentDom._children;
  vnode = createElement(Fragment, null, [vnode]); // 创建新的vnode

  let commitQueue = [];
  diff(
    parentDom, // 父节点
    ((isHydrating ? parentDom : replaceNode || parentDom)._children = vnode), // newVnode
    oldVNode || EMPTY_OBJ, // oldVNode ,初始化渲染时为空对象
    EMPTY_OBJ, // 上下文对象
    parentDom.ownerSVGElement !== undefined, // 是否为Svg节点
    replaceNode && !isHydrating // 替换的同级节点
      ? [replaceNode]
      : oldVNode
      ? null
      : EMPTY_ARR.slice.call(parentDom.childNodes),
    commitQueue, // 有阻塞渲染任务的effect组件列表——useLayoutEffect
    replaceNode || EMPTY_OBJ, // 替换的节点
    isHydrating // 是否节点复用,服务端渲染使用
  );
  commitRoot(commitQueue, vnode);
}

具体的功能不用涉及,首先进行diff,diff负责执行生命周期类方法以及调用_renderdiffed 方法。

  • _render 负责将 currentComponent 指向 vnode._component 并执行 _pendingEffects 队列。
  • diffed 执行 afterPaint(afterPaintEffects.push(c)) 会把带有 _pendingEffects 推入 afterPaintEffects 队列,然后 afterPaint 调用 afterNextFrame(flushAfterPaintEffects) 执行effect 保证其在下一帧前调用.
function afterPaint(newQueueLength) {
  // diffed在每次render内只执行一次
  if (newQueueLength === 1 || prevRaf !== options.requestAnimationFrame) {
    prevRaf = options.requestAnimationFrame;

    /* istanbul ignore next */
    (prevRaf || afterNextFrame)(flushAfterPaintEffects);
  }
}
/**
 * 当raf运行在后台标签页或者隐藏的<iframe> 里时,会被暂停调用以提升性能和电池寿命。
 * 当前帧的raf并不会结束,所以需要结合setTimeout以确保即使raf没有触发也会调用回调
 * @param {() => void} callback
 */
function afterNextFrame(callback) {
  const done = () => {
    clearTimeout(timeout);
    cancelAnimationFrame(raf);
    setTimeout(callback);
  };
  const timeout = setTimeout(done, RAF_TIMEOUT);

  let raf;
  if (typeof window !== 'undefined') {
    raf = requestAnimationFrame(done);
  }
}
function flushAfterPaintEffects() {
  afterPaintEffects.some(component => {
    if (component._parentDom) { // 如果节点还在html内
      // 执行清理函数
      component.__hooks._pendingEffects.forEach(invokeCleanup);
      // 执行effects
      component.__hooks._pendingEffects.forEach(invokeEffect);
      component.__hooks._pendingEffects = [];
    }
  });
  afterPaintEffects = [];
}

得到diff后的vnode之后,还不能进行渲染。

/**
 * @param {Array<import('../internal').Component>} commitQueue 含有layoutEffect阻塞渲染任务组件列表
 * @param {import('../internal').VNode} root vnode
 */
export function commitRoot(commitQueue, root) {
  if (options._commit) options._commit(root, commitQueue);

  commitQueue.some(c => {
    try {
      // 清空执行任务
      commitQueue = c._renderCallbacks;
      c._renderCallbacks = [];
      commitQueue.some(cb => {
        cb.call(c);
      });
    } catch (e) {
      options._catchError(e, c._vnode);
    }
  });
}

最后一个阶段在diffChildren 删除vnode之前执行.

useImperativeHandle

在官方例子里,useImperativeHandle用于获取子组件实例方法.因为自定义组件会过滤ref所以通常要与 forwardRef 组合搭配.

const FancyInput = forwardRef(
  (props, ref) => {
    const inputRef = useRef();
    useImperativeHandle(ref, () => ({
      focus: () => {
        inputRef.current.focus();
      }
    }));
    return <input ref={inputRef} ... />;
  }
)

function App(){
  const childrenRef = useRef()

  return (
    <div>
      <FancyInput ref={childrenRef}/>
      <button onClick={() => childrenRef.focus()}>click</button>
    </div>
  )
}

其原理是获取到父组件的ref后将实例方法对象传入.

/**
 * @param {object} ref
 * @param {() => object} createHandle
 * @param {any[]} args
 */
export function useImperativeHandle(ref, createHandle, args) {
  useLayoutEffect(
    () => {
      //兼容旧版本createRef
      if (typeof ref === 'function') ref(createHandle());
      else if (ref) ref.current = createHandle();
    },
    args == null ? args : args.concat(ref) // 依赖值
  );
}

useMemo 与 useCallback

useCallback 是 useMemo的函数版本,其原理实现相同.通过比较依赖的变化返回新值.

/**
 * @param {() => any} callback
 * @param {any[]} args
 */
export function useMemo(callback, args) {
  /** @type {import('./internal').MemoHookState} */
  const state = getHookState(currentIndex++);
  if (argsChanged(state._args, args)) { // 比对依赖是否重新创建
    state._args = args;
    state._callback = callback;
    return (state._value = callback());
  }

  return state._value;
}

/**
 * @param {() => void} callback
 * @param {any[]} args
 */
export function useCallback(callback, args) {
  return useMemo(() => callback, args);
}

useRef

useRef也是对于useMemo的变种.

export function useRef(initialValue) {
  return useMemo(() => ({ current: initialValue }), []);
}

createContext

// src/create-context.js
export function createContext(defaultValue) {
  const ctx = {};

  const context = {
    _id: '__cC' + i++,
    _defaultValue: defaultValue,
    Consumer(props, context) {
      return props.children(context);
    },
    Provider(props) {
      if (!this.getChildContext) {
        const subs = [];
        this.getChildContext = () => {
          ctx[context._id] = this;
          return ctx;
        };
        this.shouldComponentUpdate = _props => {
          if (props.value !== _props.value) {
            subs.some(c => {
              c.context = _props.value;
              // provide值变化时更新订阅的组件
              enqueueRender(c);
            });
          }
        };
        this.sub = c => {
          subs.push(c);
          let old = c.componentWillUnmount;
          c.componentWillUnmount = () => { // 组件卸载时从订阅中移除
            subs.splice(subs.indexOf(c), 1);
            old && old.call(c);
          };
        };
      }
      return props.children;
    }
  };

  context.Consumer.contextType = context;

  return context;
}

// src/hooks/index
/**
 * @param {import('./internal').PreactContext} context
 */
export function useContext(context) {
  const provider = currentComponent.context[context._id];
  if (!provider) return context._defaultValue; // 没有找到Provide组件
  const state = getHookState(currentIndex++);
  // This is probably not safe to convert to "!"
  if (state._value == null) {
    state._value = true;
    provider.sub(currentComponent); // 订阅组件
  }
  return provider.props.value;
}

通过订阅收发的模式生产和消费数据.

后话

本文的目的是研究Hooks原理与机制,实际上 preact 与 react 其实有很多地方不一样,其底层如children和ref的处理机制受限;children只能是数组,react则可以是任何数据;ref的获取时机;事件系统直接绑定在元素上而非基于冒泡;由于体积较小diff算法过于简单;setState的时机被推迟;生态问题...

不过作为一个只有3kb的库,确实不能对其要求太高.