一. 调试项目
基于该目录结构和代码分析React
渲染流程
1.1 目录结构
bash
root
|- src
|- App.jsx
|- index.jsx
1.2 代码示例
1.2.1 index.jsx
javascript
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
const root = createRoot(document.getElementById('app'))
root.render(<App />)
1.2.2 App.jsx
javascript
function HelloWorld() {
return <h1>hello world</h1>
}
function App() {
const [visible, setVisible] = useState(true)
return (
<div>
<h1 onClick={() => setVisible(!visible)}>Hello App</h1>
{visible && <HelloWorld />}
</div>
)
}
二. 整体流程
2.1 首次渲染
在首次渲染,需要从零到一构建整棵虚拟DOM
树和真实DOM
树,然后将构建好的DOM
树插入到指定节点上,完成渲染
首次构建的FiberNode Tree
和DOM
树结构如图所示,父FiberNode
节点通过child
属性指向子FiberNode
节点、子FiberNode
通过return
属性指向父FiberNode
节点,FiberNode
节点通过sibling
属性指向兄弟FiberNode
节点,FiberNode
节点通过stateNode
属性指向DOM
节点。
2.1.1 构建虚拟DOM
树
- 创建
FiberRootNode
节点 - 创建
App Component
对应的ReactElement
对象 - 创建根
FiberNode
节点 - 创建根
FiberNode
副本节点并赋值给workInProgess
,通过alternate
属性相互引用 - 递归遍历
FiberNode
节点,创建ReactElement
对象对应的FiberNode
节点,建立父子,兄弟关联关系,构建FiberNode Tree
2.1.2 构建DOM
树
- 创建
FiberNode
节点对应的DOM
节点 - 递归父
FiberNode
节点创建对应的DOM
节点 - 将子
FiberNode
节点的DOM
插入到父FiberNode
节点的DOM
,构建DOM
树
2.1.3 更新DOM
首次更新DOM
逻辑比较简单,即调用insertOrAppendPlacementNodeIntoContainer
方法将构建好的DOM
树插入到指定节点
2.2 更新渲染
在更新渲染过程中会构建新的FiberNode Tree
,新旧FiberNode
节点会通过alternate
属性相互引用,对于要删除的旧FiberNode
节点,会存储在父FiberNode
节点的deletions
属性中
2.2.1 构建新FiberNode Tree
- 获取旧根
FiberNode
副本节点,复制旧根FiberNode
节点属性值 - 创建
App FiberNode
副本节点 - 调用
App Component
方法获取新ReactElement
对象 - 递归遍历新
ReactElement
对象,与旧FiberNode
节点比对key
和elementType
是否相同,相同则创建旧FiberNode
副本节点,不同则创建新FiberNode
节点,将不能复用的FiberNode
节点添加到父FiberNode
及节点的deletions
属性中,构建FiberNode Tree
2.2.2 构建DOM
树
与首次渲染时不同点在于只会构建不存在旧FiberNodeTree
的新FiberNode
节点的DOM
树,新FiberNode
节点的DOM
树构建流程则和首次渲染构建DOM
树是一样的
2.2.3 更新DOM
递归遍历FiberNode Tree
节点,遍历deletions
属性中的FiberNode
节点,将其对应的DOM
节点从DOM
树中移除,对于新增的FiberNode
节点则插入到DOM
树中,完成更新渲染
三. 核心代码
3.1 首次渲染 - 构建FiberNode Tree
3.1.1 ReactDOM.createRoot
- 创建
FiberRootNode
节点 - 创建
ReactDOMRoot
对象,将FiberRootNode
节点赋值给_internalRoot
属性
javascript
function createRoot(container, options) {
// 创建FiberRootNode节点
const root = createContainer()
// 创建ReactDOMRoot对象,将FiberRootNode节点赋值给_internalRoot属性
return new ReactDOMRoot(root)
}
3.1.2 React.createElement
jsx
是react
提供的语法糖,经过babel
转换处理之后会变成React.createElement
方法调用,例如<App />
会转成React.createElement(function App() {}, null)
,创建App Component
对应的ReactElement
对象
javascript
function createElement(type, config, children) {
return ReactElement()
}
3.1.3 ReactDOM.render
- 获取
FiberRootNode
节点 - 构建
FiberNode Tree
和DOM
树,将DOM
树插入指定节点,完成渲染
javascript
ReactDOMRoot.prototype.render = function(children) {
// 获取FiberRootNode节点
const root = this._internalRoot
// 构建FiberNode Tree和DOM树,将DOM树插入指定节点,完成渲染
updateContainer(children, root, null, null)
}
3.1.4 react-reconciler prepareFreshStack
- 将
FiberRootNode
节点赋值给workInProgressRoot
- 创建根
FiberNode
副本节点并赋值给workInProgress
,通过alternate
属性相互引用
javascript
function prepareFreshStack(root, lanes) {
// 将FiberRootNode节点赋值给workInProgressRoot
workInProgressRoot = root
// 创建根FiberNode副本节点,通过alternate属性相互引用
const rootWorkInProgress = createWorkInProgress(root.current, null)
// 将根FiberNode副本节点赋值给workInProgress
workInProgress = rootWorkInProgress
return rootWorkInProgress
}
3.1.5 react-reconciler workLoopSync
递归遍历FiberNode
节点,创建ReactElement
对象对应的FiberNode
节点,建立父子、兄弟关联关系,构建FiberNode Tree
javascript
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress)
}
}
需要注意的是采用深度优先遍历算法 ,即递归遍历到第一个分支叶子节点后返回遍历下一个分支,例如下面这个示例,A
和B
组件平级,A
嵌套C
组件,那会先递归遍历A -> C
这个分支,然后返回遍历B
javascript
function C() {
console.log('C')
return <h1>hello C</h1>
}
function A() {
console.log('A')
return (
<div>
<h1>hello A</h1>
<C />
</div>
)
}
function B() {
console.log('B')
return <h1>hello B</h1>
}
function App() {
return (
<div>
<A />
<B />
</div>
)
}
// 控制台输出结果是A -> C -> B
3.1.6 react-reconciler performUnitOfWork
创建ReactElement
对象对应的FiberNode
节点,并将第一个child FiberNode
节点并赋值给workInProgress
,当遍历到分支叶子FiberNode
节点,则调用completeUnitOfWork
方法创建该FiberNode
节点对应的DOM
节点
javascript
function performUnitOfWork(unitOfWork) {
const current = unitOfWork.alternate
let next
// 创建ReactElement对象对应的FiberNode节点,返回第一个child FiberNode节点
next = beginWork(current, unitOfWork, entangledRenderLanes)
if (next === null) {
// 当前分支已遍历到叶子节点,创建该节点对应的DOM节点
completeUnitOfWork(unitOfWork)
} else {
// 将新的FiberNode节点赋值给workInProgress
workInProgress = next
}
}
3.1.7 react-reconciler beginWork
根据FiberNode tag
类型调用对应的处理逻辑,创建新的FiberNode
节点。
HostRoot
即根FiberNode
节点对应处理逻辑,参考3.1.7.1
小节FunctionComponent
即函数组件对应处理逻辑,如App Component
,参考3.1.7.2
小节HostComponent
即元素标签对应处理逻辑,如h1
,参考3.1.7.3
小节
javascript
function beginWork(current, workInProgress, renderLanes) {
switch (workInProgress.tag) {
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes)
case FunctionComponent:
return updateFunctionComponent(current, workInProgress, Component, resolvedProps, renderLanes)
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes)
}
}
3.1.7.1 react-reconciler updateHostRoot
- 获取
App Component
对应的ReactElement
对象 - 创建
ReactElement
对象对应的FiberNode
节点,建立父子关联关系
javascript
function updateHostRoot(current, workInProgress, renderLanes) {
const nextState = workInProgress.memoizedState
// 获取App Component对应的ReactElement对象
const nextChildren = nextState.element
// 创建ReactElement对象对应的FiberNode节点,新节点的return属性会指向根FiberNode节点,根FiberNode节点的child属性会指向新节点
reconcileChildren(current, workInProgress, nextChildren, renderLanes)
// 返回新的FiberNode节点
return workInProgress.child
}
3.1.7.2 react-reconciler updateFunctionComponent
- 调用
Function Component
方法,获取返回的ReactElement
对象 - 创建
ReactElement
对象对应的FiberNode
节点,建立父子关联关系
javascript
function updateFunctionComponent(current, workInProgress, Component, nextProps, renderLanes) {
let nextChildren
// 调用Function Component方法,获取返回的ReactElement对象
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
)
// 创建ReactElement对象对应的FiberNode节点,建立父子关联关系
reconcileChildren(current, workInProgress, nextChildren, renderLanes)
return workInProgress.child
}
3.1.7.3 react-reconciler updateHostComponent
- 获取元素标签的
child ReactElement
对象 - 创建
child ReactElement
对象对应的FiberNode
节点,建立父子,兄弟关联关系,返回第一个child FiberNode
节点
javascript
function updateHostComponent(current, workInProgress, renderLanes) {
const type = workInProgress.type
// 获取child ReactElement对象
let nextChildren = nextProps.children
// 创建child ReactElement对象对应的FiberNode节点,建立父子,兄弟关联关系,返回第一个child FiberNode节点
reconcileChildren(current, workInProgress, nextChildren, renderLanes)
return workInProgress.child
}
3.1.8 react-reconciler reconcileChildren
current
为null
,说明该FiberNode
节点不存在FiberNode Tree
上,属于新增节点,不需要处理副作用,可以优化性能
javascript
// 是否需要处理副作用
const reconcileChildFibers = createChildReconciler(true)
const mountChildFibers = createChildReconciler(false)
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
if (current === null) {
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes)
} else {
workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes)
}
}
3.1.9 react-reconciler reconcileChildFibersImpl
- 只有一个
child ReactElement
对象,则调用reconcileSingleElement
方法创建对应的FiberNode
节点,参考3.1.9.1
小节 - 有多个
child ReactElement
对象,则调用reconcileChildrenArray
方法遍历child ReactElement
对象创建对应的FiberNode
节点,建立节点兄弟关联关系,并返回第一个child FiberNode
节点,参考3.1.9.2
小节
javascript
function reconcileChildFibersImpl(returnFiber, currentFirstChild, newChild, lanes) {
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
// 单个child ReactElement对象
const firstChild = placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
)
return firstChild;
}
}
// 多个child ReactElement对象
if (isArray(newChild)) {
const firstChild = reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
)
return firstChild;
}
}
}
3.1.9.1 react-reconclier reconcileSingleElement
javascript
function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
const created = createFiberFromElement(element, returnFiber.mode, lanes)
created.return = returnFiber
return created
}
3.1.9.2 react-reconciler reconcileChildrenArray
遍历child ReactElement
对象,创建对应的FiberNode
节点,并建立父子、兄弟关联关系,返回第一个child FiberNode
节点
javascript
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes) {
// 第一个child FiberNode节点
let resultingFirstChild = null
// 上一个兄弟FiberNode节点
let previousNewFiber = null
let oldFiber = currentFirstChild
let newIdx = 0
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes)
if (previousNewFiber === null) {
resultingFirstChild = newFiber
} else {
// 建立FiberNode节点兄弟关联关系
previousNewFiber.sibling = newFiber
}
previousNewFiber = newFiber
}
}
return resultingFirstChild
}
3.2 首次渲染 - 构建DOM
树
3.2.1 react-reconciler completeUnitOfWork
在构建FiberNode Tree
树过程中,遍历到分支叶子FiberNode
节点时会调用此方法,创建该FiberNode
节点对应的DOM
节点。
如果该FiberNode
节点有兄弟节点,则调用performUnitOfWork
方法继续构建FiberNode Tree
树。
如果没有兄弟节点,则递归父FiberNode
节点,创建父FiberNode
节点对应的DOM
节点,同时遍历子FiberNode
节点,将子DOM
节点append
到父DOM
节点中,构建DOM
树
javascript
function completeUnitOfWork(unitOfWork) {
let completedWork = unitOfWork
do {
const current = completedWork.alternate
const returnFiber = completedWork.return
let next
// 创建FiberNode节点对应的DOM节点,构建DOM树
next = completeWork(current, completedWork, entangledRenderLanes)
if (next !== null) {
workInProgress = next
return
}
// 有兄弟FiberNode节点则调用performUnitOfWork方法
const siblingFiber = completedWork.sibling
if (siblingFiber !== null) {
workInProgress = siblingFiber
return
}
// 递归父FiberNode节点
completedWork = returnFiber
workInProgress = completedWork
} whilte (completedWork !== null)
}
3.2.2 react-reconciler completeWork
这里我们重点关注tag
属性值为HostCompoent
的FiberNode
节点
- 创建
FiberNode
节点对应的DOM
节点 - 遍历子
FiberNode
节点,将子DOM
节点append
到当前DOM
节点上 - 将
DOM
节点赋值给FiberNode
节点的stateNode
属性
javascript
function completeWork(current, workInProgress, renderLanes) {
switch (workInProgress.tag) {
case HostRoot:
bubbleProperties(workInProgress)
return null
case FunctionComponent:
bubbleProperties(workInProgress)
reutrn null
case HostComponent:
const type = workInProgress.type
// 创建FiberNode节点对应的DOM节点
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
)
// 遍历子FiberNode节点,将子DOM节点append到当前DOM节点上
appendAllChildren(instance, workInProgress, false, false)
// 将DOM节点赋值给FiberNode节点的stateNode属性
workInProgress.stateNode = instance
return null
}
}
3.3 首次渲染 - 更新DOM
3.3.1 react-reconciler insertOrAppendPlacementNodeIntoContainer
找到第一个tag
属性值为HostComponent
的FiberNode
节点,将对应的DOM
节点插入到指定节点
javascript
function insertOrAppendPlacementNodeIntoContainer(node, before, parent) {
const { tag } = node
const isHost = tag === HostComponent || tag === HostText
if (isHost) {
const stateNode = node.stateNode
if (before) {
insertInContainerBefore(parent, stateNode, before);
} else {
appendChildToContainer(parent, stateNode);
}
} else {
const child = node.child
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(child, before, parent)
let sibling = child.sibling
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent)
sibling = sibling.sibling
}
}
}
}
3.4 更新渲染 - 构建FiberNode Tree
3.4.1 react-reconciler prepareFreshStack
获取旧根FibreNode
副本节点,复制旧根FiberNode
节点属性值
javascript
function prepareFreshStack(root, lanes) {
// 将FiberRootNode节点赋值给workInProgressRoot
workInProgressRoot = root
// 获取旧根FibreNode副本节点,复制旧根FiberNode节点属性值
const rootWorkInProgress = createWorkInProgress(root.current, null)
// 将根FiberNode副本节点赋值给workInProgress
workInProgress = rootWorkInProgress
return rootWorkInProgress
}
3.4.2 react-reconciler reconcileSingleElement
比对新ReactElement
对象和旧FiberNode
节点的key
和elementType
是否相同,相同则创建旧FiberNode
副本节点,复制旧FiberNode
节点属性值,不同则创建新FiberNode
节点,将不能复用的FiberNode
节点添加到父FiberNode
节点的deletions
属性中
javascript
function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
const key = element.key
// 旧FiberNode节点
let child = currentFirstChild
while (child !== null) {
if (child.key === key) {
const elementType = element.type
// FiberNode节点key和elementType相同则复用
if (child.elementType === elementType) {
// 删除旧兄弟FiberNode节点
deleteRemainingChildren(returnFiber, child.sibling)
// 创建旧FiberNode副本节点,复制旧FiberNode节点属性值
const existing = useFiber(child, element.props)
existing.return = returnFiber
return existing
}
// elementType不同删除该旧FiberNode节点及其兄弟节点
deleteRemainingChildren(returnFiber, child)
break
} else {
// 将要删除的旧FiberNode节点添加到父FiberNode节点的deletions属性
deleteChild(returnFiber, child);
}
child = child.sibling
}
}
3.4.3 react-reconciler reconcileChildrenArray
遍历新child ReactElement
对象,与旧FiberNode
节点比对key
和elementType
是否相同,相同则创建旧FiberNode
副本节点,不能则创建新FiberNode
节点,将不能复用的旧FiberNode
节点到父FiberNode
节点的deletions
属性中
javascript
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes) {
// 第一个child FiberNode节点
let resultingFirstChild = null
// 上一个兄弟FiberNode节点
let previousNewFiber = null
let oldFiber = currentFirstChild
let newIdx = 0
let nextOldFiber = null
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber
oldFiber = null
} else {
nextOldFiber = oldFiber.sibling
}
// 创建旧FiberNode副本节点
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
)
// 说明旧FiberNode节点不能复用
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber
}
break;
}
// 建立兄弟关联关系
if (previousNewFiber === null) {
resultingFirstChild = newFiber
} else {
previousNewFiber.sibling = newFiber
}
previousNewFiber = newFiber
oldFiber = nextOldFiber
}
const existingChildren = mapRemainingChildren(oldFiber)
if (shouldTrackSideEffects) {
// 将要删除的旧FiberNode节点添加到父FiberNode节点的deletions属性
existingChildren.forEach(child => deleteChild(returnFiber, child))
}
return resultingFirstChild
}
3.5 更新渲染 - 构建DOM
树
对于新增FiberNode
节点会构建DOM
树,构建流程和首次渲染构建DOM
树是一样的
3.6 更新渲染 - 更新DOM
3.6.1 react-reconciler recursivelyTraverseMutationEffects
递归遍历FiberNode Tree
节点,遍历deletions
属性中的FiberNode
节点,将其对应的DOM
节点从DOM
树中移除
javascript
function recursivelyTraverseMutationEffects(root, parentFiber, lanes) {
const deletions = parentFiber.deletions
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
commitDeletionEffects(root, parentFiber, childToDelete);
}
}
}