This article contains 10,137 words and takes about 25 minutes to read

No one has ever seen the wind, let alone you and me. But when paper money is floating, we know it’s the wind counting money.

React affects every aspect of our work. We use it every day. The purpose of this article is to explore how React works from a theoretical level.

tool

Before writing this article, I have prepared a simple_react debugging repository. This repository will put the benchmark use cases (only two ^ ^) and React source code together in SRC folder and heat update it via snowpack. Log and debuger can be directly added to the source code for debugging. The React source code is full of dev code and undefined functions, so I rewrote the source code, typescript type specification, and removed a lot of code that wasn’t relevant to the core process (as well as some of the code that wasn’t).

If you just want to learn how React works rather than write a framework to use, then this repository is perfect for you. Of course, the repository is based on Act 16.8, and while this version does not include new features such as the current channel model Lane, it is one that I personally think is more stable and easier to read.

(If you want to debug the complete source code, you can also refer to the pull source code to debug yarn Link)

The article structure

  1. Fiber architecture design and initial rendering process
  2. Event delegation mechanism
  3. Status update
  4. Time slice

Before we look at how React works, we should make sure we know a few basics about React.

Why Framework

First, we need to know what it means to use a framework for development. If we were in the ancient days of pure JS, every data change would cause the presentation state of the component to change, so we would need to manually manipulate the DOM. If the data asynchronously changes dozens of times in a row in a given second, the presentation logic dictates that we also need to modify the DOM dozens of times in a row. Frequent DOM manipulation can have a significant impact on web page performance. Of course, creating and modifying DOM elements doesn’t cost too much performance, mainly because every time a new DOM is inserted into a document, the browser recalculates layout properties, as well as view layers, merges, and renders. Therefore, such code performance is very low.

Consider this scenario. For a front-end list component, three pieces of data are displayed when three pieces of data exist and five pieces of data are displayed when five pieces of data exist. That is, the presentation of the UI must somehow have a logical relationship with the data. If JS can sense the change in key data, use an efficient way to rewrite the DOM into a state corresponding to the data. Developers can focus on changes in business logic and data, and productivity increases dramatically.

Therefore, one of the core functions of the framework is the efficient unification of the UI layer and the data layer.

The React philosophy

React itself is not a framework. React is just a JavaScript library that builds user interfaces from components. It belongs to the View layer of MVC applications. React uses props and state to simplify the storage of critical data, which can be executed many times in a second for a React component function. Each time it is executed, the data is injected into JSX. JSX is not a real DOM and is converted to the React. CreateElement (type, props, children) function. The result is the ReactElement element, or virtual DOM, which describes how the component should be rendered in a particular browser frame.

Virtual Dom

VirtualDom is not exclusive to React, just as Redux can be used in non-React environments, they are just a design idea.

In fact, React’s Virtual Dom and diff processes before fiber were relatively straightforward. However, the whole process becomes tedious with the introduction of Fiber architecture. If you want to understand the principles of VirtualDom and diff process, you can also learn from the simple virtual-dom repository.

The essence of VirtualDom is to use JS variables to abstract the real DOM. Since every manipulation of the DOM can trigger a rearrangement of the browser’s performance, you can use VirtualDom to cache the current component state, batch process user interactions and data changes. Directly calculate the final state that each frame of the page should present, and this state exists in memory in the form of JS variables. VirtualDom ensures that every frame a user sees responds to data changes, while saving performance and preventing browser stutters.

First Render

The first thing we should notice is the render function at the entrance to the React code

ReactDOM.render(<App />, domContainer)
Copy the code

The render process requires React to construct a virtual tree structure (i.e., ReactElement and Fiber) that represents the element structure of the user’s expected page based on user-created JSX syntax. Of course, this process is relatively uncomplicated, because the document is still nothing. In terms of thinking, just generate the real DOM element from the virtual DOM node and insert the Document, and the first rendering is complete.

createReactElement

Usually we convert JSX to a JS execution function via Babel. For example, we wrote a header component in JSX in the React environment

<h1 className='title'>
   <div>Class Component</div>
</h1>
Copy the code

So this component will be transformed by Babel

React.createElement('h1', { className: 'title' }, [
   React.createElement('div', null, [ 'Class Component' ]
])
Copy the code

JSX is an extension of the React syntax recommended by JSX. Of course, you could use the react. createElement function without JSX, but you can see how much higher the mental cost of using pure JS is compared to the simple JSX. React.createElement takes the label name of the DOM element, an attribute object, and an array of child elements. The return value is a ReactElement object.

In fact, the JSX compiled JSON structure is itself an object, and is already available without the react. createElement function. So what did we do in this function.

A ReactElement element has five key attributes. We all know that to build a page, we need to describe the type and structure of the element through HTML, describe the style rendering of the element through style and class, and trigger interaction events and page updates through JS and binding events.

So the most important attribute is the first one, the element type type. If the element is a pure HTML tag element, such as div, then type will be the string div, if it is a React component, such as

function App() {
   return (
   	<div>Hello, World!</div>)}Copy the code

The value of type will refer to the App function, as well as the Class component.

The second property is props, and most of the properties we write in the HTML tag are collected in props, such as ID, className, style, children, click events, and so on.

The third and fourth attributes are key and ref, respectively, where key plays an important role in array processing and diff, and ref is the reference identifier, which I won’t go into here.

The last property is $$typeof, which points to Symbol(react.Element). As well as being a unique identifier for React elements, this tag also serves as a security function. We already know that a ReactElement is actually a JS object. Therefore, if a user maliciously stores a fake React object with intrusive functions into the server database, which is rendered as a page element in the actual rendering process, the security of the user may be threatened. Symbols cannot be stored in a database. In other words, React renders elements that have symbols compiled by JSX. (In browsers that do not support Symbol in earlier versions, strings will be used instead and this arrangement will not be protected.)

Ok, back to the render function. What happens in this function is simply to create the Root structure.

enqueueUpdate

From the designer’s point of view, according to the single responsibility principle and the open/close principle, there needs to be a data structure decoupled from the function body to tell React how to operate fiber. Instead of writing one logic for the first rendering and one logic for the second rendering. As a result, fiber has the Update queue UpdateQueue and the Update list Update structure

If you look at the definition, you’ll see that the update queue updateQueue is a linked list of updates, and the update queue is a linked list. Imagine executing setState three times in a row in a Class Component update function. Instead of mounting it as three updates to the Component, you can provide a more granular form of control. In a nutshell, small setState level updates are merged into one state update. Multiple state updates in a component are merged in the update queue of the component to calculate the newState of the component.

For the first rendering, simply attach an update symbol on the first fiber to indicate that this is a first rendered Fiber.

Export function ScheduleRootUpdate (Current: Fiber, Element: ReactElement, expirationTime: number, suspenseConfig: SuspenseConfig | null, callback? : Function) {// Create an update instance const update = createUpdate(expirationTime, expirationTime, expirationTime) Payload = {element} // Mount the update to the updateQueue property on the root fiber enqueueUpdate( current, update ) ScheduleWork( current, expirationTime ) }Copy the code

Fiber

As the core design of the entire Fiber architecture, Fiber is designed as a linked list structure.

  • Child refers to the first child of the current node
  • Return refers to the parent element of the current node
  • Sibling points to the next sibling of the sibling

If it is a tree before React16, you need to find each node through DFS deep traversal. Now all you need to do is move the pointer to the priority of Child → Sibling → return to process all the nodes

There’s another advantage of this design is working in the React time only need to use a global variable as the pointer moving in the linked list, if there is a user input or other higher priority tasks can suspend the current work, need only after the other tasks according to the position of the pointer to move down before you can continue to work. The pattern of pointer movement can be summarized as top down, left to right.

Basic structure of Kangkang Fiber

Among them

  • Type of tag fiber, such as function component, class component, native component, Portal, etc.
  • Type React See createElement above.
  • Alternate represents a two-way buffer object (see below).
  • The effectTag represents how the fiber will be processed in the next rendering. For example, if you only need to insert, this value will contain Placement, or if you need to delete, it will contain Deletion.
  • ExpirationTime expirationTime. The higher the expirationTime is, the higher the priority of this fiber is.
  • The types of firstEffect and lastEffect are the same as fiber, which are linked by nextEffect. Represents the fiber state to be updated
  • MemorizeState and memorizeProps represent the props and state of the component in the last rendering. If the update succeeds, the new pendingProps and newState will replace the values of the two variables
  • Ref reference identifier
  • stateNodeRepresents the actual state corresponding to this fiber node
    • For native components, this value points to a DOM node (created, but not inserted into the document)
    • For class components, this value points to the corresponding class instance
    • For function components, this value points to Null
    • For RootFiber, this value points to FiberRoot (figure)

The next steps are the core steps of the first render, and since this is the first render, the core task is to render the first screen elements onto the page, so this process will be synchronous.

PrepareFreshStack

Because the author is native goods did not learn English, Baidu under the discovery is ready to clean stack meaning. Combined with the following flow, you can see that the purpose of this step is to do some preparation before the actual work, such as initializing some variables, discarding unfinished work, and most importantly, creating the two-way buffer variable WorkInProgress

let workInProgress: Fiber | null = null.export function prepareFreshStack (root: FiberRoot, expirationTime: number) {
 // Reset the finishWork of the root node
 root.finishedWork = null
 root.finishedExpirationTime = ExpirationTime.NoWork

   ...

 if(workInProgress ! = =null) {
   // If WIP already exists, there is an unfinished task
   // Go up to its root fiber
   let interruptedWork = workInProgress.return
   while(interruptedWork ! = =null) {
     // unwindInterruptedWork // Erases unfinished tasks
     unwindInterruptedWork(interruptedWork)
     interruptedWork = interruptedWork.return
   }
 }
 workInProgressRoot = root
 // Create a two-way buffer object
 workInProgress = createWorkInProgress(root.current, null, expirationTime)
 renderExpirationTime = expirationTime
 workInProgressRootExitStatus = RootExitStatus.RootImcomplete
}
Copy the code

Two-way buffer variable WorkInProgress

The current represents the fiber node corresponding to the component displayed on the current page. You can think of this branch as the Master branch in Git, which represents the external state. WIP represents a pending state, that is, the state to be displayed on the next screen frame. It is like a feature branch pulled from the master, on which we can make any changes. Finally, the result of WIP is rendered on the page. According to the principle of page content corresponding to current, current will point to WIP, that is to say, WIP replaces current (the master branch of Git).

Prior to this, the alternate fields of current and WIP refer to each other, respectively.

So how was the WIP created:

// Create a workInProgress node based on the existing fiber
export function createWorkInProgress (current: Fiber, pendingProps: any, expirationTime) :Fiber {
 let workInProgress = current.alternate
 if (workInProgress === null) {
   // If currently fiber has no alternate
   // tip: We use the "double buffer pool technique" here, because we need at most two instances of a tree.
   // tip: We are free to reuse unused nodes
   // tip: This is created asynchronously, avoid using extra objects
   // tip: This also allows us to free additional memory (if needed)
   workInProgress = createFiber(
     current.tag,
     pendingProps,
     current.key,
     current.mode
   )
   workInProgress.elementType = current.elementType
   workInProgress.type = current.type
   workInProgress.stateNode = current.stateNode

   workInProgress.alternate = current
   current.alternate = workInProgress
 } else {
   // We already have a WIP
   workInProgress.pendingProps = pendingProps

   / / reset effectTag
   workInProgress.effectTag = EffectTag.NoEffect

   // Reset the effect list
   workInProgress.nextEffect = null
   workInProgress.firstEffect = null
   workInProgress.lastEffect = null
 }
Copy the code

As you can see, WIP essentially inherits the core properties of Current, but with clean fiber without some side effects and work logs.

WorkLoop

In the work loop, a while statement is executed, and each time the loop is executed, one fiber node is processed. The workLoop module has a pointer, workInProgress, that points to fiber, which is currently being processed. It moves toward the end of the list until the pointer is null, which stops this part of the workLoop.

Each fiber node is a unit of work, and React determines if it needs to pause work to check for higher-priority user interactions.

function workLoopConcurrent() {
 // Perform work until the Scheduler asks us to yield
 while(workInProgress ! = =null&&! shouldYield()) { workInProgress = performUnitOfWork(workInProgress); }}Copy the code

There are only conditions for jumping out:

  1. All fibers have been traversed
  2. Access to the current thread is transferred to an external task queue

But we’re talking about the first render, and touch rendering takes precedence over everything else, so there’s no second constraint.

function workLoopSync () {
 // Execute as long as reconcile is not completed
 while(workInProgress ! = =null) {
   workInProgress = performUnitOfWork(workInProgress as Fiber)
 }
}
Copy the code

PerformUnitOfWork & beginWork

The main work of unit work is done by the beginWork. The core job of the beginWork is to determine whether the current fiber represents a class component, function component, or native component by judging fiber.tag, and do some special processing for them. This is all in preparation for the final step: manipulating the real DOM, which tells the commitRoot function behind it what to rewrite the real DOM by changing Fiber. EffectTag and pendingProps.

switch (workInProgress.tag) {
   // RootFiber
   case WorkTag.HostRoot:
     return updateHostRoot(current as Fiber, workInProgress, renderExpirationTime)
   / / class components
   case WorkTag.ClassComponent: {
     const Component = workInProgress.type
     const resolvedProps = workInProgress.pendingProps
     return updateClassComponent(
       current,
       workInProgress,
       Component,
       resolvedProps,
       renderExpirationTime
     )
   }
   ...
}
Copy the code

Here’s an example of how to build a Class component.

As mentioned earlier, for a class component, fiber.stateNode points to a previously constructed instance of the class.

// Update the Class component
function updateClassComponent (
 current: Fiber | null,
 workInProgress: Fiber,
 Component: any,
 nextProps,
 renderExpiration: number
) {
 // If the class component is rendered, the stateNode points to the class instance
 // Otherwise stateNode points to null
 const instance = workInProgress.stateNode
if (instance === null) {
   // If no class instance has been constructed. }else {
   // If the class instance is constructed. }// Complete the render build and reconcile the react element with the existing element
const nextUnitOfWork = finishClassComponent(
 current,
 workInProgress,
 Component,
 shouldUpdate,
 false,
 renderExpiration
)
return nextUnitOfWork
Copy the code

If fiber doesn’t build a class instance, its constructor is called and the updater is mounted to the class instance. (For handling setState logic, virtually all updates on class component instances are the same object, as discussed later)

if (instance === null) {
   // This class is rendered for the first time
 if(current ! = =null) {
   // Delete the pointer between current and WIP
   current.alternate = null
   workInProgress.alternate = null
   // Insert operations
   workInProgress.effectTag |= EffectTag.Placement
 }
 // Call the constructor to create a new class instance
 // Give the class instance a pointer to the updater
 constructClassInstance(
   workInProgress,
   Component,
   nextProps,
   renderExpiration
 )

 // Mount properties to class instances and trigger multiple lifecycles
 mountClassInstance(
   workInProgress,
   Component,
   nextProps,
   renderExpiration 
 )
}
Copy the code

If the instance already exists, you need to compare the old and new props and state to see if you need to update the component (if you wrote shouldComponentUpdate). It also triggers some update-time lifecycle hooks, such as getDerivedStateFromProps, and so on.

else {
   // Render already done, update
 shouldUpdate = updateClassInstance(
   current,
   workInProgress,
   Component,
   nextProps,
   renderExpiration
 )
}
Copy the code

Performed ReactElement is Performed by calling the render function of the class after the property is calculated. Performed ReactElement is Performed by performing the render function.

// Finish building the Class component
function finishClassComponent (
 current: Fiber | null,
 workInProgress: Fiber,
 Component: any,
 shouldUpdate: boolean,
 hasContext: boolean,
 renderExpiration: number
) {

   // Error boundary capture
 const didCaptureError = false

 if(! shouldUpdate && ! didCaptureError) {if (hasContext) {
     // Throw the question
     return bailoutOnAlreadyFinishedWork(
       current,
       workInProgress,
       renderExpiration
     )
   }
 }

 / / instance
 const instance = workInProgress.stateNode

 let nextChildren

 nextChildren = instance.render()

 // Mark as completed
 workInProgress.effectTag |= EffectTag.PerformedWork

 // Start reconciling
 reconcileChildren(
   current,
   workInProgress,
   nextChildren,
   renderExpiration
 )

 return workInProgress.child
}
Copy the code

Reconciliation process

If you remember, we just built the first fiberRoot node and the first meaningless empty root before everything started, In the reconcileSingleElement process of individual elements, corresponding fibers are constructed according to the ReactElement elements obtained by previous Render and inserted into the whole Fiber chain list.

Attach a Placement tag to the Fiber effectTag via placeSingleChild. Once you have the Placement tag, you’re done moving the Fiber pointer to the next node.

// Process object type (single node)
constisObjectType = isObject(newChild) && ! isNull(newChild)/ / object
if (isObjectType) {
 switch (newChild.$$typeof) {
   case REACT_ELEMENT_TYPE: {
     // At the end of the recursive harmonic, in the process of backtracking up
     // Attach a Placement Tag to the fiber node
     return placeSingleChild(
       reconcileSingleElement(
         returnFiber,
         currentFirstChild,
         newChild,
         expirationTime
       )
     )
   }
   // There are types such as Fragment}}// If the child element is a string or number, treat it as a literal node
// It is worth mentioning if the element's children are literal nodes
// Then the text will not be converted to fiber
// It is handled as prop for the parent element
if (isString(newChild) || isNumber(newChild)) {
 return placeSingleChild(
   reconcileSingleTextNode(
     returnFiber,
     currentFirstChild,
     ' ' + newChild,
     expirationTime
   )
 )
}

/ / array
if (isArray(newChild)) {
 return reconcileChildrenArray(
   returnFiber,
   currentFirstChild,
   newChild,
   expirationTime
 )
}
Copy the code

There is not enough space in this article to cover functional and native components. Assuming we have built and reconciled all the WIP, we need to insert a lot of DOM structures for the first build, but all we have so far are some virtual Fiber nodes.

So, the completeWork will be executed on the last cell work performUnitOfWork, and before that, our cell work was moving toward the fiber node at the end. In completeWork, our job will be bottom-up, generating real DOM structures from Fiber and stitching them together into a DOM tree as we go up.

export function completeWork (
 current: Fiber | null,
 workInProgress: Fiber,
 renderExpirationTime: number
) :Fiber | null {
 // Latest props
 const newProps = workInProgress.pendingProps

 switch (workInProgress.tag) {
   ...
   case WorkTag.HostComponent: {
     // pop the context of this fiber
     popHostContext(workInProgress)
     // Get the current DOM in the stack
     const rootContainerInstance = getRootHostContainer()

   		// Native component type
     const type = workInProgress.type

     if(current ! = =null&& workInProgress.stateNode ! = =null) {
       // If you are not rendering for the first time, you can try to update and reuse existing DOM nodes
       updateHostComponent(
         current,
         workInProgress,
         type as string,
         newProps,
         rootContainerInstance
       )
     } else {
       if(! newProps) {throw new Error('If there is no newProps, it is illegal')}const currentHostContext = getHostContext()

       // Create a native component
       let instance = createInstance(
         type as string,
         newProps,
         rootContainerInstance,
         currentHostContext,
         workInProgress
       )

       // Load all previously generated child DOM elements into the instance instance
   		  // Gradually splice into a DOM tree
       appendAllChildren(instance, workInProgress, false.false)
       
       // Fiber's stateNode points to the DOM structure
       workInProgress.stateNode = instance

       // feat: This function is really hidden, I don't know how they don't say a word →_→
       // finalizeInitialChildren is used to mount properties from props to a real DOM element, and the result is called as a judgment condition
       // Returns a bool indicating whether auto focus(input, textarea...) is required.
       if (finalizeInitialChildren(instance, type as string, newProps, rootContainerInstance, currentHostContext)) {
         markUpdate(workInProgress)
       }
     }
   }
 }
 
 return null
}
Copy the code

After construction, we get the relationship structure between virtual DOM and real DOM, parent element and child element as shown in the following figure

As of now, the reconcile work has been completed and we are in the ready to commit state to the document. From the start of the completeUnitOfWork build, the rest of the process has nothing to do with the timeslice, task scheduling system, and all the events, interactions, and asynchronous tasks will hold their breath and listen to what happens next in the DOM.

// Submit the root instance (DOM) to the browser's real container root
function commitRootImpl (root: FiberRoot, renderPriorityLevel: ReactPriorityLevel) {...// Since the entire component tree is mounted this time, the root fiber node will act as the fiberRoot finishedWork
   const finishedWork = root.finishedWork
 ...
 // The effect list, which is the native component fiber that will be inserted
 let firstEffect = finishedWork.firstEffect
   ...
   let nextEffect = firstEffect

   while(nextEffect ! = =null) {
   try {
     commitMutationEffects(root, renderPriorityLevel)
   } catch(err) {
     throw new Error(err)
   }
 }
}
Copy the code

There are actually two more iterations of the Effects linked list before the commitMutationEffects function, some lifecycle processing, such as getSnapshotBeforeUpdate, and preparation of variables.

// Actually rewrite the dom function in the document
// Submit fiber Effect
function commitMutationEffects (root: FiberRoot, renderPriorityLevel: number) {
 // @question The while statement seems redundant = =
 while(nextEffect ! = =null) {
   // The current fiber tag
   const effectTag = nextEffect.effectTag

   // The switch statement below handles only Placement,Deletion, and Update
   const primaryEffectTag = effectTag & (
     EffectTag.Placement |
     EffectTag.Update |
     EffectTag.Deletion | 
     EffectTag.Hydrating
   )
   switch (primaryEffectTag) {
     case EffectTag.Placement: {
       // Perform the insert
       commitPlacement(nextEffect)
       // effectTag after completing the real-name system, the corresponding effect should be removed
       nextEffect.effectTag &= ~EffectTag.Placement
     }
     case EffectTag.Update: {
       // Update existing DOM components
       const current = nextEffect.alternate
       commitWork(current, nextEffect)
     }
   }

   nextEffect = nextEffect.nextEffect
 }
}
Copy the code

As of this point, the first render has appeared on the screen. That is, the actual DOM no longer corresponds to the current fiber, but to the workInProgress Fiber we operate on, the finishedWork variable in the function.

// After the commit Mutation phase, the workInProgress tree is already a tree corresponding to the real Dom
// So the tree is still in the componentWillUnmount phase
// So at this point, workInProgress replaces current as the new current
root.current = finishedWork
Copy the code

One click event

If you’re a worker who uses React a lot, you’ll notice that events in React burn after reading. Consider this code:

import React, { MouseEvent } from 'react'

function TestPersist () {

   const handleClick = (
   	event: MouseEvent<HTMLElement, globalThis.MouseEvent>
   ) = > {
   setTimeout(() = > console.log('event', event))
 }

   return (
   	<div onClick={handleClick}>O2</div>)}Copy the code

If we need to asynchronously retrieve the position of the click event on the screen and handle it accordingly, then setTimeout will do the trick.

The answer is no, because React uses the event delegation mechanism. The event object we get is not the original nativeEvent, but the SyntheticEvent processed by React. This can be seen from ts type. The MouseEvent we use is imported from the React package rather than the global default event type. The event is destroyed in the React event pool immediately after the handleClick function completes synchronization.

React also provides a solution using asynchronous event objects. It provides a persist function that keeps events from entering the event pool. (React17 rewrote the composable event mechanism in order to resolve some issues, events are no longer represented by documents, composable events are no longer managed by event pools, and the persist function is no longer used)

So, why use event delegate. So, going back to the classic proposition, render 2 divs horizontally and vertically doesn’t matter, 1000 components and 2000 click events. The benefits of event delegation are:

  1. Simplifies the event registration process and optimizes performance.
  2. Dom elements are constantly updated, and you can’t guarantee that the div in the next frame will have the same address in memory as the div in the last frame. Since the events are not the same, they all have to be rebound, which is annoying.

Ok, back to the point. So what happens when we click on the event. The first is that the React render function executes the event injection automatically in the JS script before it executes.

Event injection

The process of event injection is a little more complicated, not only the order between modules, but also the data is processed a lot, so there is not too much detailed code here. Some people may ask why not just write dead, browser events are not so many billion points. Just as Redux wasn’t specifically for React, React wasn’t specifically for browsers. React is only a javascipt library. It can also serve native terminals, desktops, and other terminals. So it makes sense to inject the set of events dynamically depending on the underlying environment.

Of course, the injection process doesn’t matter. React arranges how each event is written in JSX in relation to the native event (such as onClick and onClick), and the priority of the event.

/* ReactDOM environment */

// Event plugin for DOM environment
const DOMEventPluginOrder = [
 'ResponderEventPlugin'.'SimpleEventPlugin'.'EnterLeaveEventPlugin'.'ChangeEventPlugin'.'SelectEventPlugin'.'BeforeInputEventPlugin',];InjectEventPluginOrder is automatically executed when this file is imported
// Determine the order in which plugins are registered, not actually introduced
EventPluginHub.injectEventPluginOrder(DOMEventPluginOrder)

// Actually inject the event content
EventPluginHub.injectEventPluginByName({
 SimpleEventPlugin: SimpleEventPlugin
})
Copy the code

Take SimpleEventPlugin as an example. Click events and other commonly used events belong to this plugin.

// Event tuple type
type EventTuple = [
 DOMTopLevelEventType, // React event type
 string.// The event name in the browser
 EventPriority         // Event priority
]

const eventTuples: EventTuple[] = [
 // Discrete events
 // discrete events generally refer to two consecutive events in the browser that are triggered at least 33ms apart (no basis, I guess)
 // For example, if you type the keyboard twice at the speed of light, the actual trigger timestamps of the two events will still be spaced
 [ DOMTopLevelEventTypes.TOP_BLUR, 'blur', DiscreteEvent ],
 [ DOMTopLevelEventTypes.TOP_CANCEL, 'cancel', DiscreteEvent ],
 [ DOMTopLevelEventTypes.TOP_CHANGE, 'change', DiscreteEvent ],
 [ DOMTopLevelEventTypes.TOP_CLICK, 'click', DiscreteEvent ],
 [ DOMTopLevelEventTypes.TOP_CLOSE, 'close', DiscreteEvent ],
 [ DOMTopLevelEventTypes.TOP_CONTEXT_MENU, 'contextMenu', DiscreteEvent ],
 [ DOMTopLevelEventTypes.TOP_COPY, 'copy', DiscreteEvent ],
 [ DOMTopLevelEventTypes.TOP_CUT, 'cut', DiscreteEvent ],
 [ DOMTopLevelEventTypes.TOP_DOUBLE_CLICK, 'doubleClick', DiscreteEvent ],
 [ DOMTopLevelEventTypes.TOP_AUX_CLICK, 'auxClick', DiscreteEvent ],
 [ DOMTopLevelEventTypes.TOP_FOCUS, 'focus', DiscreteEvent ],
 [ DOMTopLevelEventTypes.TOP_INPUT, 'input', DiscreteEvent ],
   ...
]
Copy the code

So, how are the listening events for these events registered? Remember that when you reconcile the Class component you calculate what DOM elements to insert into the browser or how to update them. During this process, the diffProperty function is used to diff the attributes of the element, where the listener function is added by ListenTo

As you all know, the listener that ends up being bound must have been modified by React and bound to document.

function trapEventForPluginEventSystem (
 element: Document | Element | Node,
 topLevelType: DOMTopLevelEventType,
 capture: boolean
) :void {
   // Generate a listener function
 let listener
 switch (getEventPriority(topLevelType)) {
   case DiscreteEvent: {
     listener = dispatchDiscreteEvent.bind(
       null,
       topLevelType,
       EventSystemFlags.PLUGIN_EVENT_SYSTEM
     )
     break}...default: {
     listener = dispatchEvent.bind(
       null,
       topLevelType,
       EventSystemFlags.PLUGIN_EVENT_SYSTEM
     )
   }
 }
 // @todo is converted with a getRawEventName
 // The function is →_→
 // const getRawEventName = a => a
 // Although this function does nothing
 // But its name implies this step semantically
 // The purpose is to get the valid name of the first argument to addEventListener in the browser environment
 const rawEventName = topLevelType
 // Mount the capture event listener to the root node
 // Both of these sections are designed to be compatible with the addEventListener that IE encapsulates
 if (capture) {
   // Register capture events
   addEventCaptureListener(element, rawEventName, listener)
 } else {
   // Register bubbling events
   addEventBubbleListener(element, rawEventName, listener)
 }
}
Copy the code

The third parameter to addEventListener controls the capture or bubbling process

Ok, right now, click on the page and the page calls this function. The first thing this function needs to do is to know which component is being clicked on. In fact, if there is anything wrong with React, the first step is always to find the fiber that needs to be responsible for.

First, we get the target DOM element, dom.target, via nativeEvent

const nativeEventTarget = getEventTarget(nativeEvent)
Copy the code
export default function getEventTarget(nativeEvent) {
 // Compatible
 let target = nativeEvent.target || nativeEvent.srcElement || window

 // Normalize SVG
 // @todo

 return target.nodeType === HtmlNodeType.TEXT_NODE ? target.parentNode : target
}
Copy the code

In fact, React will add an attribute to the DOM element that points to the corresponding fiber. I have a question about this approach, such mapping relationship can also be achieved by maintaining a WeekMap object, the performance of operating a WeakMap may be better than operating a DOM attribute, and the latter seems to be not elegant, if you have better ideas, please also point out in the comments section.

Whenever a new DOM is constructed for Fiber in the completeWork, the DOM is given a pointer to its fiber

/ / the random Key
const randomKey = Math.random().toString(36).slice(2)

// Random Key corresponds to the current instance Key
const internalInstanceKey = '__reactInternalInstance$' + randomKey
// Key corresponds to props after render
const internalEventHandlersKey = '__reactEventHandlers$' + randomKey
// Corresponding instance
const internalContianerInstanceKey = '__reactContainer$' + randomKey

// Bind operation
export function precacheFiberNode (
 hostInst: object,
 node: Document | Element | Node
) :void {
 node[internalInstanceKey] = hostInst
}

// Read operation
export function getClosestInstanceFromNode (targetNode) {
 let targetInst = targetNode[internalInstanceKey]
 // If there is no Key, return null
 if (targetInst) {
   return targetInst
 }
 
   // Some code is omitted
   // If internalInstanceKey is not found in the DOM
 // It looks up the parent node until it finds a DOM element with internalInstanceKey
 // This is why the function is called getting the nearest (Fiber) instance from Node.return null
}
Copy the code

Now that we have the native event object, the DOM that triggered the event, and fiber, we can get our bound onClick event from Fiber. MemorizedProps. This is enough information to generate an instance of the React synthesis event, ReactSyntheticEvent.

React declares a global variable eventQueue eventQueue. This queue is used to store all events triggered by an update. We need to enqueue the click event. And then trigger.

// Event queue
let eventQueue: ReactSyntheticEvent[] | ReactSyntheticEvent | null = null

export function runEventsInBatch (
 events: ReactSyntheticEvent[] | ReactSyntheticEvent | null
) {
 if(events ! = =null) {
   // If events exist, join the event queue

   // React is an accumulateInto function
   // Maybe it was written during ES3
   eventQueue = accumulateInto<ReactSyntheticEvent>(eventQueue, events)
 }

   const processingEventQueue = eventQueue

 // Clear the queue after execution
 // Even though these events have been released, they are still iterated
 eventQueue = null

 if(! processingEventQueue)return

   // Fire these events one by one

 // forEachAccumulated is a foreach implemented by React
 forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel)
}
Copy the code
// Triggers an event and immediately releases the event to the event pool unless presistent is executed
const executeDispatchesAndRelease = function (event: ReactSyntheticEvent) {
 if (event) {
   // All listeners bound to the event type are fired in sequence
   executeDispatchesInOrder(event)
 }

 // If persist is not executed, destroy the event immediately
 if(! event.isPersistent()) { (event.constructoras any).release(event)
 }
}
Copy the code

You can see that a function release is mounted on the constructor instance of the synthesized event to release the event. If we look at the SyntheticEvent code, we can see that the concept of an eventPool, eventPool, is used.

Object.assign(SyntheticEvent.prototype, {

   // Emulate the native preventDefault function
 preventDefault: function() {
   this.defaultPrevented = true;
   const event = this.nativeEvent;
   if(! event) {return;
   }

   if (event.preventDefault) {
     event.preventDefault();
   } else {
     event.returnValue = false;
   }
   this.isDefaultPrevented = functionThatReturnsTrue;
 },

 // Simulate native stopPropagation
 stopPropagation: function() {
   const event = this.nativeEvent;
   if(! event) {return;
   }

   if (event.stopPropagation) {
     event.stopPropagation();
   } else {
     event.cancelBubble = true;
   }

   this.isPropagationStopped = functionThatReturnsTrue;
 },

 /** * After each event loop, all synthesized events that have been dispatched are released * this function allows a reference event to be used without being collected by the GC */
 persist: function() {
   this.isPersistent = functionThatReturnsTrue;
 },

 /** * Whether this event will be collected by GC */
 isPersistent: functionThatReturnsFalse,

 /** * Destroy instance * to set all fields to NULL */
 destructor: function() {
   const Interface = this.constructor.Interface;
   for (const propName in Interface) {
     this[propName] = null;
   }
   this.dispatchConfig = null;
   this._targetInst = null;
   this.nativeEvent = null;
   this.isDefaultPrevented = functionThatReturnsFalse;
   this.isPropagationStopped = functionThatReturnsFalse;
   this._dispatchListeners = null;
   this._dispatchInstances = null; }});Copy the code

React adds an event pool property directly to the constructor, which is essentially an array that will be shared globally. Whenever an event is released, if the thread pool length does not exceed the specified size (default is 10), the destroyed event is added to the event pool

// Add static properties to the synthesized event constructor
// Event pool is shared by all instances
function addEventPoolingTo (EventConstructor) {
 EventConstructor.eventPool = []
 EventConstructor.getPooled = getPooledEvent
 EventConstructor.release = releasePooledEvent
}

// Release the event
// If the event pool has capacity, add it to the event pool
function releasePooledEvent (event) {
 const EventConstructor = this
 event.destructor()
 if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
   EventConstructor.eventPool.push(event)
 }
}
Copy the code

We all know the singleton pattern, which is that there is at most one instance of a class globally. The design of the event pool is similar to the n-instance pattern, in which the instance is returned to the constructor after each event is fired, and the clean instance is reused for each subsequent event, thus reducing the memory overhead.

// Take the event instance directly from the event pool when it is needed
function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) {
 const EventConstructor = this
 if (EventConstructor.eventPool.length) {
   // Fetch the last one from the event pool
   const instance = EventConstructor.eventPool.pop()
   EventConstructor.call(
     instance,
     dispatchConfig,
     targetInst,
     nativeEvent,
     nativeInst
   )
   return instance
 }
 return new EventConstructor (
   dispatchConfig,
   targetInst,
   nativeEvent,
   nativeInst
 )
}
Copy the code

If browser events are fired frequently over a short period of time, you will end up with instances in the event pool being taken out and reused, and subsequent synthetic event objects being literally recreated and recycled by the V8 engine’s GC at the end by dropping references.

Going back to the previous event firing, if you don’t specifically name the property onClickCapture, the bubbling process will be triggered by default. React simulates this process, which is triggered layer by layer through fiber, as well as the capture process.

We all know that the normal event trigger flow is:

  1. Event capture
  2. In the event
  3. The event bubbling

Being in the event phase is a try-catch statement, so that even if an error occurs, the React error-catching mechanism will remain in place. The function entity we really want to execute is fired here:

export default function invodeGuardedCallbackImpl<
 A.B.C.D.E.F.Context> (
 name: string | null,
 func: (a: A, b: B, c: C, d: D, e: E, f: F) => void, context? : Context, a? : A, b? : B, c? : C, d? : D, e? : E, f? : F,) :void {
 const funcArgs = Array.prototype.slice.call(arguments.3)
 try {
   func.apply(context, funcArgs)
 } catch (error) {
   this.onError(error)
 }
}
Copy the code

Classes and functions

When we use a class component or a function component, the end goal is to get a JSX that describes our page. Then there’s the question of how React can distinguish between function components and class components.

While it’s easy to see the difference between a Class and a function in ES6, it’s important to remember that we’re actually using Babel compiled code, and classes are syntactically sugar chains of functions and prototypes. Perhaps the most immediate thought for most people is that since the class component inherits from React.component, it should be possible to use the class type judgment directly:

App instanceof React.Component
Copy the code

Of course, what React does is add a logo to the prototype chain

Component.prototype.isReactComponent = {}
Copy the code

The isReactComponent property of a function can be read directly from the source code to determine whether it is a class component. If the function (also the Object) cannot be found by itself, the function will be searched up the prototype chain until the Object. Prototype Object is reached.

Why isReactComponent is an object and not a Boolean and why instanceOf can’t be used

Status update

Now that we’ve looked at React’s event delegate mechanism, try changing the component’s state in a one-click event to update our page.

First of all, we know that this.setState is a method in the react.componentclass:

/ * * *@description Update the component state *@param { object | Function } PartialState Indicates the status of the next phase *@param { ?Function } Callback */ after the callback is updated
Component.prototype.setState = function (partialState, callback) {
 if(! ( isObject(partialState) || isFunction(partialState) || isNull )) {console.warn('The first argument to setState should be an object, function, or NULL')
   return
 }
 this.updater.enqueueSetState(this, partialState, callback, 'setState')}Copy the code

It appears that the core step is to trigger a updater object mounted on the instance. By default, the updater is an empty object for a booth, and while methods such as enqueueSetState are implemented, they are internally empty.

// We initialize the default update, and the real updater is injected by the renderer
this.updater = updater || ReactNoopUpdateQueue
Copy the code
export const ReactNoopUpdateQueue = {
 /** * Check whether the component is already mounted */
 isMounted: function (publishInstance) {
   // Initialize ing components and do not mount
   return false
 },

 /** * Force update */
 enqueueForceUpdate: function (publishInstance, callback, callerName) {
   console.warn('enqueueForceUpdate', publishInstance)
 },

 /** * replaces the entire state directly, usually using this or setState to update the state */
 enqueueReplaceState: function (publishInstance, completeState, callback, callerName) {
   console.warn('enqueueReplaceState', publishInstance)
 },

 /** * change part of state */
 enqueueSetState: function (publishInstance, partialState, callback, callerName) {
   console.warn('enqueueSetState', publishInstance)
 }
}
Copy the code

Remember that we get an instance of a classComponent by executing component.render (). When React gets the instance, it replaces the updater with the actual classComponentUpdater:

function adoptClassInstance (
 workInProgress: Fiber,
 instance: any
) :void {
 instance.updater = classComponentUpdate
 ...
}
Copy the code

We have just triggered the enqueueSetState function in this object, so we can look at the implementation:

const classComponentUpdate = {
 isMounted,
 /** * Triggers a component state update *@param inst ReactElement
  * @param payload any
  * @param Callback the callback after the update completes */
 enqueueSetState(
   inst: ReactElement,
   payload: any, callback? :Function
 ) {
   // ReactElement -> fiber
   const fiber = getInstance(inst)
   // The current time
   const currentTime = requestCurrentTime()
   Get current suspense Config
   const suspenseConfig = requestCurrentSuspenseConfig()
   // Calculate the task expiration time of the current Fiber node
   const expirationTime = computeExpirationForFiber(
     currentTime,
     fiber,
     suspenseConfig
   )

   // Create an update instance
   const update = createUpdate(expirationTime, suspenseConfig)
   update.payload = payload
   // Load the update to fiber's queue
   enqueueUpdate(fiber, update)
   // Schedule tasks
   ScheduleWork(fiber, expirationTime)
 },
 ...
}
Copy the code

Obviously, this function gets the fiber component, updates its expiration date in the task scheduler, and then creates a new update task to load into the Fiber task queue. Finally, ScheduleWork requires that we start blending from this fiber, and the steps for blending and updating are already outlined in the first rendering.

By the way, useState in Hooks. There are a lot of articles on hook implementation on the web that have covered everything, so we just need to be clear about the following points.

Q1. Function components do not have instances like class components. Where is the data stored

A1. Any component with the granularity of ReactElement needs to be around Fiber, and data is stored on Fiber.memorizedState

Q2. Implementation of useState

A2. If you have heard useState you should have heard useReducer and if you have heard Reducer you should know redux. First, the essence of useState is useReducer’s syntactic sugar. We all know that building a state library requires a Reducer. UseState is a special case when reducer function is A => A.

function basicStateReducer<S> (state: S, action: BasicStateAction<S>) :S {
 return typeof action === 'function' ? action(state) : action
}

function updateState<S> (
 initialState: (() => S) | S
) :S.Dispatch<BasicStateAction<S>> ] {
 return updateReducer<S, (() = > S) | S, any>(basicStateReducer, initialState)
}
Copy the code

Q3. Why are the order and number of Hooks not allowed to change

A3. Each time the Hooks function is executed, it needs to retrieve the final state of the data from the last rendering. Because the structure is a linked list rather than a Map, these final states are also ordered, so if the number and order change, the data will be corrupted.

Time scheduling mechanism

Although expirationTime mechanism has been eliminated this year, both the waterway model and expirationTime are essentially different representations of task priority.

One question we need to know before exploring the mechanics is why time slices perform better than synchronous computations. Here is an example from teacher Situ zhengmei’s article.

In Experiment 1, 1000 nodes were inserted into document at a time through a for loop

function randomHexColor(){
   return "#" + ("0000"+ (Math.random() * 0x1000000 << 0).toString(16)).substr(-6);
}
setTimeout(function() {
   var k = 0;
   var root = document.getElementById("root");
   for(var i = 0; i < 10000; i++){
       k += new Date - 0 ;
       var el = document.createElement("div");
       el.innerHTML = k;
       root.appendChild(el);
       el.style.cssText =  background:${randomHexColor()};height:40px ;
   }
}, 1000);
Copy the code

In Experiment 2, 10 setTimeout operations were performed in batches, and 100 nodes were inserted each time

function randomHexColor() {
   return "#" + ("0000" + (Math.random() * 0x1000000 << 0).toString(16)).substr(-6);
}
var root = document.getElementById("root");
setTimeout(function () {
   function loop(n) {
       var k = 0;
       console.log(n);
       for (var i = 0; i < 100; i++) {
           k += new Date - 0;
           var el = document.createElement("div"); el.innerHTML = k; root.appendChild(el); el.style.cssText = background:${randomHexColor()}; height:40px ; }if (n) {
           setTimeout(function () {
               loop(n - 1);
           }, 40);
       }
   }
   loop(100);
}, 1000);
Copy the code

Same result, the first experiment took 1000 ms, while the second experiment only took 31.5 ms.

This is related to the underlying principles of the V8 engine. We all know that browsers are single-threaded, and when GUI rendering, event handling, JS execution, etc., needs to be done at once, the V8 engine prioritizes code execution rather than optimizing execution speed. If we give the browser a little time, the browser will be able to JIT, or hot code optimization.

Simply put, JS is an interpreted language, and every execution needs to be compiled into bytecode before it can be run. But if a function is executed multiple times, the type and number of arguments remain the same. This code is then identified as hot code, following the “everything can be space for time” principle, and the bytecode of this code is cached and run directly the next time it is run without time-consuming interpretation operations. This is the interpreter + compiler model.

To put it figuratively, we can’t work foolishly all the time. We must give ourselves some time to reflect and summarize. Otherwise, the speed and efficiency of work are always linear and people will not make progress.

Remember that in the WorkLoop function, every time a fiber is processed it breaks out of the loop and executes the shouldYield function once to determine whether the execution should be handed back to the browser to handle the user’s time or render. Look at the code for the shouldYield function:

// Whether the react work should be blocked currently
function shouldYield () :boolean {
 // Get the current point in time
 const currentTime = getCurrentTime()

 // Check whether any task needs to be executed in the task queue
 advanceTimers(currentTime)

 // Retrieve the task with the highest priority in the task queue
 const firstTask = peek(taskQueue)

 Yield is required in two cases
 // 1. There is a task in the current task queue, the start time of the first task has not arrived, and the expiration time is smaller than the current task
 // 2. Within a fixed browser rendering time interval
 return( ( currentTask ! = =null&& firstTask ! = =null &&
     (firstTask as any).startTime <= currentTime &&
     (firstTask as any).expirationTime < currentTask.expirationTime
   )
   // The time slice is blocked
   || shouldYieldToHost()
 )
}
Copy the code

There are two factors that determine whether a task should currently be executed.

  1. Whether this task must be carried out, is all the so-called whether it is not first asked why are playing rogue. If the due date is not up, why not free up the thread for possible high-priority tasks first.
  2. If multiple tasks must be executed, then whether the task has the highest priority in the current queue.

If a task is out of date and must be executed, it should be in a taskQueue. Instead, the task can be placed in the delay list before its expiration date. At the end of each frame, the advanceTimer function is executed to remove some tasks from the deferred list and insert them into the queue.

Perhaps as a matter of best practice, the pending queue is a small root heap structure, while the delay queue is an ordered linked list.

Recall that React’s task scheduling requirements require the ability to interrupt work and jump the queue when a new, higher-priority task is created. In other words, React needs to maintain an array data structure that is always ordered. React implements a small root heap itself, but the root heap doesn’t need to be as ordered as heap sorting. It just needs to ensure that the highest-priority tasks reach the top of the heap after each push and pop operation.

So a key condition for shouldYield to return true is that the current heap-top task in the taskQueue heap has expired and should be paused to give up thread access.

How is the task to be executed executed? Here we need to understand the concept of a MessageChannel. Message

An instance of a Channel has two ports, the first for sending messages and the second for receiving messages. The specified callback function can be executed when the message is received.

const channel = new MessageChannel()
/ / the sender
const port = channel.port2
/ / the receiving end
channel.port1.onmessage = performWorkUntilDeadline // Do as much work as possible in a given amount of time
Copy the code

Every time there is a task in the queue, an empty message is sent through the sender of a Channel. When the receiver asynchronously receives this signal, it will execute the task as much as possible within a time slice.

// Record the end of any time slice
let deadline = 0

// Slice length in unit time
let yieldInterval = 5

// Execute the task until the current slice of free time is exhausted
function performWorkUntilDeadline () {
 if(scheduledHostCallback ! = =null) {
   If you have a task planned, it needs to be carried out
   
   // The current time
   const currentTime = getCurrentTime()

   // Block after each time slice (5ms)
   // Deadline indicates the deadline of this time piece
   deadline = currentTime + yieldInterval

   // If this function can be executed, it means that there is time left
   const hasTimeRemaining = true

   try {
     // Schedule execution of the currently blocked task
     const hasMoreWork = scheduledHostCallback(
       hasTimeRemaining,
       currentTime
     )
     
     if(! hasMoreWork) {// If there are no more tasks, clear the data
       isMessageLoopRunning = false
       scheduledHostCallback = null
     } else {
       // If there are tasks, send a message event at the end of the current time slice
   			// When the receiver receives it, it will enter the next time slice
       port.postMessage(null)}}catch (error) {
     port.postMessage(null)
     throw(error)
   }
 } else {
   // There is no task at all
   isMessageLoopRunning = false}}Copy the code

We said earlier that the WorkLoop should block if the first task in the queue has not run out of time, and if shouldYieldToHost returns true, that is, if it is in a time slice.

// Whether this is the time slice blocking interval
export function shouldYieldToHost () {
 return getCurrentTime() >= deadline
}
Copy the code

To summarize, the scheduling mechanism is actually a process in which the fiber traversal task WorkLoop and the task queue in the scheduler compete for the right to use the thread. The difference, however, is that the former is completely synchronous and only asks the scheduler “can I continue?” during each while. For each slice of time the scheduler gets access to a thread, it processes as many tasks in the queue as possible.

Traditional martial arts focus on the point, the above content, this is all React principle. In the article I did not release a lot of code, just released some fragments to support my views and views on the source code, the process is just a step by step thinking process, if you need to see more details or should start from the source code.

Of course, many of the opinions in this article are subjective and not necessarily correct. Meanwhile, I don’t think that the opinions in other articles on the Internet are exactly the same as the original intention when React was designed, and even many of the ways of writing React source code are not perfect. No matter what code we read, we should not mythologize it, but look at it dialectically. Overall, it’s 91 points.

The front-end world doesn’t need a React second, and the point of learning isn’t to prove how much we know about the framework. Instead, we can improve our own logic system by looking into the implementation ideas of these top engineers, so as to become a more rigorous person.