深入理解React渲染原理

一. 调试项目

基于该目录结构和代码分析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 TreeDOM树结构如图所示,父FiberNode节点通过child属性指向子FiberNode节点、子FiberNode通过return属性指向父FiberNode节点,FiberNode节点通过sibling属性指向兄弟FiberNode节点,FiberNode节点通过stateNode属性指向DOM节点。

2.1.1 构建虚拟DOM

  1. 创建FiberRootNode节点
  2. 创建App Component对应的ReactElement对象
  3. 创建根FiberNode节点
  4. 创建根FiberNode副本节点并赋值给workInProgess,通过alternate属性相互引用
  5. 递归遍历FiberNode节点,创建ReactElement对象对应的FiberNode节点,建立父子,兄弟关联关系,构建FiberNode Tree

2.1.2 构建DOM

  1. 创建FiberNode节点对应的DOM节点
  2. 递归父FiberNode节点创建对应的DOM节点
  3. 将子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

  1. 获取旧根FiberNode副本节点,复制旧根FiberNode节点属性值
  2. 创建App FiberNode副本节点
  3. 调用App Component方法获取新ReactElement对象
  4. 递归遍历新ReactElement对象,与旧FiberNode节点比对keyelementType是否相同,相同则创建旧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

  1. 创建FiberRootNode节点
  2. 创建ReactDOMRoot对象,将FiberRootNode节点赋值给_internalRoot属性
javascript 复制代码
function createRoot(container, options) {
    // 创建FiberRootNode节点
    const root = createContainer()
    // 创建ReactDOMRoot对象,将FiberRootNode节点赋值给_internalRoot属性
    return new ReactDOMRoot(root)
}

3.1.2 React.createElement

jsxreact提供的语法糖,经过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

  1. 获取FiberRootNode节点
  2. 构建FiberNode TreeDOM树,将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

  1. FiberRootNode节点赋值给workInProgressRoot
  2. 创建根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)
    }
}

需要注意的是采用深度优先遍历算法 ,即递归遍历到第一个分支叶子节点后返回遍历下一个分支,例如下面这个示例,AB组件平级,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
  1. 获取App Component对应的ReactElement对象
  2. 创建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
  1. 调用Function Component方法,获取返回的ReactElement对象
  2. 创建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
  1. 获取元素标签的child ReactElement对象
  2. 创建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

currentnull,说明该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属性值为HostCompoentFiberNode节点

  1. 创建FiberNode节点对应的DOM节点
  2. 遍历子FiberNode节点,将子DOM节点append到当前DOM节点上
  3. 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属性值为HostComponentFiberNode节点,将对应的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节点的keyelementType是否相同,相同则创建旧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节点比对keyelementType是否相同,相同则创建旧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);
        }
    }

}
相关推荐
&活在当下&11 分钟前
uniapp H5页面实现懒加载
前端·uni-app·h5·移动端
screct_demo14 分钟前
详细讲一下React中Redux的持久化存储(Redux-persist)
前端·react.js·前端框架
qq_4243171826 分钟前
html+css+js网页设计 美食 易班 美食街5个页面
javascript·css·html
dgwxligg27 分钟前
C# 中 `new` 关键字的用法
java·前端·c#
mr_cmx1 小时前
JS 中 json数据 与 base64、ArrayBuffer之间转换
前端·javascript·json
今早晚点睡喔2 小时前
小程序学习07—— uniapp组件通信props和$emit和插槽语法
前端·javascript·uni-app
RobinDevNotes2 小时前
刚学完Vue收集的库或项目分享
前端·vue
彳亍2612 小时前
前端笔记:vscode Vue nodejs npm
前端·vscode
Hacker_Nightrain2 小时前
[CTF/网络安全] 攻防世界 baby_web 解题详析
前端·安全·web安全
夫琅禾费米线2 小时前
React Router 用法概览
前端·javascript·react.js·前端框架