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

简介

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


通过前面的章节,我们已经实现了数据在页面上的渲染,并定义及更新了数据变量。接下来,我们将优化现有的更新逻辑。

问题清单:


当前的逻辑是从根节点开始遍历进行更新,如果节点较深,则会耗费大量时间。那么,我们应该如何优化呢?

Q1:React中怎么优化更新数据状态更新的?

  • 记录当前需更新的节点
  • 收集待更新操作,统一进行更新
jsx 复制代码
// 现在
function update() {
    workInProcessRoot = {
        dom: currentRoot.dom,
        props: currentRoot.props,
        alternate: currentRoot
    }
    nextUnitOfWork = workInProcessRoot
}

所以,我们需要一个变量来存储当前需要改变的节点和需要更新的action

jsx 复制代码
function useState(initial) {
    let currentFiber = workInProcessFiber
    const oldHook = currentFiber.alternate?.stateHooks[currentHookIndex]
    const stateHook = {
        state: oldHook ? oldHook.state : initial,
        // 保存setState的action
        queue: oldHook ? oldHook.queue : []
    }
    // 当组件更新时,将queue中的action依次执行
    stateHook.queue.forEach((action) => {
        stateHook.state = action(stateHook.state)
    })
    stateHook.queue = []

    currentHookIndex++
    stateHooks.push(stateHook)

    currentFiber.stateHooks = stateHooks

    function setState(action) {
        const eagerState =
            typeof action === 'function' ? action(stateHook.state) : action
        if (eagerState === stateHook.state) {
            return
        }

        // 当setState被调用时,将新的state放入queue中
        stateHook.queue.push(typeof action === 'function' ? action : () => action)

        workInProcessRoot = {
            // 更新时只更新当前节点
            ...currentFiber,
            alternate: currentFiber
        }
        nextUnitOfWork = workInProcessRoot
    }
    return [stateHook.state, setState]
}

function updateFunctionComponent(fiber) {
    workInProcessFiber = fiber
    // 重置状态
    workInProcessFiber.stateHooks = []
    currentHookIndex = 0
    const children = [fiber.type(fiber.props)]
    reconcileChildren(fiber, children)
}

useEffect

我们来看看官网的用法:

jsx 复制代码
import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
    const [serverUrl, setServerUrl] = useState('https://localhost:1234');

    useEffect(() => {
        const connection = createConnection(serverUrl, roomId);
        connection.connect();
        return () => {
            connection.disconnect();
        };
    }, [serverUrl, roomId]);
    // ...
}

Q2:怎么实现useEffect?

分析:

  • 触发时机是在组件渲染完成后,即收集的回调函数将在组件渲染完毕后执行。
  • 第一个参数是执行函数,第二个参数是监听的变量
  • 返回一个副作用函数,在更新后执行
jsx 复制代码
function updateFunctionComponent(fiber) {
    workInProcessFiber = fiber
    workInProcessFiber.stateHooks = []
    // 重置
    workInProcessFiber.effectHooks = []
    currentHookIndex = 0
    const children = [fiber.type(fiber.props)]
    reconcileChildren(fiber, children)
}

function commitRoot() {
    commitWork(workInProcessRoot.child)
    // 在渲染完成后,触发effect hooks
    commitEffectHooks()
    currentRoot = workInProcessRoot
    workInProcessRoot = null
    currentHookIndex = 0
}

function commitEffectHooks() {
    function runEffects(fiber) {
        if (!fiber) return
        // 如果是新创建的fiber节点,那么就是mount,否则就是update
        const isMount = !fiber.alternate
        fiber.effectHooks?.forEach((effectHook, index) => {
            if (isMount) {
                effectHook.cleanup = effectHook.callback()
                return
            }

            // handle deps change
            const oldEffectHook = fiber.alternate?.effectHooks[index]
            const depsChanged = haveDepsChanged(oldEffectHook?.deps, effectHook.deps)
            if (depsChanged) {
                oldEffectHook.cleanup?.()
                effectHook.cleanup = effectHook.callback()
            }
        })
    }

    function haveDepsChanged(oldDeps = [], newDeps = []) {
        return oldDeps.some((dep, i) => dep !== newDeps[i])
    }

    function traverseFiber(fiber) {
        if (!fiber) return
        // 处理当前fiber节点的副作用
        runEffects(fiber)
        traverseFiber(fiber.child)
        traverseFiber(fiber.sibling)
    }

    // 从根fiber节点开始遍历和处理
    traverseFiber(workInProcessFiber)
}

注意

在前面的章节中,我们没有处理一个边界情况:当fiber树长度不一致时,未对多余的节点进行删除。现在我们将补充这个边界情况的处理。

jsx 复制代码
// 创建全局变量
let deletions = []

function commitRoot() {
    deletions.forEach(commitDeletion)
    // 省略代码...
    deletions = []
}

function commitDeletion(fiber) {
    if (fiber.dom) {
        let fiberParent = fiber.parent
        while (!fiberParent.dom) {
            fiberParent = fiberParent.parent
        }
        fiberParent.dom.removeChild(fiber.dom)
    } else {
        commitDeletion(fiber.child)
    }
}

function reconcileChildren(wipFiber, children) {

    while (index < children.length) {
        // 省略代码...
        if (isSameType) {
            // 省略代码...
        } else {
            // 省略代码...
            if (oldFiber) {
                deletions.push(oldFiber)
            }
        }
        // 省略代码...
    }
    while (oldFiber) {
        deletions.push(oldFiber)
        oldFiber = oldFiber.sibling
    }
}

至此,我们已基本实现了数据渲染、更新以及useState和useEffect的功能。

相关推荐
青灯文案17 分钟前
前端 HTTP 请求由 Nginx 反向代理和 API 网关到后端服务的流程
前端·nginx·http
m0_7482548812 分钟前
DataX3.0+DataX-Web部署分布式可视化ETL系统
前端·分布式·etl
ZJ_.23 分钟前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
GIS开发特训营27 分钟前
Vue零基础教程|从前端框架到GIS开发系列课程(七)响应式系统介绍
前端·vue.js·前端框架·gis开发·webgis·三维gis
Cachel wood1 小时前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
学代码的小前端1 小时前
0基础学前端-----CSS DAY9
前端·css
joan_851 小时前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui
m0_748236112 小时前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
Watermelo6172 小时前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_748248942 小时前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5