本文通过对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=>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);// 执行effectcurrentComponent.__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: [] });// 初始化的时候,创建一个空的hookif (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,第二次渲染为falseif (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)
export interface EffectHookState {// effect hook的回调函数_value?: Effect;// 依赖项_args?: any[];// effect hook的清理函数,_value的返回值_cleanup?: Cleanup;
}
  • MemoHookStateuseMemo 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);// 这里永远打印0const 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);// 这里永远打印0const 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 值是不变的

用法一:

// 例子
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({});}}];}// 返回当前的statereturn hookState._value;
}

更新state就是调用 demo 的dispatch,也就是通过reducer(preState,action)计算出下次的state赋值给_value。然后调用组件的setState方法进行组件的diff和相应更新操作(这里是preactreact不太一样的一个地方,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);
}

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

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)函数,直接返回入参即可

// 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其实只是传特定reduceruseReducer一种实现。

EffectHookState

  • useEffectuseLayoutEffect

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WpOh4RSs-1573477097780)(…/img/1.png)]

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

/*** 接收一个包含一些必要副作用代码的函数,这个函数需要从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

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钩子中执行的,在这里执行上次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

几个关键函数

/*** 绘制之后执行回调* 执行队列内所有组件的上一次的`_pendingEffects`的清理函数和执行本次的`_pendingEffects`。*/
function flushAfterPaintEffects() {afterPaintEffects.some(component => {if (component._parentDom) {// 清理上一次的_pendingEffectscomponent.__hooks._pendingEffects.forEach(invokeCleanup);// 执行当前_pendingEffectscomponent.__hooks._pendingEffects.forEach(invokeEffect);component.__hooks._pendingEffects = [];}});// 清空afterPaintEffectsafterPaintEffects = [];
}/***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 Providerconst 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订阅回调组件的renderenqueueRender(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 的很多观念和思想是非常有帮助的。

超详细preact hook源码逐行解析相关推荐

  1. 超详细!ArrayList源码图文解析

    不诗意的女程序媛不是好厨师~ 转载请注明出处,From李诗雨-[https://blog.csdn.net/cjm2484836553/article/details/104329665] <超 ...

  2. 用Android Studio做一个超好玩的拼图游戏,附送超详细注释的源码

    文章目录 一.项目概述 二.开发环境 三.需求分析 四.实现过程 1.拼图游戏布局绘制 2.拼图游戏时间计时 3.拼图游戏打乱显示 4.拼图游戏碎片位置切换 5.拼图游戏成功的条件 6.拼图游戏重新开 ...

  3. Yolov5(1):Detect源码逐行解析

    开学时,给自己定的学习任务,直到今天才有闲空来完成.一方面是yolo代码初看觉得乱糟糟的,不想读:其次,yolo算法对于初触深度学习的我而言,还是有较大的难度. 今天学习成果就是弄懂了,yolov5的 ...

  4. vue - element <upload> 组件批量上传文档,可携带其他表单数据项一同与文件 “手动提交“ 服务器(类似百度文库系统批量上传前端界面与逻辑)超详细教程示例源码,提供界面与逻辑完整源码

    效果图 本示例使用的是 element 组件库,其实什么组件库都行(逻辑是一样),只要你是 vue.js 项目就能使用本教程. 本文实现了 vue + element 使用 upload 组件批量上传 ...

  5. Windows10超详细esmini的源码安装与测试运行——OpenScenario播放器

    esmini安装与调试--简易openscenario播放器 1. 下载源码 2. 源码编译及错误 3. 错误解决 esmini可以方便的查看openscenario的xosc文件,目前已经支持到op ...

  6. 超详细!附源码!SpringBoot+shiro+mybatis+Thymeleaf实现权限登录系统

    最近在做一个期末作品,就是使用ssm+thymeleaf+vue+shiro完成一个具有权限登录,且能实现用户信息增删查改的这么一个项目,下面仅仅是实现权限认证和登录.为什么我选shiro,而不选sp ...

  7. 微信小程序UI自动化实践:python+minium+PO模式(超详细教程附源码供下载)

    文章目录 前言 一.minium介绍 二.安装环境 1. 安装minium doc 2. 安装minium 3. 启动小程序 三.准备知识 1. 启动 2. 配置 3. 命令行运行 4. 元素定位 5 ...

  8. C语言实现扫雷游戏(超详细讲解+全部源码)

    电子信息 工科男 一点一点努力! 文章目录 前言 一.游戏介绍 二.游戏设计思路 二.具体步骤 1.创建test.c和game.c源文件以及 game.h头文件 2.创建菜单 3.创建雷盘 4.初始化 ...

  9. 用Python做一个超好玩的拼图游戏,0基础也能包你学会,附送超详细注释的源码~

    导语 你所认为的python........                                                              python & bor ...

最新文章

  1. 文档和帮助创作工具提供商Innovasys实用教程(一)
  2. 【附段错误原因,最后两个测试点】1052 Linked List Sorting (25 分)【链表类题目总结】
  3. 如何在ASP.NET服务器控件库中嵌入JavaScript脚本文件 [适用于.NET 2.0]
  4. windows API 开发飞机订票系统 图形化界面 (二)
  5. 给 asp.net core 写一个简单的健康检查
  6. 带头结点头部插入创建链表
  7. AS3开发必须掌握的内容
  8. 公司各个部门所有英文缩写
  9. Dashboard Design 4.0(Xcelsius)数据直接绑定功能:瑕瑜互见
  10. 一分钟了解英语表达:性能,能力
  11. [落选]2021微信大数据挑战赛_总结
  12. 深入浅出图神经网络|GNN原理解析☄学习笔记(四)表示学习
  13. 九宫格拼图小游戏开发笔记-随机网格生成
  14. hadoop之hdfs及其工作原理
  15. 3分钟看懂零售店新经济:零售店如何运营数据分析经营好门店?
  16. 全球与中国苯二亚甲基二异氰酸酯(XDI)市场发展形势与未来前景分析报告2022-2028年
  17. 银监会计算机专业考试,)(2015国家公务员考试银监会计算机专业考试分析
  18. 运维工程师使用的运维平台和工具包括:
  19. 三七互娱后端工程师笔试记录
  20. java 编程式事务管理_spring-编程式事务管理

热门文章

  1. 特别实用的JAVA小技巧
  2. Android个推需要的权限,Android 推送实现-接入个推(GTPush)
  3. 【图像分割】基于萤火虫优化的半监督谱聚类彩色图像分割方法(Matlab代码实现)
  4. 素问 —渗透测试基本知识
  5. 算法提高 金陵十三钗 状态亚索DP
  6. python便捷数据怎么获取_Python数据分析入门——从数据获取到可视化
  7. php支付宝接口md5签名,支付宝接口url生成,MD5验证
  8. Python3爬虫(一)抓取网页的html
  9. Tensorflow第四课,图片相似度比较前的图片裁剪
  10. 怎样购买CSDN VIP会员可以获得博客等级折扣