React’s code base is already quite large, and with v16’s Fiber refactoring, it’s easy for beginners to get bogged down in detail, which makes them feel like they’re awesome, and lose confidence and wonder if they should continue working on the front end. Try to regain some confidence in this post.
Preact, a shortened version of React, is very small but has all the guts. If you want to learn the basics of React, check out the source code for Preact, which is the purpose of this article.
There are many excellent articles about the React principle. This article is a summary of the old wine in a new bottle, which will pave the way for the following articles.
The length of the article is long and the reading time is about 20min, which is mainly occupied by codes. In addition, the flow chart is also drawn to facilitate the understanding of codes.
Note: This code is based on Preact V10, and features such as SVG, replaceNode and context are omitted
- Virtual-DOM
- Starting from the createElement method
- The realization of the Component
- The diff algorithm
- diffChildren
- diff
- diffElementNodes
- diffProps
- The realization of the Hooks
- useState
- useEffect
- Technology map
- extension
Virtual-DOM
Virtual-dom is essentially a tree of objects, nothing special, that eventually maps to graphic objects. The core of Virtual-DOM is its diff algorithm.
You can imagine that there is a DOM mapper, as the name implies. The job of this “DOM mapper” is to map the Virtual-DOM object tree to the DOM of the browser page, but only to improve the DOM ‘performance ‘. Instead of rendering the entire Virtual-dom tree in full every time, it supports receiving two virtual-DOM object trees (one before and one after the update) and using the diff algorithm to calculate the differences between the two virtual-dom trees. Then apply only those differences to the actual DOM tree, reducing the cost of DOM changes.
Virtual-dom is a bit of a challenge, but it’s faster than React. Why? . Never leave the scene to judge a technology. React was so popular on the web that some people thought virtual-dom was bad and JQuery was too weak.
In terms of performance, however awesome the framework is, it needs to manipulate the native DOM, and it’s not necessarily as’ refined ‘as you would use JQuery to manipulate the DOM manually. Improper use of the frame can also result in a small state modification, resulting in a rendering avalanche (extensive re-rendering); Similarly, although JQuery can refine DOM operations, unreasonable DOM update strategies may also become performance bottlenecks for applications. So it all depends on how you use it.
So why virtual-dom?
My personal understanding is to liberate productivity. As hardware gets better and web applications get more complex, productivity needs to keep up. While manual manipulation of the DOM is possible to achieve greater performance and flexibility, it is too inefficient for most developers, and we can afford to sacrifice a bit of performance for greater development efficiency.
Therefore, the greater significance of Virtual-DOM lies in the change of development mode: Declarative and data-driven, developers do not need to care about the details of DOM manipulation (property manipulation, event binding, DOM node changes), which means that the application development mode is changed to View = F (state), which is a great boost to the liberation of productivity.
Of course, Virtual-dom is neither the only nor the first such solution. Template-based implementations, such as AngularJS and Vue1. X, can also make this transition. That might be better performance than their virtual-dom counterparts, plus the virtual-DOM is more thoroughly abstracted in the rendering layer, no longer coupled to the DOM itself, such as rendering as ReactNative, PDF, terminal UI, etc.
Starting from the createElement method
Many people equate JSX to virtual-DOM, but there is no direct relationship between the two. We know that JSX is just a syntactic sugar.
For example, < a href = “/” > < span > Home < / span > < / a > will eventually converted to h (‘ a ‘, {href: ‘/’}, h (” span “, null, ‘Home’)) in this form, h is JSX Element factory method.
H under React is react. createElement. Most virtual-dom frameworks use h. h as an alias for createElement. The Vue ecosystem also uses this convention. .
JSX factories can be configured using @jsx annotations or the Babel configuration item:
/** * @jsx h */
render(<div>hello jsx</div>, el);
Copy the code
This article isn’t a primer on React or Preact, so check out the official tutorial for more.
Now look at createElement, which simply constructs an object (VNode):
// ⚛️type Specifies the type of the node, including DOM elements (string) and custom components, and fragments, which, if null, represent text nodes
export function createElement(type, props, children) {
props.children = children;
// ⚛️ apply defaultProps
if(type ! =null&& type.defaultProps ! =null)
for (let i in type.defaultProps)
if (props[i] === undefined) props[i] = type.defaultProps[i];
let ref = props.ref;
let key = props.key;
// ...
// ⚛️ build the VNode object
return createVNode(type, props, key, ref);
}
export function createVNode(type, props, key, ref) {
return { type, props, key, ref, / *... Ignore some of the built-in fields */ constructor: undefined };
}
Copy the code
With JSX and components, you can construct a complex tree of objects:
render(
<div className="container">
<SideBar />
<Body />
</div>,
root,
);
Copy the code
The realization of the Component
Components are the soul of a view framework, just as functions are to functional languages and classes are to object-oriented languages, complex applications cannot be formed without components.
Component-based thinking recommends divide-and-conquer an application, breaking down and combining components at different levels to simplify application development and maintenance and make it easier to understand. Technically, a component is a custom element type that can declare its inputs (props), have its own lifecycle and state and methods, and ultimately output a tree of Virtual-DOM objects as a branch of the application Virtual-DOM tree.
Preact’s custom components are implemented based on the Component class. The most basic component is state maintenance, which is implemented through setState:
function Component(props, context) {}
/ / ⚛ ️ setState implementation
Component.prototype.setState = function(update, callback) {
// Clone the State for the next rendering, _nextState is used in some lifecycle mode (shouldComponentUpdate)
let s = (this._nextState ! = =this.state && this._nextState) ||
(this._nextState = assign({}, this.state));
/ / state update
if (typeofupdate ! = ='function' || (update = update(s, this.props)))
assign(s, update);
if (this._vnode) { / / mounted
// Push the render callback queue and call it in batches after rendering is complete
if (callback) this._renderCallbacks.push(callback);
// Put it into the asynchronous scheduling queue
enqueueRender(this); }};Copy the code
EnqueueRender puts components in an asynchronous batch queue, which merges frequent setState calls, and the implementation is simple:
let q = [];
// Asynchronous scheduler, used to execute a callback asynchronously
const defer = typeof Promise= ='function'
? Promise.prototype.then.bind(Promise.resolve()) // micro task
: setTimeout; // Call back to setTimeout
function enqueueRender(c) {
// There is no need to push components already in the queue repeatedly
if(! c._dirty && (c._dirty =true) && q.push(c) === 1)
defer(process); // When the queue changes from empty to non-empty, scheduling starts
}
// Empty the queue in batches and call Component's forceUpdate
function process() {
let p;
// Sort the queue from lower level components first?
q.sort((a, b) = > b._depth - a._depth);
while ((p = q.pop()))
if (p._dirty) p.forceUpdate(false); // false does not force updates, that is, shouldComponentUpdate should not be ignored
}
Copy the code
Ok, the above code shows that setState is essentially a component re-rendering call to forceUpdate. Dig a little deeper into the forceUpdate implementation.
Instead of looking at diff as a black box, it is a DOM mapper that takes in two VNode trees and a DOM mount point that can create, remove, or update components and DOM elements during alignment. Trigger the corresponding lifecycle method.
Component.prototype.forceUpdate = function(callback) { // callback places the rendered callback
let vnode = this._vnode, dom = this._vnode._dom, parentDom = this._parentDom;
if (parentDom) { // It has been mounted
constforce = callback ! = =false;
let mounts = [];
// Call diff to re-render and virtual-dom the current component
// ⚛️ ignore these parameters for a moment and think of diff as a black box, which is a DOM mapper,
dom = diff(parentDom, vnode, vnode, mounts, this._ancestorComponent, force, dom);
if(dom ! =null&& dom.parentNode ! == parentDom) parentDom.appendChild(dom); commitRoot(mounts, vnode); }if (callback) callback();
};
Copy the code
The render method implements the same implementation as forceUpdate, calling the diff algorithm to perform DOM updates, but specifying a DOM container externally:
/ / simplified version
export function render(vnode, parentDom) {
vnode = createElement(Fragment, null, [vnode]);
parentDom.childNodes.forEach(i= > i.remove())
let mounts = [];
diffChildren(parentDom, null oldVNode, mounts, vnode, EMPTY_OBJ);
commitRoot(mounts, vnode);
}
Copy the code
Comb through the above process:
Other features of the component, such as initialization and lifecycle functions, are not seen so far. These features are defined in the diff function, which is called during component mounting or updating. Diff will be covered in the next section
The diff algorithm
As you can see, the createElement and Component logic is thin, and the main logic is concentrated in the diff function. React calls this process Reconciliation and Differantiate in Preact.
To simplify the implementation of Preact, diff and DOM are mixed together, but the logic is clear:
├─ ├─ SRC /diff ├── ├─ SRC/Diff ├── ├─ props.js #Copy the code
Before diving into the diff program, let’s take a look at the basic object structure to understand the program flow. Take a look at the appearance of a VNode:
type ComponentFactory<P> = preact.ComponentClass<P> | FunctionalComponent<P>;
interface VNode<P = {}> {
// Node type. The built-in DOM element is of type string, while custom components are of type Component. Function components in Preact are just special Component types
type: string | ComponentFactory<P> | null;
props: P & { children: ComponentChildren } | string | number | null;
key: Key
ref: Ref<any> | null;
/** * Internal cache information */
// VNode child node
_children: Array<VNode> | null;
// The associated DOM node, the first child of the Fragment
_dom: PreactElement | Text | null;
// Fragment, or the component returns the last DOM child of the Fragment,
_lastDomChild: PreactElement | Text | null;
/ / Component instance
_component: Component | null;
}
Copy the code
diffChildren
We’ll start with the simplest one, which already guessed that diffChildren is used to compare two VNode lists.
As shown in the figure above, a variable oldDOM representing the current insertion position needs to be maintained, starting with the first element of the DOM childrenNode and then pointing to the next sibling of newDOM each time an update is inserted or newDOM is inserted.
As we traverse the newChildren list, we try to find the old VNode with the same key and diff with it. If the new VNode and the old VNode are not in the same position, this requires them to be moved; For newly added DOM, if the oldDOM insertion position is already at the end, it is appended directly to the parent node; otherwise, it is inserted before oldDOM.
Unmount the unused vnodes from the old VNode list.
Take a closer look at the source code:
export function diffChildren(
parentDom, //Children's parent DOM element, newParentVNode,//VNode oldParentVNode, the new parent of children,//The virtualized "old" vnodes compare the virtualized "children" with the virtualized "children".//Store component instances that were mounted during the comparison, and after the comparison, trigger the componentDidMount life cycle function ancestorComponent for those components,//The immediate father of children'components'Render (= render)VNodeComponent instance ofoldDom, // Currently mountedDOMfordiffChildrenSpeaking,oldDomStart by pointing to the first child node.{
let newChildren = newParentVNode._children || toChildArray(newParentVNode.props.children, (newParentVNode._children = []), coerceToVNode, true,);
let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR;
// ...
// ⚛️ iterate through new children
for (i = 0; i < newChildren.length; i++) {
childVNode = newChildren[i] = coerceToVNode(newChildren[i]); // Normalize the VNode
if (childVNode == null) continue
// ⚛️ find if there is a corresponding element in oldChildren. If there is an element in oldChildren, remove it from oldChildren by setting it to undefined
// Keep null if not found
oldVNode = oldChildren[i];
for (j = 0; j < oldChildrenLength; j++) {
oldVNode = oldChildren[j];
if (oldVNode && childVNode.key == oldVNode.key && childVNode.type === oldVNode.type) {
oldChildren[j] = undefined;
break;
}
oldVNode = null; // No old node was found, indicating a new node
}
// ⚛️ to recursively compare vNodes
newDom = diff(parentDom, childVNode, oldVNode, mounts, ancestorComponent, null, oldDom);
// Vnode is not unloaded by diff
if(newDom ! =null) {
if(childVNode._lastDomChild ! =null) {
// ⚛️ The current VNode type is Fragment
// Only vnodes with fragments or components returning fragments will have a non-null _lastDomChild from the DOM tree at the end of the Fragment:
//
// <> <> 👈 Fragment type, diff will recursively compare its children, so in the end we just need to point newDom to the last child after comparison
// a <- diff -> b
// b a ----+
/ / < / a > < / a > \
//
x
👈oldDom will point to this
//
newDom = childVNode._lastDomChild;
} else if (oldVNode == null|| newDom ! = oldDom || newDom.parentNode ==null) {
// ⚛️ newDom does not match the current oldDom, try to add or modify the location
outer: if (oldDom == null|| oldDom.parentNode ! == parentDom) {// ⚛️oldDom points to the end, i.e. there are no more elements, just insert; This is where the first rendering will be called
parentDom.appendChild(newDom);
} else {
// This is an optimization measure, remove will not affect the normal program. Ignore this code for clarity
// Try to find oldChildLength/2 elements backwards. If you find oldChildLength, you do not need to call insertBefore. This code reduces the frequency of insertBefore calls
for (sibDom = oldDom, j = 0; (sibDom = sibDom.nextSibling) && j < oldChildrenLength; j += 2) {
if (sibDom == newDom)
break outer;
}
// ⚛️insertBefore() moves newDom before oldDomparentDom.insertBefore(newDom, oldDom); }}// ⚛️ In other cases, newDom === oldDOM is not handled
⚛️ oldDom points to the next DOM nodeoldDom = newDom.nextSibling; }}// ⚛️ offloads elements not set to undefined
for (i = oldChildrenLength; i--; )
if(oldChildren[i] ! =null) unmount(oldChildren[i], ancestorComponent);
}
Copy the code
To understand how diffChilrend is called:
Summarize the flow chart
diff
The diff function is used to compare two vNodes. The diff function is verbose, but there is no complicated logic in it, mainly some custom component lifecycle processing. So start with the flow chart, if you’re not interested in the code you can skip it.
Source code parsing:
export function diff(
parentDom, //The parent DOM node, newVNode,//New VNode oldVNode,//Old VNode mounts,//AncestorComponent will be batchprocessed after diff,//The immediate parent component force,//ShouldComponentUpdate oldDom is ignored if it is true,//The DOM node currently mounted) {
/ /...
try {
outer: if (oldVNode.type === Fragment || newType === Fragment) {
// the Fragment type is ⚛️ and diffChildren is used for comparison
diffChildren(parentDom, newVNode, oldVNode, mounts, ancestorComponent, oldDom);
// ⚛️ Record the start and end DOM of the Fragment
let i = newVNode._children.length;
if (i && (tmp = newVNode._children[0]) != null) {
newVNode._dom = tmp._dom;
while (i--) {
tmp = newVNode._children[i];
if (newVNode._lastDomChild = tmp && (tmp._lastDomChild || tmp._dom))
break; }}}else if (typeof newType === 'function') {
// ⚛️ Customize the component type
if (oldVNode._component) {
// ⚛️ ️ The component instance already exists
c = newVNode._component = oldVNode._component;
newVNode._dom = oldVNode._dom;
} else {
// ⚛️ initializes the component instance
if (newType.prototype && newType.prototype.render) {
// ⚛️ class component
newVNode._component = c = new newType(newVNode.props, cctx); // eslint-disable-line new-cap
} else {
// ⚛️ function component
newVNode._component = c = new Component(newVNode.props, cctx);
c.constructor = newType;
c.render = doRender;
}
c._ancestorComponent = ancestorComponent;
c.props = newVNode.props;
if(! c.state) c.state = {}; isNew = c._dirty =true;
c._renderCallbacks = [];
}
c._vnode = newVNode;
if (c._nextState == null) c._nextState = c.state;
// ⚛️getDerivedStateFromProps lifecycle method
if(newType.getDerivedStateFromProps ! =null)
assign(c._nextState == c.state
? (c._nextState = assign({}, c._nextState)) // Lazy copy
: c._nextState,
newType.getDerivedStateFromProps(newVNode.props, c._nextState),
);
if (isNew) {
// ⚛️ calls some pre-mount lifecycle methods
/ / ⚛ ️ componentWillMount
if (newType.getDerivedStateFromProps == null&& c.componentWillMount ! =null) c.componentWillMount();
/ / ⚛ ️ componentDidMount
Components are pushed into the mounts array and called in batches after the entire component tree diff is complete. They are called in the commitRoot method
// Called in first out (stack) order, i.e. ComponentDidMount is called first for the child component
if(c.componentDidMount ! =null) mounts.push(c);
} else {
// ⚛️ calls some of the life-cycle methods related to re-rendering
/ / ⚛ ️ componentWillReceiveProps
if (newType.getDerivedStateFromProps == null && force == null&& c.componentWillReceiveProps ! =null)
c.componentWillReceiveProps(newVNode.props, cctx);
/ / ⚛ ️ shouldComponentUpdate
if(! force && c.shouldComponentUpdate ! =null && c.shouldComponentUpdate(newVNode.props, c._nextState, cctx) === false) {
// shouldComponentUpdate returns false to cancel the render update
c.props = newVNode.props;
c.state = c._nextState;
c._dirty = false;
newVNode._lastDomChild = oldVNode._lastDomChild;
break outer;
}
/ / ⚛ ️ componentWillUpdate
if(c.componentWillUpdate ! =null) c.componentWillUpdate(newVNode.props, c._nextState, cctx);
}
// ⚛️ At this point the props and state have been identified, and the props and state have been cached and updated to prepare the rendering
oldProps = c.props;
oldState = c.state;
c.props = newVNode.props;
c.state = c._nextState;
let prev = c._prevVNode || null;
c._dirty = false;
/ / ⚛ ️ rendering
let vnode = (c._prevVNode = coerceToVNode(c.render(c.props, c.state)));
/ / ⚛ ️ getSnapshotBeforeUpdate
if(! isNew && c.getSnapshotBeforeUpdate ! =null) snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
// ⚛️ component level, which affects the update priority
c._depth = ancestorComponent ? (ancestorComponent._depth || 0) + 1 : 0;
// ⚛️ recursively diff rendering results
c.base = newVNode._dom = diff(parentDom, vnode, prev, mounts, c, null, oldDom);
if(vnode ! =null) {
newVNode._lastDomChild = vnode._lastDomChild;
}
c._parentDom = parentDom;
// ⚛️ apply ref
if ((tmp = newVNode.ref)) applyRef(tmp, c, ancestorComponent);
// ⚛️ calls renderCallbacks, the setState callback
while ((tmp = c._renderCallbacks.pop())) tmp.call(c);
/ / ⚛ ️ componentDidUpdate
if(! isNew && oldProps ! =null&& c.componentDidUpdate ! =null) c.componentDidUpdate(oldProps, oldState, snapshot);
} else {
// ⚛️ compares two DOM elements
newVNode._dom = diffElementNodes(oldVNode._dom, newVNode, oldVNode, mounts, ancestorComponent);
if ((tmp = newVNode.ref) && oldVNode.ref !== tmp) applyRef(tmp, newVNode._dom, ancestorComponent);
}
} catch (e) {
// ⚛️ catches render errors and passes them to the parent component's didCatch lifecycle method
catchErrorInComponent(e, ancestorComponent);
}
return newVNode._dom;
}
Copy the code
diffElementNodes
Comparing two DOM elements is very simple:
function diffElementNodes(dom, newVNode, oldVNode, mounts, ancestorComponent) {
// ...
// ⚛️ create a DOM node
if (dom == null) {
if (newVNode.type === null) {
// ⚛️ text node, with no attributes and children, returns directly
return document.createTextNode(newProps);
}
dom = document.createElement(newVNode.type);
}
if (newVNode.type === null) {
// ⚛️ text node update
if(oldProps ! == newProps) dom.data = newProps; }else {
if(newVNode ! == oldVNode) {// newVNode ! == oldVNode this indicates a static node
let oldProps = oldVNode.props || EMPTY_OBJ;
let newProps = newVNode.props;
// ⚛️ dangerouslySetInnerHTML processing
let oldHtml = oldProps.dangerouslySetInnerHTML;
let newHtml = newProps.dangerouslySetInnerHTML;
if (newHtml || oldHtml)
if(! newHtml || ! oldHtml || newHtml.__html ! = oldHtml.__html) dom.innerHTML = (newHtml && newHtml.__html) ||' ';
// ⚛️ recurses over child elements
diffChildren(dom, newVNode, oldVNode, context, mounts, ancestorComponent, EMPTY_OBJ);
// ⚛️ to recursively compare DOM attributesdiffProps(dom, newProps, oldProps, isSvg); }}return dom;
}
Copy the code
diffProps
The diffProps is used to update the attributes of a DOM element
export function diffProps(dom, newProps, oldProps, isSvg) {
let i;
const keys = Object.keys(newProps).sort();
// ⚛️ compare and set properties
for (i = 0; i < keys.length; i++) {
const k = keys[i];
if(k ! = ='children'&& k ! = ='key'&& (! oldProps || (k ==='value' || k === 'checked'? dom : oldProps)[k] ! == newProps[k])) setProperty(dom, k, newProps[k], oldProps[k], isSvg); }// ⚛️ clears properties
for (i in oldProps)
if(i ! = ='children'&& i ! = ='key' && !(i in newProps))
setProperty(dom, i, null, oldProps[i], isSvg);
}
Copy the code
The diffProps implementation is relatively simple, which is to traverse whether the property has changed, and if it has changed, set the property through setProperty. For failed props, setProperty is also null. The slightly more complicated one is setProperty. This involves handling events, converting names, and so on:
function setProperty(dom, name, value, oldValue, isSvg) {
if (name === 'style') {
// ⚛️ style Settings
const set = assign(assign({}, oldValue), value);
for (let i in set) {
// The style attribute has not changed
if ((value || EMPTY_OBJ)[i] === (oldValue || EMPTY_OBJ)[i]) continue;
dom.style.setProperty(
i[0= = =The '-' && i[1= = =The '-' ? i : i.replace(CAMEL_REG, '- $&'),
value && i in value
? typeof set[i] === 'number' && IS_NON_DIMENSIONAL.test(i) === false
? set[i] + 'px'
: set[i]
: ' './ / to empty); }}else if (name[0= = ='o' && name[1= = ='n') {
// ⚛️ Event binding
letuseCapture = name ! == (name = name.replace(/Capture$/.' '));
let nameLower = name.toLowerCase();
name = (nameLower in dom ? nameLower : name).slice(2);
if (value) {
// ⚛️ adds the event for the first time. Note that eventProxy is the event handler
// Preact collects all event handlers into dom._Listeners and distributes them collectively
// function eventProxy(e) {
// return this._listeners[e.type](options.event ? options.event(e) : e);
// }
if(! oldValue) dom.addEventListener(name, eventProxy, useCapture); }else {
// Remove the event
dom.removeEventListener(name, eventProxy, useCapture);
}
// Save the event queue
(dom._listeners || (dom._listeners = {}))[name] = value;
} else if(name ! = ='list'&& name ! = ='tagName' && name in dom) {
// ⚛️DOM object properties
dom[name] = value == null ? ' ' : value;
} else if (
typeofvalue ! = ='function'&& name ! = ='dangerouslySetInnerHTML'
) {
// ⚛️DOM element attributes
if (value == null || value === false) {
dom.removeAttribute(name);
} else{ dom.setAttribute(name, value); }}}Copy the code
This is the end of the Diff algorithm. The logic is not particularly complex, of course, Preact is an extremely simplified framework. React is much more complex, especially after the React Fiber refactoring. You can also think of Preact as a historical review of React and be interested in learning more about the latest React architecture.
The realization of the Hooks
React16.8 officially introduced hooks, which bring a new way of developing the React component that makes code simpler. React Hooks: Not Magic, Just Arrays This article has revealed the basic implementation of hooks, which are simply arrays based. Preact also implements the hooks mechanism, which is only a few hundred lines, so let’s get a feel for it.
The hooks function itself is not integrated within the Preact code base, but is imported via Preact /hooks
import { h } from 'preact';
import { useEffect } from 'preact/hooks';
function Foo() {
useEffect((a)= > {
console.log('mounted'); } []);return <div>hello hooks</div>;
}
Copy the code
So how does Preact extend the diff algorithm to implement hooks? In fact, Preact provides options objects to extend Preact diff. Options are similar to Preact lifecycle hooks that are called during diff (I’ve omitted the above code for brevity). Such as:
export function diff(/ *... * /) {
// ...
// ⚛️ start diff
if ((tmp = options.diff)) tmp(newVNode);
try {
outer: if (oldVNode.type === Fragment || newType === Fragment) {
// Fragment diff
} else if (typeof newType === 'function') {
// Custom component diff
// ⚛️ start rendering
if ((tmp = options.render)) tmp(newVNode);
try {
// ..
c.render(c.props, c.state, c.context),
} catch (e) {
// ⚛️ catch an exception
if ((tmp = options.catchRender) && tmp(e, c)) return;
throwe; }}else {
// DOM element diff
}
/ / ⚛ ️ diff
if ((tmp = options.diffed)) tmp(newVNode);
} catch (e) {
catchErrorInComponent(e, ancestorComponent);
}
return newVNode._dom;
}
// ...
Copy the code
useState
Start with the most commonly used useState:
export function useState(initialState) {
// ⚛️OK is just an array, no Magic, each hooks call increments currenIndex, fetching state from the current component
const hookState = getHookState(currentIndex++);
// ⚛️ initializes
if(! hookState._component) { hookState._component = currentComponent;// The current component instance
hookState._value = [
// ⚛️state, initialize state
typeof initialState === 'function' ? initialState() : initialState,
/ / ⚛ ️ dispatch
value => {
const nextValue = typeof value === 'function' ? value(hookState._value[0]) : value;
if (hookState._value[0] !== nextValue) {
// ⚛️ saves the state and calls setState to force an update
hookState._value[0] = nextValue; hookState._component.setState({}); }},]; }return hookState._value; // [state, dispatch]
}
Copy the code
As you can see from the code, the key is the implementation of getHookState
import { options } from 'preact';
let currentIndex; // Save the index of the current hook
let currentComponent;
// ⚛️render hook, called before the component starts rendering
// Because Preact is rendered recursively and synchronously, and Javascript is single-threaded, it is safe to reference the component instance currently being rendered
options.render = vnode= > {
currentComponent = vnode._component; // Save the component currently being rendered
currentIndex = 0; // Index is reset to 0 when rendering starts
// useEffect can be understood
UseEffect clears the Effect that was not processed in the last rendering (useEffect). This happens only during a quick re-rendering, and is normally processed in an asynchronous queue
if(currentComponent.__hooks) { currentComponent.__hooks._pendingEffects = handleEffects( currentComponent.__hooks._pendingEffects, ); }};/ / ⚛ ️ no magic! Is just an array, and the state is stored in the component instance's _list array
function getHookState(index) {
// Get or initialize the list
const hooks = currentComponent.__hooks ||
(currentComponent.__hooks = {
_list: [].// Put state
_pendingEffects: [], // Place the effect to be processed, saved by useEffect
_pendingLayoutEffects: [], // Place the layoutEffect to be processed and save it with useLayoutEffect
});
// Create a state
if (index >= hooks._list.length) {
hooks._list.push({});
}
return hooks._list[index];
}
Copy the code
The general process is as follows:
useEffect
See also useEffect and useLayoutEffect. UseEffect is similar to useLayouteEffect except that it is triggered at a different time. UseEffect draws the trigger after it has finished rendering. UseLayoutEffect triggers when diff is complete:
export function useEffect(callback, args) {
const state = getHookState(currentIndex++);
if (argsChanged(state._args, args)) {
// ⚛️ Status changes
state._value = callback;
state._args = args;
currentComponent.__hooks._pendingEffects.push(state); // ⚛️ advance _pendingEffects queueafterPaint(currentComponent); }}export function useLayoutEffect(callback, args) {
const state = getHookState(currentIndex++);
if (argsChanged(state._args, args)) {
// ⚛️ Status changes
state._value = callback;
state._args = args;
currentComponent.__hooks._pendingLayoutEffects.push(state); // ⚛️ advance _pendingLayoutEffects queues}}Copy the code
UseEffect is placed in an asynchronous queue and is scheduled by requestAnimationFrame to batch process:
// This is an asynchronous queue similar to the one above
afterPaint = component= > {
if(! component._afterPaintQueued &&// Avoid duplicate push-in of components
(component._afterPaintQueued = true) &&
afterPaintEffects.push(component) === 1 // Start scheduling
)
requestAnimationFrame(scheduleFlushAfterPaint); // Scheduled by requestAnimationFrame
};
function scheduleFlushAfterPaint() {
setTimeout(flushAfterPaintEffects);
}
function flushAfterPaintEffects() {
afterPaintEffects.some(component= > {
component._afterPaintQueued = false;
if (component._parentDom)
// Clear the _pendingEffects queue
component.__hooks._pendingEffects = handleEffects(component.__hooks._pendingEffects);
});
afterPaintEffects = [];
}
function handleEffects(effects) {
// Clear before calling effect
effects.forEach(invokeCleanup); // Call cleanup
effects.forEach(invokeEffect); // Call effect again
return [];
}
function invokeCleanup(hook) {
if (hook._cleanup) hook._cleanup();
}
function invokeEffect(hook) {
const result = hook._value();
if (typeof result === 'function') hook._cleanup = result;
}
Copy the code
Let’s see how LayoutEffect is triggered. It’s very simple. It’s triggered after diff is done.
options.diffed = vnode= > {
const c = vnode._component;
if(! c)return;
const hooks = c.__hooks;
if(hooks) { hooks._pendingLayoutEffects = handleEffects(hooks._pendingLayoutEffects); }};Copy the code
👌 hooks Basic principles, finally, let’s use a graph to sum up.
Technology map
It’s a long article, mostly with too much code, and I don’t like reading it myself, so I didn’t expect readers to see it. After the article thinks way to improve improve again. Thank you for reading this far.
The main character of this issue is itself a small but beautiful view framework, with no other technology stack. Here are some other small and beautiful libraries from Preact author developit.
- Workerize elegantly executes and invokes programs in webWorker
- Microbundle zero-configuration library packaging tool
- Greenlet is similar to workerize, which executes a single asynchronous function in a Webworker, whereas workerize is a module
- Mitt EventEmitter 200 byte
- DLV securely accesses deeply nested object properties, similar to loDash’s GET method
- Snarkdown 1KB Markdown Parser
- Unistore Redux state container supports React and Preact
- Stockroom supports a status manager in webWorker
extension
- Preact: Into the void 0
- React Virtual DOM vs Incremental DOM vs Ember’s Glimmer: Fight