从零到一:打造你的Mini-React (二)

简介

在本篇文章中,我们将一起探索如何从零构建一个迷你版的React框架。通过这个过程,不仅能够加深对React内部机制的理解,还能学习到虚拟DOM、组件生命周期和状态管理等核心概念的实际应用。本文旨在深入浅出地理解React工作原理。

问题清单


Q1:怎么处理数组元素?

在渲染过程中,我们对元素el进行类型检查。当确认el为数组类型时,特殊处理。

接上述文章,我们修改一下用例

jsx 复制代码
// App.jsx
import React from './core/React'

const LARGE_ARRAY_SIZE = 100

function generateLargeArray() {
  return Array.from({ length: LARGE_ARRAY_SIZE }, (_, index) => index)
}

function App() {
  const largeArray = generateLargeArray()

  return (
    <div>
      <ul>
        {largeArray.map((i) => (
          <li key={i}>{i}</li>
        ))}
      </ul>
    </div>
  )
}

export default App
jsx 复制代码
// React.js
// 省略...
function render(el, container) {
  if (typeof el === 'function') el = el()
  // handle arrays elements
  if (Array.isArray(el)) {
    el.forEach((el) => render(el, container))
    return
  }
  // 省略...
}

Q2:假如dom树非常大,渲染会发生卡顿,怎么处理?

采用分块渲染策略是一种高效的手段。这种策略充分利用了浏览器的API------requestIdleCallback,借此在浏览器空闲时间执行渲染任务。通过这种方式,渲染工作得以在可用的时间段内平均分配,从而显著减少性能瓶颈的发生,并提升用户体验。


分块渲染

根据上述思路,我们须对render函数进行相应的修改。鉴于需要实现分块渲染,我们需引入一种能够描述节点的数据结构,该结构不仅要能够描述节点,还需要支持渲染过程的中断与恢复功能。我们来看下React的解决策略------Fiber架构。

可以看下我们当前的结构树,如果我们需要遍历这个树,通常来说用链表来存储,则每个节点都需要记录

  • 父节点
  • 兄弟节点
  • 子节点
  • 挂载的dom
  • 节点类型

值得注意的是,实际上在React的源代码中,这一结构比上述所述更为复杂。源码地址

此外,为了能在系统空闲时进行渲染,我们需要借助requestIdleCallback函数。这一机制使得渲染工作能够高效地利用系统的空闲周期执行,从而优化性能并提升用户体验。

所以,我们修改原有的render函数

jsx 复制代码
// React.js

function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.map((child) => {
                const isTextNode =
                    typeof child === 'string' || typeof child === 'number'
                return isTextNode ? createTextNode(child) : child
            })
        }
    }
}
function createTextNode(text) {
    return {
        type: 'TEXT_ELEMENT',
        props: {
            nodeValue: text,
            children: []
        }
    }
}

let nextUnitOfWork = null
function render(element, container) {
    // nextUnitOfWork 是 fiber 树的根节点
    nextUnitOfWork = {
        dom: container,
        props: {
            children: [element]
        },
        alternate: null,
        parent: null,
        child: null,
        sibling: null
    }
}

function workLoop(deadline) {
    let shouldYield = false
    while (nextUnitOfWork && !shouldYield) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
        shouldYield = deadline.timeRemaining() < 1
    }
    // 闲时执行
    requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)

function updateProps(dom, props) {
    const isProperty = (key) => key !== 'children'
    Object.keys(props)
        .filter(isProperty)
        .forEach((name) => {
            dom[name] = props[name]
        })
}

function createDom(fiber) {
    const dom =
        fiber.type === 'TEXT_ELEMENT'
            ? document.createTextNode('')
            : document.createElement(fiber.type)
    updateProps(dom, fiber.props)
    return dom
}

function reconcileChildren(wipFiber, elements) {
    let prevSibling = null
    let index = 0
    while (index < elements.length) {
        const element = elements[index]
        const newFiber = {
            type: element.type,
            props: element.props,
            parent: wipFiber,
            dom: null
        }

        if (index === 0) {
            wipFiber.child = newFiber
        } else {
            prevSibling.sibling = newFiber
        }

        prevSibling = newFiber
        index++
    }
}
function updateFunctionComponent(fiber) {
    // Run the function component to get the children
    const children = [fiber.type(fiber.props)]
    reconcileChildren(fiber, children)
}

function updateHostComponent(fiber) {
    if (!fiber.dom) {
        fiber.dom = createDom(fiber)
    }

    if (fiber.parent) {
        if (!fiber.parent.dom) {
            // function component 的 fiber 没有 dom
            // 如果 fiber 的父节点没有 dom,就找到最近的有 dom 的父节点
            while (!fiber.parent.dom) {
                fiber.parent = fiber.parent.parent
            }
        }
        fiber.parent.dom.appendChild(fiber.dom)
    }

    const elements = fiber.props.children
    reconcileChildren(fiber, elements)
}

function performUnitOfWork(fiber) {
    const isFunctionComponent = fiber.type instanceof Function

    if (isFunctionComponent) {
        updateFunctionComponent(fiber)
    } else {
        updateHostComponent(fiber)
    }

    const children = isFunctionComponent
        ? [fiber.type(fiber.props)]
        : fiber.props.children
    reconcileChildren(fiber, children)

    if (fiber.child) {
        return fiber.child
    }
    let nextFiber = fiber
    while (nextFiber) {
        if (nextFiber.sibling) {
            return nextFiber.sibling
        }
        nextFiber = nextFiber.parent
    }
}

可以看到,页面正常渲染

Q3:当浏览器资源被充分占用且缺乏空闲时间,而渲染进程尚未完成时,应该怎么处理?

为了优化渲染过程并有效管理资源消耗,可以采取一种策略,即首先构建一个Fiber树,Fiber树构建完成后,可以将其在适当的时间点一次性挂载到DOM上。

统一提交

为了解决上述问题,我们需要设计一个函数,来完成最后一次性提交,这就是React中的commitWork

因为我们需要在最后才一次性挂载,所以需要记录fiber树的根节点

jsx 复制代码
// React.js
// 省略...

// 记录fiber树的根节点
let root = null
function render(element, container) {
    // nextUnitOfWork 是 fiber 树的根节点
    nextUnitOfWork = {
        dom: container,
        props: {
            children: [element]
        },
        alternate: null,
        parent: null,
        child: null,
        sibling: null
    }
    // new feature
    root = nextUnitOfWork
}


function workLoop(deadline) {
    // 省略...
    // 当没有下一个工作单元时,提交整个 fiber 树
    if (!nextUnitOfWork && root) {
        commitRoot()
    }

    requestIdleCallback(workLoop)
}


function commitRoot() {
    // 递归提交整个fiber树到DOM
    commitWork(root.child)
    // 重置工作单元
    root = null
}

function commitWork(fiber) {
    if (!fiber) {
        return
    }
    let fiberParent = fiber.parent
    // handle function component
    while (!fiberParent.dom) {
        fiberParent = fiberParent.parent
    }

    if (fiber.dom) {
        fiberParent.dom.appendChild(fiber.dom)
    }
    commitWork(fiber.child)
    commitWork(fiber.sibling)
}

function updateHostComponent(fiber) {
    if (!fiber.dom) {
        fiber.dom = createDom(fiber)
    }
    
    const elements = fiber.props.children
    reconcileChildren(fiber, elements)
}

总结

我们成功地实现了元素在界面中的正确渲染,并对渲染过程进行了优化,采用了分块渲染与统一提交的策略。通过引入Fiber架构,我们能够有效地追踪当前的元素结构树,同時利用一个指针来引用整个渲染过程的根节点。这种方法不仅提高了渲染效率,而且为动态更新提供了支持。


在后续章节中,我们将详细介绍

  • 如何实现属性(props)的更新机制
  • useState和useEffect
相关推荐
乐多_L40 分钟前
使用vue3框架vue-next-admin导出表格excel(带图片)
前端·javascript·vue.js
南望无一1 小时前
React Native 0.70.x如何从本地安卓源码(ReactAndroid)构建
前端·react native
Mike_188702783511 小时前
1688代采下单API接口使用指南:实现商品采集与自动化下单
前端·python·自动化
鲨鱼辣椒️面1 小时前
HTML视口动画
前端·html
一小路一1 小时前
Go Web 开发基础:从入门到实战
服务器·前端·后端·面试·golang
堇舟1 小时前
HTML第一节
前端·html
纯粹要努力1 小时前
前端跨域问题及解决方案
前端·javascript·面试
小刘不知道叫啥1 小时前
React源码揭秘 | 启动入口
前端·react.js·前端框架
kidding7231 小时前
uniapp引入uview组件库(可以引用多个组件)
前端·前端框架·uni-app·uview
合法的咸鱼1 小时前
uniapp 使用unplugin-auto-import 后, vue文件报红问题
前端·vue.js·uni-app