preact hook源码逐行解析
flytam Lv4

本文通过对preacthook源码分析,理解和掌握react/preacthook用法以及一些常见的问题。虽然reactpreact的实现上有一定的差异,但是对于hook的表现来说,是基本一致的。对于 preacthook`分析,我们很容易旧记住 hook 的使用和防止踩一些误区

preact hook 作为一个单独的包preact/hook引入的,它的总代码包含注释区区 300 行。

在阅读本文之前,先带着几个问题阅读:

1、函数组件是无状态的,那么为什么 hook 让它变成了有状态呢?

2、为什么 hook 不能放在 条件语句里面

3、为什么不能在普通函数执行 hook

基础

前面提到,hookpreact中是通过preact/hook内一个模块单独引入的。这个模块中有两个重要的模块内的全局变量:1、currentIndex:用于记录当前函数组件正在使用的 hook 的顺序(下面会提到)。2、currentComponent。用于记录当前渲染对应的组件。

preact hook 的实现对于原有的 preact 是几乎零入侵。它通过暴露在preact.options中的几个钩子函数在preact的相应初始/更新时候执行相应的hook逻辑。这几个钩子分别是_render=> 组件的render方法=>diffed=>_commit=>umount

  • \_render位置。执行组件的 render 方法之前执行,用于执行_pendingEffects_pendingEffects是不阻塞页面渲染的 effect 操作,在下一帧绘制前执行)的清理操作和执行未执行的。这个钩子还有一个很重要的作用就是让 hook 拿到当前正在执行的render的组件实例
    options
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
options._render = vnode => {
// render 钩子函数
if (oldBeforeRender) oldBeforeRender(vnode);

currentComponent = vnode._component;
currentIndex = 0;

if (currentComponent.__hooks) {
// 执行清理操作
currentComponent.__hooks._pendingEffects.forEach(invokeCleanup);
// 执行effect
currentComponent.__hooks._pendingEffects.forEach(invokeEffect);
currentComponent.__hooks._pendingEffects = [];
}
};

结合_render在 preact 的执行时机,可以知道,在这个钩子函数里是进行每次 render 的初始化操作。包括执行/清理上次未处理完的 effect、初始化 hook 下标为 0、取得当前 render 的组件实例。

  • diffed位置。 vnode 的 diff 完成之后,将当前的_pendingEffects推进执行队列,让它在下一帧绘制前执行,不阻塞本次的浏览器渲染。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
options.diffed = vnode => {
if (oldAfterDiff) oldAfterDiff(vnode);

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

const hooks = c.__hooks;
if (hooks) {
// 下面会提到useEffect就是进入_pendingEffects队列
if (hooks._pendingEffects.length) {
// afterPaint 表示本次帧绘制完,下一帧开始前执行
afterPaint(afterPaintEffects.push(c));
}
}
};
  • \_commit位置。初始或者更新 render 结束之后执行_renderCallbacks,在这个\_commit中只执行 hook 的回调,如useLayoutEffect。(_renderCallbacks是指在preact中指每次 render 后,同步执行的操作回调列表,例如setState的第二个参数 cb、或者一些render后的生命周期函数、或者forceUpdate的回调)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
options._commit = (vnode, commitQueue) => {
if (options._commit) options._commit(root, commitQueue);
commitQueue.some(component => {
// 执行上次的_renderCallbacks的清理函数
component._renderCallbacks.forEach(invokeCleanup);
// _renderCallbacks有可能是setState的第二个参数这种的、或者生命周期、或者forceUpdate的回调。
// 通过_value判断是hook的回调则在此出执行
component._renderCallbacks = component._renderCallbacks.filter(cb =>
cb._value ? invokeEffect(cb) : true
);
});

if (oldCommit) oldCommit(vnode, commitQueue);
};
  • unmount。 组件的卸载之后执行effect的清理操作
1
2
3
4
5
6
7
8
9
10
11
12
options.unmount = vnode => {
if (oldBeforeUnmount) oldBeforeUnmount(vnode);

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

const hooks = c.__hooks;
if (hooks) {
// _cleanup 是effect类hook的清理函数,也就是我们每个effect的callback 的返回值函数
hooks._list.forEach(hook => hook._cleanup && hook._cleanup());
}
};

对于组件来说加入的 hook 只是在 preact 的组件基础上增加一个__hook 属性。在 preact 的内部实现中,无论是函数组件还是 class 组件, 都是实例化成 PreactComponent,如下数据结构

1
2
3
4
5
6
7
8
export interface Component extends PreactComponent<any, any> {
__hooks?: {
// 每个组件的hook存储
_list: HookState[];
// useLayoutEffect useEffect 等
_pendingEffects: EffectHookState[];
};
}

对于问题 1 的回答,通过上面的分析,我们知道,hook最终是挂在组件的__hooks属性上的,因此,每次渲染的时候只要去读取函数组件本身的属性就能获取上次渲染的状态了,就能实现了函数组件的状态。这里关键在于getHookState这个函数。这个函数也是整个preact hook中非常重要的

1
2
3
4
5
6
7
8
9
10
11
12
function getHookState(index) {
if (options._hook) options._hook(currentComponent);
const hooks =
currentComponent.__hooks ||
(currentComponent.__hooks = { _list: [], _pendingEffects: [] });

// 初始化的时候,创建一个空的hook
if (index >= hooks._list.length) {
hooks._list.push({});
}
return hooks._list[index];
}

这个函数是在组件每次执行useXxx的时候,首先执行这一步获取 hook 的状态的(以useEffect为例子)。所有的hook都是使用这个函数先获取自身 hook 状态

1
2
3
4
5
export function useEffect(callback, args) {
//....
const state = getHookState(currentIndex++);
//.....
}

这个currentIndex在每一次的render过程中是从 0 开始的,每执行一次useXxx后加一。每个hook在多次render中对于记录前一次的执行状态正是通过currentComponent.__hooks中的顺序决定。所以如果处于条件语句,如果某一次条件不成立,导致那个useXxx没有执行,这个后面的 hook 的顺序就发生错乱并导致 bug。

例如

1
2
3
4
5
6
7
8
const Component = () => {
const [state1, setState1] = useState();
// 假设condition第一次渲染为true,第二次渲染为false
if (condition) {
const [state2, setState2] = useState();
}
const [state3, setState3] = useState();
};

第一次渲染后,__hooks = [hook1,hook2,hook3]
第二次渲染,由于const [state2, setState2] = useState();被跳过,通过currentIndex取到的const [state3, setState3] = useState();其实是hook2。就可能有问题。所以,这就是问题 2,为什么 hook 不能放到条件语句中。

经过上面一些分析,也知道问题 3 为什么 hook 不能用在普通函数了。因为 hook 都依赖了 hook 内的全局变量currentIndexcurrentComponent。而普通函数并不会执行options.render钩子重置currentIndex和设置currentComponent,当普通函数执行 hook 的时候,currentIndex为上一个执行 hook 组件的实例的下标,currentComponent为上一个执行 hook 组件的实例。因此直接就有问题了。

hook 分析

虽然 preact 中的 hook 有很多,数据结构来说只有 3 种HookState结构,所有的 hook 都是在这 3 种的基础上实现的。这 3 种分别是

  • EffectHookState (useLayoutEffect useEffect useImperativeHandle)
1
2
3
4
5
6
7
8
export interface EffectHookState {
// effect hook的回调函数
_value?: Effect;
// 依赖项
_args?: any[];
// effect hook的清理函数,_value的返回值
_cleanup?: Cleanup;
}
  • MemoHookState (**useMemo** useRef useCallback
1
2
3
4
5
6
7
8
export interface MemoHookState {
// useMemo的返回值
_value?: any;
// 前一次的依赖数组
_args?: any[];
//useMemo传入的callback
_callback?: () => any;
}
  • ReducerHookState (useReducer useState ``)
1
2
3
4
export interface ReducerHookState {
_value?: any;
_component?: Component;
}
  • useContext 这个比较特殊

MemoHookState

MemoHook是一类用来和性能优化有关的 hook

useMemo

作用:把创建函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算

1
2
3
4
5
6
// 例子
const Component = props => {
// 假设calculate是个消耗很多的计算操作
const result = calculate(props.xx);
return <div>{result}</div>;
};

默认情况下,每次Component渲染都会执行calculate的计算操作,如果calculate是一个大计算量的函数,这里会有造成性能下降,这里就可以使用useMemo来进行优化了。这样如果calculate依赖的值没有变化,就不需要执行这个函数,而是取它的缓存值。要注意的是calculate对外部依赖的值都需要传进依赖项数组,否则当部分值变化是,useMemo却还是旧的值可能会产生 bug。

1
2
3
4
5
6
// 例子
const Component = props => {
// 这样子,只会在props.xx值改变时才重新执行calculate函数,达到了优化的目的
const result = useMemo(() => calculate(props.xx), [props.xx]);
return <div>{result}</div>;
};

useMemo源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
function useMemo(callback, args) {
// state是MemoHookState类型
const state = getHookState(currentIndex++);
// 判断依赖项是否改变
if (argsChanged(state._args, args)) {
// 存储本次依赖的数据值
state._args = args;
state._callback = callback;
// 改变后执行`callback`函数返回值。
return (state._value = callback());
}
return state._value;
}

useMemo的实现逻辑不复杂,判断依赖项是否改变,改变后执行callback函数返回值。值得一提的是,依赖项比较只是普通的===比较,如果依赖的是引用类型,并且直接改变改引用类型上的属性,将不会执行callback

useCallback

作用:接收一个内联回调函数参数和一个依赖项数组(子组件依赖父组件的状态,即子组件会使用到父组件的值) ,useCallback 会返回该回调函数的 memorized 版本,该回调函数仅在某个依赖项改变时才会更新

假设有这样一段代码

1
2
3
4
5
6
// 例子
const Component = props => {
const [number, setNumber] = useState(0);
const handle = () => console.log(number);
return <button onClick={handle}>按钮</button>;
};

对于每次的渲染,都是新的 handle,因此 diff 都会失效,都会有一个创建一个新的函数,并且绑定新的事件代理的过程。当使用useCallback后则会解决这个问题

1
2
3
4
5
6
7
// 例子
const Component = props => {
const [number, setNumber] = useState(0);
// 这里,如果number不变的情况下,每次的handle是同一个值
const handle = useCallback(() => () => console.log(number), [number]);
return <button onClick={handle}>按钮</button>;
};

有一个坑点是,[number]是不能省略的,如果省略的话,每次打印的log永远是number的初始值 0

1
2
3
4
5
6
7
// 例子
const Component = props => {
const [number, setNumber] = useState(0);
// 这里永远打印0
const handle = useCallback(() => () => console.log(number), []);
return <button onClick={handle}>按钮</button>;
};

至于为什么这样,结合useMomo的实现分析。useCallback是在useMemo的基础上实现的,只是它不执行这个 callback,而是返回这个 callback,用于执行。

1
2
3
4
function useCallback(callback, args) {
// 直接返回这个callback,而不是执行
return useMemo(() => callback, args);
}

我们想象一下,每次的函数组件执行,都是一个全新的过程。而我们的 callback 只是挂在MemoHook_value字段上,当依赖没有改变的时候,我们执行的callback永远是创建的那个时刻那次渲染的形成的闭包函数。而那个时刻的number就是初次的渲染值。

1
2
3
4
5
6
7
8
9
10
11
// 例子
const Component = props => {
const [number, setNumber] = useState(0);
// 这里永远打印0
const handle = useCallback(
() => /** 到了后面的时候,我们的handle并不是执行这次的callback,而是上次的那个记录的callback*/ () =>
console.log(number),
[]
);
return <button onClick={handle}>按钮</button>;
};

useMemouseCallback对于性能优化很好用,但是并不是必须的。因为对于大多数的函数来说,一方面创建/调用消耗并不大,而记录依赖项是需要一个遍历数组的对比操作,这个也是需要消耗的。因此并不需要无脑useMemouseCallback,而是在一些刚好的地方使用才行

useRef

作用:useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initialValue)。就是在函数组件中替代React.createRef的功能或者类似于this.xxx的功能。在整个周期中,ref 值是不变的

用法一:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 例子
const Component = props => {
const [number, setNumber] = useState(0);
const inputRef = useRef(null)
const focus = useCallback(
() =>inputRef.focus(),
[]
);
return<div>
<input ref={inputRef}>
<button onClick={focus}>按钮</button>
</div>;
};

用法二:类似于this

1
2
3
4
5
6
7
8
9
10
11
12
13
// 例子
const Component = props => {
const [number, setNumber] = useState(0);
const inputRef = useRef(null)
const focus = useCallback(
() =>inputRef.current.focus(),
[]
);
return<div>
<input ref={node => inputRef.current = node}>
<button onClick={focus}>按钮</button>
</div>;
};

之所以能这么用,在diff过程中于applyRef这个函数,react也是类似。
diff中,通过applyRef将dom对象挂到对应的ref上)

1
2
3
4
5
6
7
8
export function applyRef(ref, value, vnode) {
try {
if (typeof ref == "function") ref(value);
else ref.current = value;
} catch (e) {
options._catchError(e, vnode);
}
}

查看useRef的源码。

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

可见 就是初始化的时候创建一个{current:initialValue},不依赖任何数据,需要手动赋值修改

ReducerHookState

useReducer

useReducer和使用redux非常像。

用法:

1
2
3
4
// reducer就是平时redux那种reducer函数
// initialState 初始化的state状态
// init 一个函数用于惰性计算state初始值
const [state, dispatch] = useReducer(reducer, initialState, init);

计数器的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const initialState = 0;
function reducer(state, action) {
switch (action.type) {
case "increment":
return { number: state.number + 1 };
case "decrement":
return { number: state.number - 1 };
default:
return state;
}
}
function init(initialState) {
return { number: initialState };
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState, init);
return (
<div>
{state.number}
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
</div>
);
}

对于熟悉redux的同学来说,一眼明了。后面提到的useState旧是基于useReducer实现的。

源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
export function useReducer(reducer, initialState, init) {
const hookState = getHookState(currentIndex++);
// 前面分析过ReducerHookState的数据结构,有两个属性
// _value 当前的state值
// _component 对应的组件实例

if (!hookState._component) {
// 初始化过程
// 因为后面需要用到setState更新,所以需要记录component的引用
hookState._component = currentComponent;

hookState._value = [
// init是前面提到的惰性初始化函数,传入了init则初始值是init的计算结果
// 没传init的时候是invokeOrReturn。这里就是直接返回初始化值
/***
*
* ```js
* invokeOrReturn 很精髓
* 参数f为函数,返回 f(arg)
* 参数f非函数,返回f
* function invokeOrReturn(arg, f) {
return typeof f === "function" ? f(arg) : f;
}
* ```
*/
!init ? invokeOrReturn(undefined, initialState) : init(initialState),

action => {
// reducer函数计算出下次的state的值
const nextValue = reducer(hookState._value[0], action);
if (hookState._value[0] !== nextValue) {
hookState._value[0] = nextValue;
// setState开始进行下一轮更新
hookState._component.setState({});
}
}
];
}

// 返回当前的state
return hookState._value;
}

更新state就是调用 demo 的dispatch,也就是通过reducer(preState,action)计算出下次的state赋值给_value。然后调用组件的setState方法进行组件的diff和相应更新操作(这里是preactreact不太一样的一个地方,preact 的函数组件在内部和 class 组件一样使用 component 实现的)。

useState

useState大概是 hook 中最常用的了。类似于 class 组件中的 state 状态值。

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Component = () => {
const [number, setNumber] = useState(0);
const [index, setIndex] = useIndex(0);
return (
<div>
{/* setXxx可以传入回调或者直接设置值**/}
<button onClick={() => setNumber(number => number + 1)}>
更新number
</button>
{number}
//
<button onClick={() => setIndex(index + 1)}>更新index</button>
{index}
</div>
);
};

上文已经提到过,useState是通过useReducer实现的。

1
2
3
4
5
6
7
8
9
10
11
export function useState(initialState) {
/***
*
* ```js
* function invokeOrReturn(arg, f) {
return typeof f === "function" ? f(arg) : f;
}
* ```
*/
return useReducer(invokeOrReturn, initialState);
}

只要我们给useReduecrreducer参数传invokeOrReturn函数即可实现useState。回顾下useStateuseReducer的用法

1
2
3
4
5
6
7
const [index, setIndex] = useIndex(0);
setIndex(index => index + 1);
// or
setIndex(1);
//-----
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: "some type" });

1、对于setState直接传值的情况。reducerinvokeOrReturn)函数,直接返回入参即可

1
2
// action非函数,reducer(hookState._value[0], action)结果为action
const nextValue = reducer(hookState._value[0], action);

2、对于setState直接参数的情况的情况。

1
2
// action为函数,reducer(hookState._value[0], action)结果为action(hookState._value[0])
const nextValue = reducer(hookState._value[0], action);

可见,useState其实只是传特定reduceruseReducer一种实现。

EffectHookState

  • useEffectuseLayoutEffect

这两个 hook 的用法完全一致,都是在 render 过程中执行一些副作用的操作,可来实现以往 class 组件中一些生命周期的操作。区别在于,
useEffect 的 callback 执行是在本次渲染结束之后,下次渲染之前执行。 useLayoutEffect则是在本次会在浏览器 layout 之后,painting 之前执行,是同步的。

用法。传递一个回调函数和一个依赖数组,数组的依赖参数变化时,重新执行回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 接收一个包含一些必要副作用代码的函数,这个函数需要从DOM中读取layout和同步re-render
* `useLayoutEffect` 里面的操作将在DOM变化之后,浏览器绘制之前 执行
* 尽量使用`useEffect`避免阻塞视图更新
*
* @param effect Imperative function that can return a cleanup function
* @param inputs If present, effect will only activate if the values in the list change (using ===).
*/
export function useLayoutEffect(effect: EffectCallback, inputs?: Inputs): void;
/**
* 接收一个包含一些必要副作用代码的函数。
* 副作用函数会在浏览器绘制后执行,不会阻塞渲染
*
* @param effect Imperative function that can return a cleanup function
* @param inputs If present, effect will only activate if the values in the list change (using ===).
*/
export function useEffect(effect: EffectCallback, inputs?: Inputs): void;

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function LayoutEffect() {
const [color, setColor] = useState("red");
useLayoutEffect(() => {
alert(color);
}, [color]);
useEffect(() => {
alert(color);
}, [color]);
return (
<>
<div id="myDiv" style={{ background: color }}>
颜色
</div>
<button onClick={() => setColor("red")}>红</button>
<button onClick={() => setColor("yellow")}>黄</button>
<button onClick={() => setColor("blue")}>蓝</button>
</>
);
}

从 demo 可以看出,每次改变颜色,useLayoutEffect的回调触发时机是在页面改变颜色之前,而useEffect的回调触发时机是页面改变颜色之后。它们的实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export function useLayoutEffect(callback, args) {
const state = getHookState(currentIndex++);
if (argsChanged(state._args, args)) {
state._value = callback;
state._args = args;

currentComponent._renderCallbacks.push(state);
}
}

export function useEffect(callback, args) {
const state = getHookState(currentIndex++);
if (argsChanged(state._args, args)) {
state._value = callback;
state._args = args;

currentComponent.__hooks._pendingEffects.push(state);
}
}

它们的实现几乎一模一样,唯一的区别是useLayoutEffect的回调进的是_renderCallbacks数组,而useEffect的回调进的是_pendingEffects

前面已经做过一些分析,_renderCallbacks是在\_commit钩子中执行的,在这里执行上次renderCallbackseffect的清理函数和执行本次的renderCallbacks\_commit则是在preactcommitRoot中被调用,即每次 render 后同步调用(顾名思义 renderCallback 就是 render 后的回调,此时 DOM 已经更新完,浏览器还没有 paint 新一帧,上图所示的 layout 后 paint 前)因此 demo 中我们在这里alert会阻塞浏览器的 paint,这个时候看不到颜色的变化。

_pendingEffects则是本次重绘之后,下次重绘之前执行。在 hook 中的调用关系如下

1、 options.differed 钩子中(即组件 diff 完成后),执行afterPaint(afterPaintEffects.push(c))将含有_pendingEffects的组件推进全局的afterPaintEffects队列

2、afterPaint中执行执行afterNextFrame(flushAfterPaintEffects)。在下一帧 重绘之前,执行flushAfterPaintEffects。同时,如果 100ms 内,当前帧的 requestAnimationFrame 没有结束(例如窗口不可见的情况),则直接执行flushAfterPaintEffectsflushAfterPaintEffects函数执行队列内所有组件的上一次的_pendingEffects的清理函数和执行本次的_pendingEffects

几个关键函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* 绘制之后执行回调
* 执行队列内所有组件的上一次的`_pendingEffects`的清理函数和执行本次的`_pendingEffects`。
*/
function flushAfterPaintEffects() {
afterPaintEffects.some(component => {
if (component._parentDom) {
// 清理上一次的_pendingEffects
component.__hooks._pendingEffects.forEach(invokeCleanup);
// 执行当前_pendingEffects
component.__hooks._pendingEffects.forEach(invokeEffect);
component.__hooks._pendingEffects = [];
}
});
// 清空afterPaintEffects
afterPaintEffects = [];
}

/**
*preact的diff是同步的,是宏任务。
newQueueLength === 1 保证了afterPaint内的afterNextFrame(flushAfterPaintEffects)只执行一遍。因为会调用n次宏任务的afterPaint结束后,才会执行flushAfterPaintEffects一次将所有含有pendingEffect的组件进行回调进行
* */
afterPaint = newQueueLength => {
if (newQueueLength === 1 || prevRaf !== options.requestAnimationFrame) {
prevRaf = options.requestAnimationFrame;

// 执行下一帧结束后,清空 useEffect的回调
(prevRaf || afterNextFrame)(flushAfterPaintEffects);
}
};
/**
* 希望在下一帧 重绘之前,执行callback。同时,如果100ms内,当前帧的requestAnimationFrame没有结束(例如窗口不可见的情况),则直接执行callback
*/
function afterNextFrame(callback) {
const done = () => {
clearTimeout(timeout);
cancelAnimationFrame(raf);
setTimeout(callback);
};
const timeout = setTimeout(done, RAF_TIMEOUT);
const raf = requestAnimationFrame(done);
}

useImperativeHandle

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// React文档的例子
function FancyInput(props, ref) {
const inputRef = useRef();
// 第一个参数是 父组件 ref
// 第二个参数是返回,返回的对象会作为父组件 ref current 属性的值
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

function App(){
const ref = useRef()

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

默认情况下,函数组件是没有ref属性,通过forwardRef(FancyInput)后,父组件就可以往子函数组件传递ref属性了。useImperativeHandle的作用就是控制父组件不能在拿到子组件的ref后为所欲为。如上,父组件拿到FancyInput后,只能执行focus,即子组件决定对外暴露的 ref 接口,class组件是无法做到的。

1
2
3
4
5
6
7
8
9
function useImperativeHandle(ref, createHandle, args) {
useLayoutEffect(
() => {
if (typeof ref === "function") ref(createHandle());
else if (ref) ref.current = createHandle();
},
args == null ? args : args.concat(ref)
);
}

useImperativeHandle的实现也是一目了然,因为这种是涉及到 dom 更新后的同步修改,所以是用useLayoutEffect实现的。从实现可看出,useImperativeHandle也能接收依赖项数组的

createContext

接收一个 context 对象(Preact.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。当组件上层最近的<MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。

使用 context 最大的好处就是避免了深层组件嵌套时,需要一层层往下通过 props 传值。使用 createContext 可以非常方便的使用 context 而不用再写繁琐的Consumer

react context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const context = Preact.createContext(null);

const Component = () => {
// 每当Context.Provider value={{xx:xx}}变化时,Component都会重新渲染
const { xx } = useContext(context);
return <div></div>;
};

const App = () => {
return (
<Context.Provider value={{ xx: xx }}>
<Component></Component>
</Context.Provider>
);
};

useContext实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function useContext(context) {
// 每个`preact`组件的context属性都保存着当前全局context的Provider引用,不同的context都有一个唯一id
// 获取当前组件 所属的Context Provider
const provider = currentComponent.context[context._id];
if (!provider) return context._defaultValue;
const state = getHookState(currentIndex++);
if (state._value == null) {
// 初始化的时候将当前 组件订阅 Provider的value变化
// 当Provider的value变化时,重新渲染当前组件
state._value = true;
provider.sub(currentComponent);
}
return provider.props.value;
}

可以看出,useContext会在初始化的时候,当前组件对应的Context.Provider会把该组件加入订阅回调(provider.sub(currentComponent)),当 Provider value 变化时,在 Provider 的shouldComponentUpdate周期中执行组件的 render。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//.....
// Provider部分源码
Provider(props) {
//....
// 初始化Provider的时候执行的部分
this.shouldComponentUpdate = _props => {
if (props.value !== _props.value) {
subs.some(c => {
c.context = _props.value;
// 执行sub订阅回调组件的render
enqueueRender(c);
});
}
};
this.sub = c => {
subs.push(c);
let old = c.componentWillUnmount;
c.componentWillUnmount = () => {
// 组件卸载的时候,从订阅回调组件列表中移除
subs.splice(subs.indexOf(c), 1);
old && old.call(c);
};
};
}
//....

总结: preactreact在源码实现上有一定差异,但是通过对 preact hook 源码的学习,对于理解 hook 的很多观念和思想是非常有帮助的。


最后附上带了注释的 hook 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
import { options } from "preact";

/\*_ @type {number} _/
let currentIndex;

/\*_ @type {import('./internal').Component} _/
let currentComponent;

/\*_ @type {Array<import('./internal').Component>} _/
let afterPaintEffects = [];

let oldBeforeRender = options.\_render;
options.\_render = vnode => {
// render 钩子函数
if (oldBeforeRender) oldBeforeRender(vnode);

currentComponent = vnode.\_component;
currentIndex = 0;

if (currentComponent.**hooks) {
// 执行清理操作
currentComponent.**hooks.\_pendingEffects.forEach(invokeCleanup);
// 执行 effect
currentComponent.**hooks.\_pendingEffects.forEach(invokeEffect);
currentComponent.**hooks.\_pendingEffects = [];
}
};

// \_pendingEffects paint 后调用
// \_renderCallbacks render 后同步调用
// render(执行 render 方法之前) -> diffed(diff 结束) -> \_commit(初始或者更新生命周期结束之后) -> unmount(卸载)
let oldAfterDiff = options.diffed;
options.diffed = vnode => {
if (oldAfterDiff) oldAfterDiff(vnode);

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

const hooks = c.\_\_hooks;
if (hooks) {
if (hooks.\_pendingEffects.length) {
afterPaint(afterPaintEffects.push(c));
}
}
};

let oldCommit = options.\_commit;
options.\_commit = (vnode, commitQueue) => {
commitQueue.some(component => {
component.\_renderCallbacks.forEach(invokeCleanup);
// \_renderCallbacks 有可能是 setState 的第二个参数这种的、或者生命周期、或者 forceUpdate 的回调。
// 通过\_value 判断是 hook 的回调
component.\_renderCallbacks = component.\_renderCallbacks.filter(cb =>
cb.\_value ? invokeEffect(cb) : true
);
});

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

let oldBeforeUnmount = options.unmount;
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());
}
};

/\*\*

- Get a hook's state from the currentComponent
- @param {number} index The index of the hook to get
- @returns {import('./internal').HookState}
_/
function getHookState(index) {
if (options.\_hook) options.\_hook(currentComponent);
// Largely inspired by:
// _ https://github.com/michael-klein/funcy.js/blob/f6be73468e6ec46b0ff5aa3cc4c9baf72a29025a/src/hooks/core_hooks.mjs
// _ https://github.com/michael-klein/funcy.js/blob/650beaa58c43c33a74820a3c98b3c7079cf2e333/src/renderer.mjs
// Other implementations to look at:
// _ https://codesandbox.io/s/mnox05qp8
const hooks =
currentComponent.**hooks ||
(currentComponent.**hooks = { \_list: [], \_pendingEffects: [] });

// 初始化的时候新 hook 的情况
if (index >= hooks.\_list.length) {
hooks.\_list.push({});
}
return hooks.\_list[index];
}

/\*\*

- @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;
}

/\*\*

- @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;

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;

currentComponent._renderCallbacks.push(state);

}
}

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

/\*\*

- @param {object} ref
- @param {() => object} createHandle
- @param {any[]} args
\*/
export function useImperativeHandle(ref, createHandle, args) {
useLayoutEffect(
() => {
if (typeof ref === "function") ref(createHandle());
else if (ref) ref.current = createHandle();
},
args == null ? args : args.concat(ref)
);
}

/\*\*

- @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);
}

/\*\*

- @param {import('./internal').PreactContext} context
\*/
export function useContext(context) {
const provider = currentComponent.context[context._id];
if (!provider) return context.\_defaultValue;
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;
}

/\*\*

- Display a custom label for a custom hook for the devtools panel
- @type {<T>(value: T, cb?: (value: T) => string | number) => void}
\*/
export function useDebugValue(value, formatter) {
if (options.useDebugValue) {
options.useDebugValue(formatter ? formatter(value) : value);
}
}

// Note: if someone used Component.debounce = requestAnimationFrame,
// then effects will ALWAYS run on the NEXT frame instead of the current one, incurring a ~16ms delay.
// Perhaps this is not such a big deal.
/\*\*

- Schedule afterPaintEffects flush after the browser paints
- @type {(newQueueLength: number) => void}
_/
/_ istanbul ignore next \*/
let afterPaint = () => {};

/\*\*

- 绘制之后执行回调
\*/
function flushAfterPaintEffects() {
afterPaintEffects.some(component => {
if (component.\_parentDom) {
// 清理上一次的 Effect
component.**hooks.\_pendingEffects.forEach(invokeCleanup);
// 执行当前 effect
component.**hooks.\_pendingEffects.forEach(invokeEffect);
component.\_\_hooks.\_pendingEffects = [];
}
});
afterPaintEffects = [];
}

const RAF_TIMEOUT = 100;

/\*\*

- 希望在下一帧 重绘之前,执行 callback。同时,如果 100ms 内,当前帧没有结束(例如窗口不可见的情况),则直接执行 callback
\*/
function afterNextFrame(callback) {
const done = () => {
clearTimeout(timeout);
cancelAnimationFrame(raf);
setTimeout(callback);
};
const timeout = setTimeout(done, RAF_TIMEOUT);
const raf = requestAnimationFrame(done);
}

/_ istanbul ignore else _/
if (typeof window !== "undefined") {
let prevRaf = options.requestAnimationFrame;
afterPaint = newQueueLength => {
if (newQueueLength === 1 || prevRaf !== options.requestAnimationFrame) {
prevRaf = options.requestAnimationFrame;

// 执行下一帧结束后,清空 useEffect的回调
(prevRaf || afterNextFrame)(flushAfterPaintEffects);
}

};
}

/\*\*

- 执行清理 effect 操作。
- @param {import('./internal').EffectHookState} hook
\*/
function invokeCleanup(hook) {
if (hook.\_cleanup) hook.\_cleanup();
}

/\*\*

- 执行 effect hook 的 cb,并将清理函数赋值给\_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;
}

/\*\*

- 判断两个数组是否变化
- @param {any[]} oldArgs
- @param {any[]} newArgs
\*/
function argsChanged(oldArgs, newArgs) {
return !oldArgs || newArgs.some((arg, index) => arg !== oldArgs[index]);
}

/\*\*

- 执行或者返回
\*/
function invokeOrReturn(arg, f) {
return typeof f === "function" ? f(arg) : f;
}
  • Post title:preact hook源码逐行解析
  • Post author:flytam
  • Create time:2019-11-11 13:39:34
  • Post link:https://blog.flytam.vip/preact hook源码逐行解析.html
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.