Preface
What is preact? React’s 3KB lightweight solution has the same es6api
Although preact and react have the same API, the differences in their internal implementation mechanism are still huge. But this does not prevent us from reading and learning the source code of preact. To put it another way, at the beginning of this year, one of my buddies @ Xiaohan met a Daniel from Facebook in an interview with a company in Beijing. The Daniel from Facebook also recommended him to read and learn the source code of preact.
Hooks are not magic, and their design has nothing to do with react (Dan Abramov). It’s the same in preact, so even if you haven’t read the source code of preact or react, you can understand the implementation of hooks.
I hope that the following sharing can give you some inspiration to understand the implementation behind hooks.
On the rules of hooks
The rules for using hooks in react are as follows. We can see that the use of hooks is highly dependent on the execution order. After reading the source code, we will know why there are two rules for using hooks.
- ✅ use hook only at the top level. Do not call hook in loops, conditions, or nested functions.
- Do not call Hook in the ordinary JavaScript function.
Analysis of hooks source code
getHookState
getHookState
Function, will be in theCurrent componentOn the instance of__hooks
Attribute.__hooks
As an object,__hooks
Object_list
Attribute usagearrayAll types ofhooks(useState, useEffect…………)
Results of the execution of, return values, etc. because_list
Attribute is used to store state in the form of array, so the execution order of each hooks is particularly important.
function getHookState(index) {
if (options._hook) options._hook(currentComponent);
//Check the component to see if it has the "hooks" property. If not, proactively mount an empty "hooks" object
const hooks =
currentComponent.__hooks ||
(currentComponent.__hooks = {
_List: [], // / list stores the status of all hooks
_Pendingeffects: [], // / state of useeffect stored in pendingeffects
_Pendinglayouteffects: [], // / state of uselayouteffects stored in pendinglayouteffects
_handles: []
});
//According to index. Determine whether there is a corresponding state in the "hooks." list array.
//If not, an empty state is actively added.
if (index >= hooks._list.length) {
hooks._list.push({});
}
//Returns the status corresponding to the index in the "hooks." list array
return hooks._list[index];
}
Some key global variables to be used
stay
getHookState
In, we used global variablescurrentComponent
。 variablecurrentComponent
Points to an instance of the current component. How do we get the reference of the current component instance? Combined with the source code of hooks and preact, it is found that when preactdiff
The virtual node vnode of the current component will be passed to the options. Render function, so that we can get the instance of the current component smoothly.
//Execution order pointer of current hooks
let currentIndex;
//Instance of the current component
let currentComponent;
let oldBeforeRender = options._render;
// vnode is
options._render = vnode => {
if (oldBeforeRender) oldBeforeRender(vnode);
//Instance of current component
currentComponent = vnode._component;
//Reset the index. The hooks state list of each component is accumulated from 0
currentIndex = 0;
if (currentComponent.__hooks) {
currentComponent.__hooks._pendingEffects = handleEffects(
currentComponent.__hooks._pendingEffects
);
}
};
//The omitted diff method
function diff() {
let tmp, c;
// ...
//Mount an instance of the current component on vnode
newVNode._component = c = new Component(newProps, cctx);
// ...
//Pass vnode to the options. Render function, so that we can get an instance of the current component
if ((tmp = options._render)) tmp(newVNode);
}
useState && useReducer
useState
useState
Based onuseReducer
Encapsulation. See below for detailsuseReducer
//Usestate accepts an initial value of initialstate, initializing state
function useState(initialState) {
return useReducer(invokeOrReturn, initialState);
}
invokeOrReturn
invokeOrReturn
It’s a simple tool function. I won’t elaborate here.
function invokeOrReturn(arg, f) {
return typeof f === "function" ? f(arg) : f;
}
useReducer
useReducer
Three parameters are accepted.reducer
Responsible for handlingdispatch
Sponsoredaction
,initialState
yesstate
Initial value of the state,init
Is a function of inerting the initial value.useReducer
Return[state, dispatch]
The content of the format.
function useReducer(reducer, initialState, init) {
//The currentindex is incremented by one, creating a new state, which will be stored in the currentcomponent. \\\\\\\\\
const hookState = getHookState(currentIndex++);
if (!hookState._component) {
//State stores references to the current component
hookState._component = currentComponent;
hookState._value = [
//Return initialstate if the third parameter ` init 'is not specified
//If the third parameter is specified, it returns the initialstate processed by the function of inerting the initial value
//'usestate' is based on the encapsulation of 'usereducer'.
//In 'usestate', hookstate. U value [0], by default, returns initialstate directly
!init ? invokeOrReturn(null, initialState) : init(initialState),
//Hookstate. U value [1], accept an 'action', {type: 'XX'}
//Because 'usestate' is based on the encapsulation of 'usereducer', the action parameter may also be a new state value, or the state update function as a parameter
action => {
//Returns the new status value
const nextValue = reducer(hookState._value[0], action);
//Update status with new status value
if (hookState._value[0] !== nextValue) {
hookState._value[0] = nextValue;
//Call setstate of component and perform diff operation again (in preact, the actual DOM node will be updated synchronously during diff process)
hookState._component.setState({});
}
}
];
}
//For usereduer, return [state, dispath]
//For usestate, return [state, setstate]
return hookState._value;
}
⭐️useEffect
useEffect
Let’s do side effects in function components. Event binding, data request, dynamic DOM modification.useEffect
It will be executed after each react rendering. Whether it is the first time to mount, or update. Useeffect can return a function that will be executed when react is cleared. Whenever this effect is executed, the previous effect is cleared. Cleanup is also performed when the component is uninstalled.
function useEffect(callback, args) {
//The currentindex is incremented by 1, adding a new status to the currentcomponent. Hooks. List
const state = getHookState(currentIndex++);
//Argschanged function, which checks whether the dependency of useeffect has changed.
//If there is a change, argschanged returns true, and the callback of useeffect will be executed again.
//If there is no change, argschanged returns false and does not perform a callback
//In the first rendering, if state. Args is equal to undefined, argschanged directly returns true
if (argsChanged(state._args, args)) {
state._value = callback;
//Save the last dependency in useeffect's state and use it for comparison the next time
state._args = args;
//Store the state of useeffect in hooks. Pending effects
currentComponent.__hooks._pendingEffects.push(state);
//Add the component of callback that needs to execute useeffect to the array of afterpainteffects and save it temporarily
//Because we need to wait for the rendering to finish, and execute the callback of useeffect
afterPaint(currentComponent);
}
}
argsChanged
argsChanged
Is a simple utility function to compare the differences between two arrays. Returns false if each item in the array is equal, and true if one item is not equal. The main purpose is to compare the dependence of hooks such as useeffect and usememo.
function argsChanged(oldArgs, newArgs) {
return !oldArgs || newArgs.some((arg, index) => arg !== oldArgs[index]);
}
afterPaint
afterPaint
Function that will need to be executeduseEffect
The component of the callback, push to the globalafterPaintEffects
Array.
let afterPaintEffects = [];
let afterPaint = () => {};
if (typeof window !== "undefined") {
let prevRaf = options.requestAnimationFrame;
afterPaint = component => {
if (
//The "afterpaintqueued" attribute ensures that each component can only be pushed once into the afterpainteffects
(!component._afterPaintQueued &&
(component._afterPaintQueued = true) &&
//Afterpainteffects. Push (component) = = 1, ensuring that 'saferaf' will only be executed once before emptying
//Add component to the afterpainteffects array
afterPaintEffects.push(component) === 1) ||
prevRaf !== options.requestAnimationFrame
) {
prevRaf = options.requestAnimationFrame;
//Execute saferaf (flusafterpainteffects)
(options.requestAnimationFrame || safeRaf)(flushAfterPaintEffects);
}
};
}
safeRaf
safeRaf
Will open arequestAnimationFrame
, it will callflushAfterPaintEffects
, handle the callback of useeffect.
const RAF_TIMEOUT = 100;
function safeRaf(callback) {
const done = () => {
clearTimeout(timeout);
cancelAnimationFrame(raf);
setTimeout(callback);
};
const timeout = setTimeout(done, RAF_TIMEOUT);
//The diff process is synchronous, and the requestanimationframe will be executed after the diff is completed (after the macro task is completed)
const raf = requestAnimationFrame(done);
}
flushAfterPaintEffects
flushAfterPaintEffects
Responsible for handlingafterPaintEffects
All components in the array
function flushAfterPaintEffects() {
//Loop through all components to be processed in the afterpainteffects array
afterPaintEffects.some(component => {
component._afterPaintQueued = false;
if (component._parentDom) {
//Use handleeffects to clear the state of all useeffects in currentcomponent. Hooks. Pending effects
//Handleeffects performs the logic of clearing and executing the effect
//Finally, handleeffects returns an empty array, which resets the component. Hooks. Pending effects
component.__hooks._pendingEffects = handleEffects(
component.__hooks._pendingEffects
);
}
});
//Empty afterpainteffects
afterPaintEffects = [];
}
handleEffects
Clear and execute the
useEffect
function handleEffects(effects) {
//Clear effect
effects.forEach(invokeCleanup);
//Perform all effects
effects.forEach(invokeEffect);
return [];
}
invokeCleanup
//Perform clear effect
function invokeCleanup(hook) {
if (hook._cleanup) hook._cleanup();
}
invokeEffect
function invokeEffect(hook) {
const result = hook._value();
//If the return value of the callback of useeffect is a function
//The function is recorded to the "cleanup" property of useeffect
if (typeof result === "function") {
hook._cleanup = result;
}
}
useMemo && useCallback
useMemo
A remembered value is returned.useCallback
A remembered callback function is returned.useMemo
The memorized value is recalculated when the dependent array changes.useCallback
It will return a new function when the dependent array changes.
useMemo
function useMemo(callback, args) {
//The currentindex is incremented by 1, adding a new status to the currentcomponent. Hooks. List
const state = getHookState(currentIndex++);
//Determine whether the dependent array changes
//If there is a change, the callback will be executed again, and the new return value will be returned
//Otherwise, return the last return value
if (argsChanged(state._args, args)) {
state._args = args;
state._callback = callback;
//State. U valuerecord the last return value (for usecallback, record the last callback)
return state._value = callback();
}
//Returns the return value of callback
return state._value;
}
useCallback
useCallback
Based onuseMemo
Encapsulation. Only when the dependent array changes,useCallback
A new function will be returned, otherwise the first incoming call will always be returned.
function useCallback(callback, args) {
return useMemo(() => callback, args);
}
useRef
useRef
It’s also based onuseMemo
Encapsulation. But the difference is that the dependency array passes in an empty array, which means that every timeuseRef
Will be recalculated.
function useRef(initialValue) {
return useMemo(() => ({ current: initialValue }), []);
}
Application of useref
⭐️Because useref is recalculated every time, we can use the feature to avoid the side effects of closure
//Old values will be printed out
function Bar () {
const [ count, setCount ] = useState(0)
const showMessage = () => {
console.log(`count: ${count}`)
}
setTimeout(() => {
//The printed output is still '0', forming a closure
showMessage()
}, 2000)
setTimout(() => {
setCount((prevCount) => {
return prevCount + 1
})
}, 1000)
return <div/>
}
//Using useref will print out the new value
function Bar () {
const count = useRef(0)
const showMessage = () => {
console.log(`count: ${count.current}`)
}
setTimeout(() => {
//The new value '1' is printed, and the latest value is obtained by count.current
showMessage()
}, 2000)
setTimout(() => {
count.current += 1
}, 1000)
return <div/>
}
useLayoutEffect
useEffec
T will be executed after the diff algorithm finishes rendering the dom. AnduseEffect
The difference is,useLayoutEffect
It will be completed in diff algorithmAfter DOM update, before browser drawingAt all times.useLayoutEffect
How to do it? Similar to the method of getting the current component, preact inserts aoptions.diffed
Hook.
function useLayoutEffect(callback, args) {
//The currentindex is incremented by 1, adding a new status to the currentcomponent. Hooks. List
const state = getHookState(currentIndex++);
//If the array is dependent, the update will be skipped if there is no change
//If the array is dependent, the parameter change performs a callback
if (argsChanged(state._args, args)) {
state._value = callback;
//Record the last dependency array
state._args = args;
currentComponent.__hooks._pendingLayoutEffects.push(state);
}
}
//Options.diffed will update the browser before the diff algorithm finishes redrawing
options.diffed = vnode => {
if (oldAfterDiff) oldAfterDiff(vnode);
const c = vnode._component;
if (!c) return;
const hooks = c.__hooks;
if (hooks) {
hooks._handles = bindHandles(hooks._handles);
//Execute the callback of uselayouteffects of the component
hooks._pendingLayoutEffects = handleEffects(hooks._pendingLayoutEffects);
}
};
//The omitted diff method
function diff() {
let tmp, c;
// ...
// ...
//Before browser drawing, after the diff algorithm is updated, execute the callback of uselayouteeffect
if (tmp = options.diffed) tmp(newVNode);
//Return to the updated Dom and redraw
return newVNode._dom;
}
useImperativeHandle
useImperativeHandle
You can customize the instance values exposed to the parent component.useImperativeHandle
Should andforwardRef
Use together. So let’s first look at preact.forwardRef
The specific implementation of.
forwardRef
Forwardref creates a react component that accepts the ref attribute, but forwards the ref to the component’s child nodes. We ref access the element instance on the child node.
How to use forwardref
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
))
const ref = React.createRef()
//Component accepts ref attribute, but forwards ref to < button >
<FancyButton ref={ref}>Click me!</FancyButton>
The source code of forwardref in preact
//FN is the rendering function, taking (props, ref) as the parameter
function forwardRef(fn) {
function Forwarded(props) {
//Props.ref is the ref on the component created by forwardref
let ref = props.ref;
delete props.ref;
//Call the render function, render the component, and forward ref to the render function
return fn(props, ref);
}
Forwarded.prototype.isReactComponent = true;
Forwarded._forwarded = true;
Forwarded.displayName = 'ForwardRef(' + (fn.displayName || fn.name) + ')';
return Forwarded;
}
useImperativeHandle && bindHandles
function useImperativeHandle(ref, createHandle, args) {
//// currentindex increases by 1, adding a new status to currentcomponent. \\\\\\\\\
const state = getHookState(currentIndex++);
//Determine if dependency has changed
if (argsChanged(state._args, args)) {
//Save the last dependency in useeffect's state and use it for comparison the next time
state._args = args;
//Add the state of useimperatoryhandle to the hooks. Handles array
//Ref is the ref forwarded by forwardref
//The return value of CreateHandle is the custom value exposed by useimperatoryhandle to the parent component
currentComponent.__hooks._handles.push({ ref, createHandle });
}
}
// options.diffed calls bindHandles and handles __hooks._handles.
function bindHandles(handles) {
handles.some(handle => {
if (handle.ref) {
//Replace the current of forward ref
//The replacement is the return value of the second parameter of useimperativehandle
handle.ref.current = handle.createHandle();
}
});
return [];
}
Take an example
function Bar(props, ref) {
useImperativeHandle(ref, () => ({
hello: () => {
alert('Hello')
}
}));
return null
}
Bar = forwardRef(Bar)
function App() {
const ref = useRef('')
setTimeout(() => {
//Useimperatoryhandle modifies the current value of ref
//The current value is the return value of the second parameter of useimperatoryhandle
//So we can call the Hello method exposed by useimperativehandle
ref.current.hello()
}, 3000)
return <Bar ref={ref}/>
}
Recommended reading
- React hooks: not magic, just arrays
- How Are Function Components Different from Classes?