本文通过对
preact
的hook
源码分析,理解和掌握react/preact
的hook
用法以及一些常见的问题。虽然react
和preact
的实现上有一定的差异,但是对于hook
的表现来说,是基本一致的。对于 preact的
hook`分析,我们很容易旧记住 hook 的使用和防止踩一些误区
preact hook 作为一个单独的包preact/hook
引入的,它的总代码包含注释区区 300 行。
在阅读本文之前,先带着几个问题阅读:
1、函数组件是无状态的,那么为什么 hook 让它变成了有状态呢?
2、为什么 hook 不能放在 条件语句里面
3、为什么不能在普通函数执行 hook
基础
前面提到,hook
在preact
中是通过preact/hook
内一个模块单独引入的。这个模块中有两个重要的模块内的全局变量:1、currentIndex
:用于记录当前函数组件正在使用的 hook 的顺序(下面会提到)。2、currentComponent
。用于记录当前渲染对应的组件。
preact hook
的实现对于原有的 preact
是几乎零入侵。它通过暴露在preact.options
中的几个钩子函数在preact
的相应初始/更新时候执行相应的hook
逻辑。这几个钩子分别是_render
=>diffed
=>_commit
=>umount
\_render
位置。执行组件的 render 方法之前执行,用于执行_pendingEffects
(_pendingEffects
是不阻塞页面渲染的 effect 操作,在下一帧绘制前执行)的清理操作和执行未执行的。这个钩子还有一个很重要的作用就是让 hook 拿到当前正在执行的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 = [];
}
};
结合_render
在 preact 的执行时机,可以知道,在这个钩子函数里是进行每次 render 的初始化操作。包括执行/清理上次未处理完的 effect、初始化 hook 下标为 0、取得当前 render 的组件实例。
diffed
位置。 vnode 的 diff 完成之后,将当前的_pendingEffects
推进执行队列,让它在下一帧绘制前执行,不阻塞本次的浏览器渲染。
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
的回调)。
options._commit = (vnode, 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
的清理操作
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,如下数据结构
export interface Component extends PreactComponent<any, any> {
__hooks?: {
// 每个组件的hook存储
_list: HookState[];
// useLayoutEffect useEffect 等
_pendingEffects: EffectHookState[];
};
}
对于问题 1 的回答,通过上面的分析,我们知道,hook
最终是挂在组件的__hooks
属性上的,因此,每次渲染的时候只要去读取函数组件本身的属性就能获取上次渲染的状态了,就能实现了函数组件的状态。这里关键在于getHookState
这个函数。这个函数也是整个preact
hook
中非常重要的
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 状态
export function useEffect(callback, args) {
//....
const state = getHookState(currentIndex++);
//.....
}
这个currentIndex
在每一次的render
过程中是从 0 开始的,每执行一次useXxx
后加一。每个hook
在多次render
中对于记录前一次的执行状态正是通过currentComponent.__hooks
中的顺序决定。所以如果处于条件语句,如果某一次条件不成立,导致那个useXxx
没有执行,这个后面的 hook 的顺序就发生错乱并导致 bug。
例如
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 内的全局变量currentIndex
和currentComponent
。而普通函数并不会执行options.render
钩子重置currentIndex
和设置currentComponent
,当普通函数执行 hook 的时候,currentIndex
为上一个执行 hook 组件的实例的下标,currentComponent
为上一个执行 hook 组件的实例。因此直接就有问题了。
hook 分析
虽然 preact 中的 hook 有很多,数据结构来说只有 3 种HookState
结构,所有的 hook 都是在这 3 种的基础上实现的。这 3 种分别是
EffectHookState
(useLayoutEffect
useEffect
useImperativeHandle
)
export interface EffectHookState {
// effect hook的回调函数
_value?: Effect;
// 依赖项
_args?: any[];
// effect hook的清理函数,_value的返回值
_cleanup?: Cleanup;
}
MemoHookState
(useMemo
useRef
useCallback
)
export interface MemoHookState {
// useMemo的返回值
_value?: any;
// 前一次的依赖数组
_args?: any[];
//useMemo传入的callback
_callback?: () => any;
}
ReducerHookState
(useReducer
useState
``)
export interface ReducerHookState {
_value?: any;
_component?: Component;
}
useContext
这个比较特殊
MemoHookState
MemoHook
是一类用来和性能优化有关的 hook
useMemo
作用:把创建函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算
// 例子
const Component = props => {
// 假设calculate是个消耗很多的计算操作
const result = calculate(props.xx);
return <div>{result}</div>;
};
默认情况下,每次Component
渲染都会执行calculate
的计算操作,如果calculate
是一个大计算量的函数,这里会有造成性能下降,这里就可以使用useMemo
来进行优化了。这样如果calculate
依赖的值没有变化,就不需要执行这个函数,而是取它的缓存值。要注意的是calculate
对外部依赖的值都需要传进依赖项数组,否则当部分值变化是,useMemo
却还是旧的值可能会产生 bug。
// 例子
const Component = props => {
// 这样子,只会在props.xx值改变时才重新执行calculate函数,达到了优化的目的
const result = useMemo(() => calculate(props.xx), [props.xx]);
return <div>{result}</div>;
};
useMemo
源码分析
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 版本,该回调函数仅在某个依赖项改变时才会更新
假设有这样一段代码
// 例子
const Component = props => {
const [number, setNumber] = useState(0);
const handle = () => console.log(number);
return <button onClick={handle}>按钮</button>;
};
对于每次的渲染,都是新的 handle,因此 diff 都会失效,都会有一个创建一个新的函数,并且绑定新的事件代理的过程。当使用useCallback
后则会解决这个问题
// 例子
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
// 例子
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,用于执行。
function useCallback(callback, args) {
// 直接返回这个callback,而不是执行
return useMemo(() => callback, args);
}
我们想象一下,每次的函数组件执行,都是一个全新的过程。而我们的 callback 只是挂在MemoHook
的_value
字段上,当依赖没有改变的时候,我们执行的callback
永远是创建的那个时刻那次渲染的形成的闭包函数。而那个时刻的number
就是初次的渲染值。
// 例子
const Component = props => {
const [number, setNumber] = useState(0);
// 这里永远打印0
const handle = useCallback(
() => /** 到了后面的时候,我们的handle并不是执行这次的callback,而是上次的那个记录的callback*/ () =>
console.log(number),
[]
);
return <button onClick={handle}>按钮</button>;
};
useMemo
和useCallback
对于性能优化很好用,但是并不是必须的。因为对于大多数的函数来说,一方面创建/调用消耗并不大,而记录依赖项是需要一个遍历数组的对比操作,这个也是需要消耗的。因此并不需要无脑useMemo
和useCallback
,而是在一些刚好的地方使用才行
useRef
作用:useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initialValue)。就是在函数组件中替代React.createRef
的功能或者类似于this.xxx
的功能。在整个周期中,ref 值是不变的
用法一:
// 例子
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
// 例子
const Component = props => {
const [number, setNumber] = useState(0);
const inputRef = useRef(null)
const focus = useCallback(
() =>inputRef.focus(),
[]
);
return<div>
<input ref={node => inputRef.current = node}>
<button onClick={focus}>按钮</button>
</div>;
};
之所以能这么用,在于applyRef
这个函数,react
也是类似。
export function applyRef(ref, value, vnode) {
try {
if (typeof ref == "function") ref(value);
else ref.current = value;
} catch (e) {
options._catchError(e, vnode);
}
}
查看useRef
的源码。
function useRef(initialValue) {
return useMemo(() => ({ current: initialValue }), []);
}
可见 就是初始化的时候创建一个{current:initialValue}
,不依赖任何数据,需要手动赋值修改
ReducerHookState
useReducer
useReducer
和使用redux
非常像。
用法:
// reducer就是平时redux那种reducer函数
// initialState 初始化的state状态
// init 一个函数用于惰性计算state初始值
const [state, dispatch] = useReducer(reducer, initialState, init);
计数器的例子。
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
实现的。
源码分析
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
和相应更新操作(这里是preact
和react
不太一样的一个地方,preact 的函数组件在内部和 class 组件一样使用 component 实现的)。
useState
useState
大概是 hook 中最常用的了。类似于 class 组件中的 state 状态值。
用法
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
实现的。
export function useState(initialState) {
/***
*
* ```js
* function invokeOrReturn(arg, f) {
return typeof f === "function" ? f(arg) : f;
}
* ```
*/
return useReducer(invokeOrReturn, initialState);
}
只要我们给useReduecr
的reducer
参数传invokeOrReturn
函数即可实现useState
。回顾下useState
和useReducer
的用法
const [index, setIndex] = useIndex(0);
setIndex(index => index + 1);
// or
setIndex(1);
//-----
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: "some type" });
1、对于setState
直接传值的情况。reducer
(invokeOrReturn
)函数,直接返回入参即可
// action非函数,reducer(hookState._value[0], action)结果为action
const nextValue = reducer(hookState._value[0], action);
2、对于setState
直接参数的情况的情况。
// action为函数,reducer(hookState._value[0], action)结果为action(hookState._value[0])
const nextValue = reducer(hookState._value[0], action);
可见,useState
其实只是传特定reducer
的useReducer
一种实现。
EffectHookState
useEffect
和useLayoutEffect
这两个 hook 的用法完全一致,都是在 render 过程中执行一些副作用的操作,可来实现以往 class 组件中一些生命周期的操作。区别在于,
useEffect
的 callback 执行是在本次渲染结束之后,下次渲染之前执行。 useLayoutEffect
则是在本次会在浏览器 layout 之后,painting 之前执行,是同步的。
用法。传递一个回调函数和一个依赖数组,数组的依赖参数变化时,重新执行回调。
/**
* 接收一个包含一些必要副作用代码的函数,这个函数需要从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;
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
的回调触发时机是页面改变颜色之后。它们的实现如下
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
钩子中执行的,在这里执行上次renderCallbacks
的effect
的清理函数和执行本次的renderCallbacks
。\_commit
则是在preact
的commitRoot
中被调用,即每次 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 没有结束(例如窗口不可见的情况),则直接执行flushAfterPaintEffects
。flushAfterPaintEffects
函数执行队列内所有组件的上一次的_pendingEffects
的清理函数和执行本次的_pendingEffects
。
几个关键函数
/**
* 绘制之后执行回调
* 执行队列内所有组件的上一次的`_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 一起
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 接口。
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
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
实现
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。
//.....
// 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);
};
};
}
//....
总结: preact
和react
在源码实现上有一定差异,但是通过对 preact hook 源码的学习,对于理解 hook 的很多观念和思想是非常有帮助的。
最后附上带了注释的 hook 源码
展开查看
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: // _ github.com/michael-kle… // _ github.com/michael-kle… // Other implementations to look at: // _ 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} initialState */ export function useState(initialState) { return useReducer(invokeOrReturn, initialState); }
/**
@param {import('./index').Reducer<any, any>} reducer
@param {import('./index').StateUpdater} 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 {(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; }