理解 React 原生标签的初步渲染机制

77 阅读2分钟

目标

本文从 createRoot 开始,将一步一步实现原生组件的初步渲染,废话不多说,直接开始

开始

1. 实现 createRoot

使用方法:

const root = document.getElementById('root')
React.createRoot(root).render(<App/>)

实现过程:

export default createRoot(container: HTMLElement) {
    const root = {
        // 源码中这里包裹了一层
        containerInfo: container
    }
    return new ReactDOMRoot(root)
}

function ReactDOMRoot(internalRoot) {
    this._internalRoot = inernalRoot
}

// 给 ReactDOMRoot 的原型链上添加一个 render 方法
ReactDOMRoot.prototype.render = function(children) {
    const root = this._internalRoot
    
    // 更新节点
    updateContainer(children, root)
}

function updateContainer(element, container) {
    const { containerInfo } = container
    
    // 创建 Fiber 节点
    const fiber = createFiber(element, {
        // 这里的 nodeName 会返回一个大写的标签,例如 DIV,这里我们转换成小写
        type: containerInfo.nodeName.toLowerCase(),
        
        // 保存当前节点的 DOM 或 实例(类组件)
        stateNode: containerInfo
    })
    
    // 初次渲染或更新节点
    scheduleUpdateOnFiber(fiber)
}

ReactFiberWorkLoop

// 全程 work in process 表示当前正在工作中的
let wip = null

// 用来记录根节点
let wipRoot = null

// 这个函数作用:初次渲染以及执行更新
export function scheduleUpdateOnFiber(fiber) {
    wip = fiber
    wipRoot = fiber
}

function performUnitOfWork() {
    const { tag } = wip
    switch(tag) {
    
        // 原生标签
        case HostComponent:
            updateHostComponent(wip)
            break
            
        // 函数组件
        case FunctionComponent:
            updateFunctionComponent(wip)
            break
            
        // 类组件
        case ClassComponent:
            updateClassComponent(wip)
            break
            
        // 空节点组件
        case Fragment:
            updateFragmentComponent(wip)
            break
            
        // 文本节点
        case HostText:
            updateHostText(wip)
            break
            
        default:
            break
    }
}


function workLoop(IdleDeadLine) {

    // 执行所有任务
    while(wip && IdleDeadLine.timeRemaining() > 0 ) {
        performUnitOfWork()
    }
    
    // 任务完成,进行 Commit 阶段
    if(!wip && wipRoot) {
        commitRoot()
    }
}

function commitRoot() {
    commitWorker(wipRoot)
    wipRoot = null
}

function commitWorker(wip) {
    // 这个函数做三件事
    // 递归终止条件
    if(!wip) {
        return
    }
    // 1. 提交自己本身
    const { flags, stateNode } = wip
    
    // 原生标签的 parentNode 即 wip.return
    const parentNode = wip.return 
    // 如果是类组件或者函数组件,写法会发生变化
    if(flags & Placement && stateNode) {
        parentNode.appendChild(stateNode)
    }
    
    // 2. 提交子节点
    commitWorker(wip.child)
    // 3. 提交兄弟节点
    commitWorker(wip.sibling)
}

// 这个函数是 window 上的,不懂的可以去 mdn 查找一下
requestIdleCallback(workLoop)

ReactFiberReconciler

export function updateHostComponent(wip) {
    if(!wip.stateNode) {
        wip.stateNode = document.createElement(wip.type)
        // 处理属性值
        updateNode(wip.stateNode, wip.props)
    }
    
    // 处理原生标签
    reconcileChildren(wip, wip.props.children)
}

// 处理属性值
export function updateNode(node, nextVal) {
    Object.keys(nextVal).forEach(key => {
        if(key === 'children') {
            if(isStringOrNumber(nextVal[key])) {
                 node.textContent = nextVal[key]
            }
        } else {
            node[key] = nextVal[key]
        }
    })
}

// 处理原生标签
function reconcileChildren(wip, children) {
    
    if(isStringOrNumber(children)) {
        return
    }
    
    // 确保 children 是一个 数组
    const newChildren = Array.isArray(children) ? children : [children]
    
    let previousNewFiber = null
    
    for(let i = 0, n = newChildren.length, i < n;i ++) {
        const newChild = newChildren[i]
        if(newChild === null) {
            continue
        }
        const newFiber = createFiber(newChild, wip)
        
        if(previousNewFiber === null) {
            // 挂载头结点
            wip.child = newFiber
        } else {
            // 上一个节点的兄弟节点是 newFiber
            previousNewFiber.sibling = newFiber
        }
        previousNewFiber = newFiber
    }
}

总结

本文实现了 React 中对原生标签的初步渲染流程,当我们使用 createRoot 创建节点调用 render 方法之后,React 内部会逐一执行

  • updateContainer(更新节点)
  • scheduleUpdateOnFiber(初步渲染以及更新节点)
  • workLoop(执行当前所有的任务,之后进入 Commit 阶段)
  • performUnitOfWork (按照当前节点执行任务,初始化节点等)
  • updateNode(处理属性值)
  • reconcileChildren(处理,渲染原生标签)
  • commitRoot(从自身开始更新,按照 自身 -> 子 -> 兄弟 的顺序进行更新)

至此,我们的原生标签就能渲染出来啦