mini-react 01|实现常用组件的渲染

201 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

这回,咱们整个大的,实现一个自己的👉 mini-react,这一节我们实现几种常用的组件渲染。

每一个功能实现我都会作为一个单独的 commit 进行提交。本节代码在 v0.0.1 分支上。 文中难免有错误或遗漏的地方,还请帮忙指正,谢谢~ 跪求您帮我点个🌟,谢谢您了~

高能预警,本文全程没有任何尿点,概念性的东西写出来就太多了,网上很多。如果有什么不明白的可以留言~

创建 React 中的一些Tag标识

创建src/ReactWorkTags.ts 文件,这些变量表示着组件的类型,这里我们暂时实现一些比较常见的类型。


export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;

Fiber 的含义

fiber 在 React 是非常重要的概念,我们在解释 fiber 时需要从三个层面去解释它

  1. 作为架构来说,之前React15的Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler。React16的Reconciler基于Fiber节点实现,被称为Fiber Reconciler。
  2. 作为静态的数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息。
  3. 作为动态的工作单元来说,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)。

PS:以上引用自 kasong 大佬。👉原文地址

写创建 Fiber 节点的函数

创建 src/ReactFiber.ts(fiber 节点是根据 jsx 对象进行创建的)

import { Placement } from "./ReactFiberFlags"
import { FunctionComponent, HostComponent } from "./ReactWorkTags"
import { isFn, isStr } from "./utils"

// vnode 就是当前节点的 jsx 对象
// returnFiber 就是父 fiber
export function createFiber(vnode, returnFiber) {
    const fiber: any = {
        // 组件类型
        type: vnode.type,
        key: vnode.key,
        props: vnode.props,

        stateNode: null,
        // 第一个子 fiber
        child: null,
        // 下一个兄弟 fiber
        sibling: null,
        // 父 fiber
        return: returnFiber,
        // 标记(这里先默认标记为 Placement)
        flags: Placement,

        alternate: null,
        // 要删除的节点,null 或 []
        deletions: null,
        // 当前 fiber 所在的层级的 index
        index: null,
    }

    const { type } = vnode

    if (isStr(type)) {
        fiber.tag = HostComponent
    } else if (isFn(type)) {
        // 函数组件以及类组件都会被判断为 function,但暂不做处理
        fiber.tag = FunctionComponent
    }

    return fiber
}

原生组件的渲染(将 jsx 处理成 fiber )

src/ReactFiberReconciler.ts

import { createFiber, Fiber } from "./ReactFiber";
import { isArray, isStringOrNumber, updateNode } from "./utils";

// 处理原生组件
export function updateHostComponent(workInProgress: Fiber) {

    if (!workInProgress.stateNode) {
        const element = document.createElement(workInProgress.type)

        workInProgress.stateNode = element
        // 处理 props
        updateNode(element, workInProgress.props)
    }
    // 往下处理子节点,children 是 jsx 对象
    reconcileChildren(workInProgress, workInProgress.props.children)
}

export function updateFunctionComponent(workInProgress: Fiber) { }

export function updateClassComponent(workInProgress: Fiber) { }

export function updateHostTextComponent(workInProgress: Fiber) { }

export function updateFragmentComponent(workInProgress: Fiber) { }


function reconcileChildren(workInProgress: Fiber, children) {
    if (isStringOrNumber(children)) {

            return
    }
    // 这里先将子节点都当作数组来处理
    const newChildren: any[] = isArray(children) ? children : [children]
    // 用于保存上个 fiber 节点
    let previousNewFiber: Fiber = null
    for (let i = 0; i < newChildren.length; i++) {
        const newChild = newChildren[i]
        if (newChild === null) {
            // 会遇到 null 的节点,直接忽略即可
            continue
        }

        const newFiber = createFiber(newChild, workInProgress)

        if (previousNewFiber === null) {
            // 第一个子节点直接保存到 workInProgress 上
            workInProgress.child = newFiber
        } else {
            // 后续都保存到上一个节点的 sibling 上
            previousNewFiber.sibling = newFiber
        }
        // 更新
        previousNewFiber = newFiber
    }
}

利用浏览器空闲时间来处理 fiber

src/ReactFiberWorkLoop.ts

import { Fiber } from "./ReactFiber";
import { Placement } from "./ReactFiberFlags";
import { updateClassComponent, updateFragmentComponent, updateFunctionComponent, updateHostComponent, updateHostTextComponent } from "./ReactFiberReconciler";
import { ClassComponent, Fragment, FunctionComponent, HostComponent, HostText } from "./ReactWorkTags";

// 当前正在处理的节点
let workInProgress: Fiber | null = null;

// 根节点
let workInProgressRoot: Fiber | null = null;

// 初次渲染和更新
export function scheduleUpdateOnFiber(fiber) {
    workInProgress = fiber;
    workInProgressRoot = fiber
}

function performUnitOfWork() {
    const { tag } = workInProgress

    // todo 1. 更新当前组件
    switch (tag) {
        case HostComponent:
            updateHostComponent(workInProgress);
            break;

        case FunctionComponent:
            updateFunctionComponent(workInProgress);
            break;

        case ClassComponent:
            updateClassComponent(workInProgress);
            break;

        case Fragment:
            updateFragmentComponent(workInProgress);
            break;

        case HostText:
            updateHostTextComponent(workInProgress);
            break;

        default:
            break;
    }

    // todo 2. 更新子组件(深度优先遍历)
    const child = workInProgress.child;
    if (child) {
        // 处理子节点
        workInProgress = child;
        return
    }

    while(workInProgress) {
        if (workInProgress.sibling) {
            workInProgress = workInProgress.sibling;
            return
        }
        // 没有兄弟节点就往上归一层,尝试处理上一层的兄弟节点
        workInProgress = workInProgress.return;
    }
    // 没有兄弟节点,也没有上一层,就结束
    workInProgress = null
}


function workLoop(IdleDeadLine: IdleDeadline) {
    while(workInProgress && IdleDeadLine.timeRemaining() > 0) {
        // 处理成 fiber,挂载 stateNode props 等操作
        performUnitOfWork()
    }
}

// 利用 requestIdleCallback 来计算处理
requestIdleCallback(workLoop)

处理完 fiber 以后创建 dom 并更新到真实 dom 上

src/ReactFiberWorkLoop.ts

function workLoop(IdleDeadLine: IdleDeadline) {
    while(workInProgress && IdleDeadLine.timeRemaining() > 0) {
        // 处理成 fiber,挂载 stateNode props 等操作
        performUnitOfWork()
    }

    if (!workInProgress && workInProgressRoot) {
        // workInProgress === null 且 workInProgressRoot 存在说明所有的 fiber 都处理完了
        // 需要更新到页面上
        commitRoot()
    }
}
	
// 提交
function commitRoot() {
    commitWorker(workInProgressRoot)
    // 提交完以后需要清空 workInProgressRoot 防止重复提交
    workInProgressRoot = null
}


// 深度优先遍历
function commitWorker(workInProgress: Fiber) {
    if (!workInProgress) return
    // 1. 提交自己

    const parentNode = workInProgress.return.stateNode
    const { flags, stateNode } = workInProgress
    if (flags & Placement && stateNode) {
        parentNode.appendChild(stateNode)
    }

    // 2. 提交子节点
    commitWorker(workInProgress.child)
    // 3. 提交兄弟节点
    commitWorker(workInProgress.sibling)
}

实现函数组件的渲染

src/ReactFiberReconciler.ts

export function updateFunctionComponent(workInProgress: Fiber) { 
    const { type, props } = workInProgress
    // type 就是函数,把 props 传过去就能拿到 jsx 对象了
    const children = type(props)
    // 向下 reconciler
    reconcileChildren(workInProgress, children)
}

函数组件是没有 stateNode 的,所以在渲染时 appendChild 会抛错,那这里需要找到真正的 dom 就需要往上找了

src/ReactFiberWorkLoop.ts

 function commitWorker(workInProgress: Fiber) {
    if (!workInProgress) return
    // 1. 提交自己

    // 需要找到实际渲染的父节点
    let parentNode = getParentNode(workInProgress.return)

    const { flags, stateNode } = workInProgress
    if (flags & Placement && stateNode) {
        parentNode.appendChild(stateNode)
    }
    // 2. 提交子节点
    commitWorker(workInProgress.child)
    // 3. 提交兄弟节点
    commitWorker(workInProgress.sibling)
}

// 函数组件等 是没有stateNode,所以要往上找真正的 dom 节点
function getParentNode(fiber: Fiber) {
    let stateNode = fiber.stateNode
    while(!stateNode) {
        fiber = fiber.return
        stateNode = fiber.stateNode
    }
    return stateNode
}

实现类组件的渲染

因为 typeof ClassComponent 和 FunctionComponent 都是 "function",所以需要处理一下创建对应的 fiber 节点时打的 tag 来区分这两者。

ClassComponent extends 自 React.Component,所以我们的区分点就在 Component 的原型上加个标志 isReactComponent,接着在创建 fiber 时根据这个标志来区分是 FunctionComponent 还是 ClassComponent

创建 React.Component 并添加 Class 组件的标志src/Component.ts

export default functino Component(props) {
    this.props = props
}

Component.prototype.isReactComponent = true

根据原型上的标志区分 fiber 的类型src/ReactFiber.ts


export function createFiber(vnode, returnFiber) {
    // ......

    if (isStr(type)) {
        fiber.tag = HostComponent
    } else if (isFn(type)) {
        // 函数组件以及类组件都会被判断为 function
        // 根据判断原型上是否有 isReactComponent 来判断出是 class 还是 function 的类型(isReactComponent 来自于 React.Component )
        fiber.tag = (type as Function).prototype.isReactComponent ? ClassComponent : FunctionComponent
    }
}

实现 ClassComponent 的 reconcilersrc/ReactFiberReconciler.ts

export function updateClassComponent(workInProgress: Fiber) {
    const { type, props } = workInProgress
    const instance = new type(props)
    // class 的 children 来自于 render 函数
    const children = instance.render()
    reconcileChildren(workInProgress, children)
}

实现文本组件的渲染

文本组件虽然创建了,但是可以通过打印的 fiber 来看,它上面的属性全部都是 null 或 undefined,所以在 performUnitOfWork 中不能走到对应的类型处理,需要给文本组件添加上类型src/ReactFiber.ts


export function createFiber(vnode, returnFiber) {
    // ......

    if (isStr(type)) {
        fiber.tag = HostComponent
    } else if (isFn(type)) {
        // 函数组件以及类组件都会被判断为 function
        // 根据判断原型上是否有 isReactComponent 来判断出是 class 还是 function 的类型(isReactComponent 来自于 React.Component )
        fiber.tag = (type as Function).prototype.isReactComponent ? ClassComponent : FunctionComponent
    } else if (isUndefined(type)) {
        fiber.tag = HostText
        fiber.props = {
            // 直接将文本放到 children 上
            children: vnode
        }
    }
}

这样就可以进入到 updateHostTextComponent 接着我们处理一下文本节点的 fiber,这部分直接创建一个文本,并将其直接放到fiber的 stateNode 属性上即可 src/ReactFiberReconciler.ts

export function updateHostTextComponent(workInProgress: Fiber) {
    const { props } = workInProgress
    // 创建一个文本节点放到 stateNode 上
    workInProgress.stateNode = document.createTextNode(props.children)
}

这样文本节点就可以被渲染出来了

Fragment组件渲染

Fragment 组件的 type 是 Symbol(react.fragment),这里我们直接进入到判断type的 else 分支即可。按照上面的套路,Fragment 也是加上对应的 tag,然后在对应的 reconciler 中进行处理src/ReactFiber.ts

export function createFiber(vnode, returnFiber) {
    // ......

    if (isStr(type)) {
        fiber.tag = HostComponent
    } else if (isFn(type)) {
        fiber.tag = (type as Function).prototype.isReactComponent ? ClassComponent : FunctionComponent
    } else if (isUndefined(type)) {
        fiber.tag = HostText
        fiber.props = {
            children: vnode
        }
    } else {
        // 直接打上 tag 即可
        fiber.tag = Fragment
    }
}

同样也是在对应的 reconciler 中处理一下,这里直接调用 reconcileChildren 传入workInProgress 和 children 即可src/ReactFiberReconciler.ts

export function updateFragmentComponent(workInProgress: Fiber) {
    const { props } = workInProgress
    reconcileChildren(workInProgress, props.children)
}

至此,我们实现了 React 中的 Fiber 创建,以及 ReactDOM.createRoot 和常用的几种组件渲染。 在这一节当中,我们使用的是浏览器提供的 requestIdleCallback,下一节我们自己实现一个来替代它。

👉 github 地址 在这里,本节代码在 v0.0.1分支。希望能得到你的 🌟~