Hit the React-Mini version of the code
Recently, I learned an article about the basic implementation of React principle. I have learned a lot and would like to write my personal gains and summary here.
0, JSX
const element = (
<input value="todo" />
)
Copy the code
Write the simplest JSX that we know will be converted to an Object, as shown below
const element = {
type: 'input'.props: {
value: 'todo'.children: [].}}Copy the code
1, the createElement method
How do you convert JSX into an Object? So I need to write a function called createElement to do that
function createElement(type, props, ... children) {
return {
type,
props: {
...props,
children
}
}
}
const element = createElement(
'input',
{
value: 'todo'})Copy the code
But what about JSX with nested child elements, for example
const element = (
<div id="foo">
<b />
<b />
</div>
)
Copy the code
So it’s going to be the following
/* @return { type: 'div', props: { id: 'foo', children: [ { type: 'b', props: { children: [] } }, { type: 'b', props: { children: [] } }, ] } } * / const element = createElement( 'div', { id: 'foo' }, createElement('b'), createElement('b'), )Copy the code
There is, of course, a special case where the child element contains text
const element = (
<div id="foo">
<b />
bar
</div>
)
Copy the code
So call time is zero
/* @return { type: 'div', props: { id: 'foo', children: [ { type: 'b', props: { children: [] } }, 'bar' ] } } */
const element = createElement(
'div',
{
id: 'foo'
},
createElement('b'),
'bar'
)
Copy the code
As you can see, the children array in div contains an object and a string ‘bar’. For further processing, we need to convert the text element to an object we agreed on, as follows: createElement
function createTextElement(text) {
return {
type: 'TEXT_ELEMENT'.props: {
nodeValue: text,
children: [],}}}function createElement(type, props, ... children) {
return {
type,
props: {
...props,
children: children.map(child= >
typeof child === 'object'
? child
: createTextElement(child)
)
}
}
}
Copy the code
Eventually convert to
{
"type": "div"."props": {
"id": "foo"."children": [{"type": "b"."props": { "children": []}}, {"type": "TEXT_ELEMENT"."props": { "nodeValue": "bar"."children": []}}]}}Copy the code
2, render
Now that we have the JSX object, we need to call reactdom. render to render the page container
const container = document.getElementById('root')
ReactDOM.render(element, container)
Copy the code
Render method basic implementation, as follows
function render(element, container) {
const dom =
element.type == 'TEXT_ELEMENT'
? document.createTextNode(' ')
: document.createElement(element.type)
// All properties on element.props, except children, are dom node properties
const isProperty = key= >key ! = ='children'
// Add attributes to the DOM
Object.keys(element.props)
.filter(isProperty)
.forEach(name= > {
dom[name] = element.props[name]
})
// Recursively iterate over the child elements in the DOM
element.props.children.forEach(child= >
render(child, dom)
)
container.appendChild(dom)
}
const ReactDOM = {
render
}
Copy the code
3, concurrent mode
As of the above, we can write the JSX and give it to createElemnt to convert to a JS object and finally to Render.
Render uses recursive traversal above, which can be a bit problematic. Once render is executed, the render function does not end until the DOM tree is rendered. If the DOM tree is very large, it might block the main thread for too long. If the browser needs to handle high-priority work such as user input or smooth animations, it must wait until the rendering is complete.
React uses idle time (which does not affect the delay of critical events such as animations and input responses) to build the virtual DOM tree. Finally, the entire virtual DOM tree is built before rendering. How does React allow free time for virtual DOM builds?
React has deprecated the requestIdleCallback method, however, due to some issues with the API (not executing frequently enough for smooth UI rendering, compatibility, etc.).
Github.com/facebook/re…
Github.com/hushicai/hu…
However, we can still use this API to implement the Concurrent mode functionality of the simple React version. Below, we call requestIdleCallback over and over again, executing tasks as soon as we find that there is enough free time in each frame and that there are still unfinished tasks.
let nextUnitOfWork = null
function workLoop(deadline) {
// Whether to stop
let shouldYield = false
while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork)// Stop if the idle time is less than 1ms
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork() {
// ...
}
Copy the code
4. Fiber model
Let’s move on to the Fiber model. Let’s say you have a piece of JSX
const element = (
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>
)
Copy the code
The createElement method used previously would have converted each element in JSX to an object with attributes like Type, children, and so on. Based on this, we are going to enrich these objects and design them as fiber models. Rules are
- Every element has an attribute that points to its parent (except the root element)
- Each element has an attribute that points to its first child (if it has a son)
- Each element has an attribute that points to its adjacent sibling element (if any)
The Render method in Section 2 is a one-time recursive render from the root node. Now we must optimize the, add each element to add attributes and child elements of the operations are divided into individual tasks, use idle time to perform a task, when there is no free time, record the current task and stop execution, continue to the next free time, until to complete all the tasks, then rendering task can be achieved. Also, there’s a key function performUnitOfWork that needs to be implemented in the last section
function performUnitOfWork(fiber) {
if(! fiber.dom) {// Create the dom fragment corresponding to fiber
fiber.dom = createDom(fiber)
}
const elements = fiber.props.children
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
// point to the parent element
parent: fiber,
dom: null,}// The parent element only needs to refer to its first child
if (index === 0) {
fiber.child = newFiber
} else {
// Each element needs to refer to its next sibling
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
// If there is a next sibling element, otherwise the parent element's next sibling element will be looked up
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
function createDom(fiber) {
const dom =
fiber.type === 'TEXT_ELEMENT'
? document.createTextNode(' ')
: document.createElement(fiber.type)
const isProperty = key= >key ! = ='children'
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name= > {
dom[name] = fiber.props[name]
})
return dom
}
Copy the code
So the render function is changed to
let nextUnitOfWork = null
function workLoop(deadline) {
// Whether to stop
let shouldYield = false
while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork) shouldYield = deadline.timeRemaining() <1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function render(element, container) {
// start render, nextUnitOfWork from the root node
nextUnitOfWork = {
dom: container,
props: {
children: [element],
}
}
}
const ReactDOM = {
createElement,
render
}
Copy the code
5, Render and commit phases
The last section talked about using free time to execute each unit task in order to create the Fiber model, so when the entire Fiber virtual tree is built, it’s time to actually render the DOM.
So the question is, how do we know when the virtual tree is built and when we’re ready to render it?
Starting with a review of the Fiber model and the performUnitOfWork function, you can see that the process of building a virtual tree is similar to DFS (depth-first traversal). It traverses from the root to the bottom and back again until it returns to the root. Since the root node has no younger and parent elements, the build is complete (performUnitOfWork() returns undefined).
At this point we’ll change the render function again and declare a variable wipRoot to store the root node of the virtual tree under construction.
let nextUnitOfWork = null
// Record the root node
let wipRoot = null
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
}
nextUnitOfWork = wipRoot
}
Copy the code
In concurrent mode 3, we learned that React-Mini would call requestIdleCallback repeatedly to implement the sharding task, so this is where we determine whether the tree is complete and decide to start rendering.
function workLoop(deadline) {
let shouldYield = false
while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() <1
}
if(! nextUnitOfWork && wipRoot) { commitRoot() } requestIdleCallback(workLoop) } requestIdleCallback(workLoop)Copy the code
The commitRoot function is simple
function commitRoot() {
commitWork(wipRoot.child)
// Reset the root node
wipRoot = null
}
function commitWork(fiber) {
if(! fiber) {return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
Copy the code
6, Reconciliation
At this point, we’ve done our initial rendering work. What about updating the render again? We need to consider updating the DOM and deleting the DOM.
First of all, we don’t need to completely build a new DOM tree to render again, we can compare the current virtual tree with the virtual tree to render, and only need to change or delete elements to minimize unnecessary rendering.
Therefore, we need a variable to record the current virtual tree and establish a relationship with the virtual tree to be rendered
let nextUnitOfWork = null
// Current virtual root node
let currentRoot = null
// Root node of the virtual tree in progress
let wipRoot = null
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
// Build relationships
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
}
function commitRoot() {
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
Copy the code
So the comparison process is in formUnitofwork, and we need to change that
function performUnitOfWork(fiber) {
if(! fiber.dom) { fiber.dom = createDom(fiber) }const elements = fiber.props.children
reconcileChildren(fiber, elements)
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
Copy the code
There is a function for reconcileChildren, whose main function is fiber contrast.
// oldFiber: div --> div --> div --> div
// elements: div --> div --> p
Update update add delete
Copy the code
Here is a comparison of the old Fiber and the new JSX in position order.
- Update an element of the same type in the same position.
- If the element type is different but the location is the same, add is added.
- If the old fiber has elements in the same position but the new JSX has no elements, delete is deleted.
Take the example above. React does not compare keys, but compares them in order of position. React adds keys to elements. React.docschina.org/docs/lists-…
function reconcileChildren(wipFiber, elements) {
let index = 0
let oldFiber = wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null
while( index < elements.length || oldFiber ! =null
) {
const element = elements[index]
let newFiber = null
// Check whether the old element and the new element are of the same type
const sameType = oldFiber && element && element.type == oldFiber.type
if (sameType) {
// update
}
if(element && ! sameType) {// add
}
if(oldFiber && ! sameType) {// delete
}
if (oldFiber) {
oldFiber = oldFiber.sibling
}
if (index === 0) {
wipFiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
Copy the code
Then how to set the new newFiber specifically? We added an extra field effectTag to indicate which newFiber was added, deleted or modified, which was convenient for judging in the subsequent rendering task.
if (sameType) {
// update
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: 'UPDATE',}}if(element && ! sameType) {// add
newFiber = {
type: element.type,
props: element.props,
dom: null.parent: wipFiber,
alternate: null.effectTag: 'PLACEMENT',}}if(oldFiber && ! sameType) {// delete
oldFiber.effectTag = 'DELETION'
deletions.push(oldFiber)
}
Copy the code
Since the old fibers that need to be deleted do not need to be put back into the virtual tree, they are stored separately with deletions array variables, and the corresponding DOM is removed through the number group during subsequent rendering.
Deletions also need to go into other functions.
let nextUnitOfWork = null
// Current virtual root node
let currentRoot = null
// Root node of the virtual tree in progress
let wipRoot = null
// The node to be deleted
let deletions = null
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
// Reset the node to be deleted
deletions = []
nextUnitOfWork = wipRoot
}
function commitRoot() {
// Iterate over the unmount node
deletions.forEach(commitWork)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
Copy the code
Now that fiber comparison is over, it’s time for rendering. We need to modify the commitWork function to add, delete, and modify fibers with different effectTags.
function commitWork(fiber) {
if(! fiber) {return
}
const domParent = fiber.parent.dom
if (fiber.effectTag === 'PLACEMENT'&& fiber.dom ! =null) {
domParent.appendChild(fiber.dom)
} else if (fiber.effectTag === 'UPDATE'&& fiber.dom ! =null) {
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
} else if (fiber.effectTag === 'DELETION') {
domParent.removeChild(fiber.dom)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
Copy the code
A new function, updateDom, is used to add, delete, or modify attributes on the DOM.
const isEvent = key= > key.startsWith('on')
const isProperty = key= >key ! = ='children' && !isEvent(key)
const isNew = (prev, next) = > key= >prev[key] ! == next[key]const isGone = (prev, next) = > key= >! (keyin next)
function updateDom(dom, prevProps, nextProps) {
// Remove old or changed event listeners
Object.keys(prevProps)
.filter(isEvent)
.filter(
key= >! (keyin nextProps) || isNew(prevProps, nextProps)(key)
)
.forEach(name= > {
const eventType = name.toLowerCase().substring(2)
dom.removeEventListener(
eventType,
prevProps[name]
)
})
// Delete the old attributes
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name= > {
dom[name] = ' '
})
// Sets new or changed properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name= > {
dom[name] = nextProps[name]
})
// Add event listener
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name= > {
const eventType = name.toLowerCase().substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
}
Copy the code
Since the createDom function did not allow for event listening, change it
function createDom(fiber) {
const dom =
fiber.type == 'TEXT_ELEMENT'
? document.createTextNode(' ')
: document.createElement(fiber.type)
updateDom(dom, {}, fiber.props)
return dom
}
Copy the code
The React-Mini’s main features are now complete. The latter is a supplement.
7, the Function of Components
Let’s go back to the createElement section
// write JSX
const element = (
<input value="todo" />
)
// convert JSX
const element = createElement(
'input',
{
value: 'todo'})// 3. Convert JSX to object
const element = {
type: 'input'.props: {
value: 'todo'.children: [].}}Copy the code
What if you write JSX as a function component
function App(props) {
return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
Copy the code
According to the previous rules, it will also be converted
function App(props) {
return Didact.createElement(
'h1'.null.'Hi ',
props.name
)
}
const element = Didact.createElement(App, {
name: 'foo',})const element = {
type: App,
props: {
name: 'foo'.children: [{type: 'h1'.props: {
children: [{type: 'TEXT_ELEMENT'.props: { 'nodeValue': 'Hi '.'children': []}}, {type: 'TEXT_ELEMENT'.props: { 'nodeValue': 'foo'.'children': []}}]}}Copy the code
What’s special here is that element’s attribute type is no longer a string of tag types, but a function. So the previous code needs to be changed again.
The first is the formUnitofwork function
function performUnitOfWork(fiber) {
// if (! fiber.dom) {
// fiber.dom = createDom(fiber)
// }
// const elements = fiber.props.children
// reconcileChildren(fiber, elements)
const isFunctionComponent = fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
Copy the code
This is handled differently depending on the type attribute
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function updateHostComponent(fiber) {
if(! fiber.dom) { fiber.dom = createDom(fiber) }const elements = fiber.props.children
reconcileChildren(fiber, elements)
}
Copy the code
Then think about the features of function components, such as the following
function App() {
return (
<span>foo</span>)}const element = (
<div id="root">
<App />
</div>
)
/ / fiber model
div --> App --> span
// Final render
<div>
<span>foo</span>
</div>
Copy the code
In fact, there is no DOM corresponding to the Fiber node on the App layer, so the SPAN tag should cross the App node and be rendered as a child of the DIV tag. So review commitWork
function commitWork(fiber) {
if(! fiber) {return
}
// We can't find parent-dom directly, but we can find parent-.parent-dom, or the next level
// const domParent = fiber.parent.dom
// Find the parent node of the DOM
let domParentFiber = fiber.parent
while(! domParentFiber.dom) { domParentFiber = domParentFiber.parent }const domParent = domParentFiber.dom
// ...
}
Copy the code
The complete commitWork is below
function commitWork(fiber) {
if(! fiber) {return
}
let domParentFiber = fiber.parent
while(! domParentFiber.dom) { domParentFiber = domParentFiber.parent }const domParent = domParentFiber.dom
if (fiber.effectTag === 'PLACEMENT'&& fiber.dom ! =null) {
domParent.appendChild(fiber.dom)
} else if (fiber.effectTag === 'UPDATE'&& fiber.dom ! =null) {
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
} else if (fiber.effectTag === 'DELETION') {
commitDeletion(fiber, domParent)
}
if(fiber.effectTag ! = ='DELETION') {
commitWork(fiber.child)
commitWork(fiber.sibling)
}
}
Copy the code
The same is true when deleting nodes
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}
Copy the code
8 Hooks.
Since hooks came out, there have been joys and sorrows. Happy people find it easy to use hooks, as in the simple example below
function Counter() {
const [state, setState] = useState(1)
return (
<button onClick={()= > setState(c => c + 1)}>
Count: {state}
</button>)}Copy the code
But how exactly do hooks work? We need to start where we created the function component, which was the updateFunctionComponent function
// Fiber under construction
let wipFiber = null
// Record the hook execution position
let hookIndex = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
Copy the code
As you can see, we declared two variables, wipFiber and hookIndex, to store the fiber under construction and record the hook position, respectively.
Here is the implementation of useState
function useState(inital) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : inital,
queue: [],}// Fiber stores hook state on the node
wipFiber.hooks.push(hook)
// Hook position forward
hookIndex++
return [hook.state]
}
Copy the code
Here a hook uses an array to store state, and each time a hook is used, the array moves forward. Therefore, the position of the old hook and the new hook should be placed one by one, so that the new hook can accurately depend on the state of the old hook. Thus is easy to understand why the React to the rules of using hook react.docschina.org/docs/hooks-…
React actually uses linked lists to store hook states, but arrays are used for convenience.
Currently only state is returned using useState, and setState needs to be added
function useState(inital) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : inital,
queue: [],}const actions = oldHook ? oldHook.queue : []
actions.forEach(action= > {
const isFunction = action instanceof Function
/ / update the state
hook.state = isFunction ? action(hook.state) : action
})
const setState = action= > {
hook.queue.push(action)
// Trigger virtual tree building and rendering like the render function
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
deletions = []
}
// Fiber stores hook state on the node
wipFiber.hooks.push(hook)
// Hook position forward
hookIndex++
return [hook.state, setState]
}
Copy the code
React Mini is now ready!
Full code github.com/Zeng-J/reac…
conclusion
After learning React, I got to know some basic concepts. However, React min has a lot of details to improve, such as not considering key comparison, not considering recycling old fiber, etc. Anyway, with the understanding of the basic concept, the follow-up liver source code is a little less laborious.
The article study
Pomb. Us/build – your -…