从零到一:打造你的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
相关推荐
zhanghaisong_201521 分钟前
Caused by: org.attoparser.ParseException:
前端·javascript·html·thymeleaf
Eric_见嘉24 分钟前
真的能无限试(白)用(嫖)cursor 吗?
前端·visual studio code
DK七七1 小时前
多端校园圈子论坛小程序,多个学校同时代理,校园小程序分展示后台管理源码
开发语言·前端·微信小程序·小程序·php
老赵的博客1 小时前
QSS 设置bug
前端·bug·音视频
Chikaoya1 小时前
项目中用户数据获取遇到bug
前端·typescript·vue·bug
南城夏季1 小时前
蓝领招聘二期笔记
前端·javascript·笔记
Huazie1 小时前
来花个几分钟,轻松掌握 Hexo Diversity 主题配置内容
前端·javascript·hexo
NoloveisGod2 小时前
Vue的基础使用
前端·javascript·vue.js
GISer_Jing2 小时前
前端系统设计面试题(二)Javascript\Vue
前端·javascript·vue.js
海上彼尚2 小时前
实现3D热力图
前端·javascript·3d