alt=poster

本节是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的库,确实不能对其要求太高.