七天 仅仅两百多行实现一个mini-react!

引言:在朋友圈看到崔哥发布了一个mini-react游戏副本的训练营,五百人一起打卡学习,属实狠狠心动了呢,本人是一名正是持续学习前端的大三学生,最近正在备战秋招,遇到这个和那么多大佬一起学习的机会当然就选择参加了此次训练营,此篇文章会简要记录我这八天的学习过程。

Day1:

01实现最简mini-react

通过由简到繁循序渐进的思路方式去对齐ReactAPI,实现将文本渲染到网页。

思路:

  1. 从最看得见的真实DOM的创建渲染开始,然后先定义写死的虚拟DOM
  2. 生成动态的虚拟DOM树-此处用了递归
  3. 真实DOM动态生成
  4. 最后重构react api——主要指两个 API: ReactDom.createAPI, 及其 Render 函数

02使用jsx

借助vite将js改成jsx

思路:Vite 内置了对 JSX 的支持,会在构建过程中将 JSX 转换为对应的 react.createElement 调用,该功能基于 [esbuild]

Day2:

01实现任务调度器

在day1中的代码有一个问题,当dom树非常大时,渲染会出现卡顿,原因是浏览器中js是单线程,当任务很多时会阻塞页面渲染。 解决方案是采用分治思想,将一个大任务拆分成多个小task

思路:使用浏览器自带的api——Window.requestIdleCallback()插入一个回调函数,这个回调函数在浏览器空闲时期被调用。 回调函数会接收到一个名为 IdleDeadline 的参数, 其中的IdleDeadline.timeRemaining() 返回当前闲置周期的预估剩余毫秒数。可以判断目前是否有足够的时间来执行更多的任务。 只要在足够的时间内操作dom,当时间不够时,就跳出操作。等待下一个空闲时期继续执行。

02实现fiber架构

什么是fiber架构,先来了解一下它的核心思想?

Fiber 架构的核心思想是将协调过程分解为多个可中断的单元,以便在处理过程中可以中断、恢复和优先级调度。它使用了一种称为“Fiber”的数据结构,表示组件树的工作单元。

在此处通俗点来说就是如何去分步渲染dom?

其实,就是遍历树的同时转化为链表,将节点按顺序链接起来,再使用requestIdleCallback来调度更新DOM。

难点:如何做到处理过程中可以中断和恢复?也就是做到每次只渲染几个节点,并且在下次执行的时候依然从之前的位置开始执行?

 数组遍历是不可打断的,可以把数组结构改为链表结构,可以配合调度器分片,可打断。并且重新执行时通过指针进行回溯。采用链表的方式把任务串起来。 把dom树转换 成链表结构。 child ——sibling —— uncle 使用任务调度器,按照顺序渲染节点。

大致过程:

渲染当前节点X时要做的事:

  1. 生成真实dom,赋上对应属性
  2. 转换成链表 X
  3. 初始化X的子节点是什么,子节点的下一个兄弟节点是什么
  4. 渲染结束时返回下一个节点

Day3:

01实现统一提交

背景:第一天的时候,如果需要递归的树很大,就会导致渲染卡顿,在第二天通过fiber链表解决,但是会导致节点的部分渲染,而第三天我们将通过统一提交解决部分渲染问题

问题:因为我们使用的是 requestIdleCallback 这个API 来实现渲染任务调度的,会有一个问题 ,就是有空余时间的时候才会回调,那么就可能出现一个节点树,渲染到某个层级就停住了,此时主进程没有时间了,那就等着,用户看到的情况就是渲染了部分,过了一会又渲染一部分。

解决思路:在构建完fiber结构后再统一插入dom,让所有dom在链表处理完以后再统一提交,进行append操作添加到父级。

02实现function component

思路:把fc当成一个盒子,函数式组件,主要是vnode包了一层function的盒子,需要开盒,就能渲染。

  1. fiber.type()返回的是 vDom
  2.  没有 vDom 属性得继续向上查找
  3. fiber.props
  4.  处理 child 为 string | number 类型

Day4:

01实现事件绑定

思路:在处理 props 的地方去加判断, 通过属性是否以"on"开头判断是否是事件,然后给dom添加事件监听器。

02更新Props

那我们需要解决三个问题 :
  1.  如何得到新的 DOM 树
  2.  如何找到老的节点
  3.  如何 diff props

1.思路:之前在执行 render 函数的时候我们得到的一个的DOM树,通过给 nextWorkOfUnit 不断地赋值,在每个 Fiber 的 dom 属性上就形成了 DOM 树。那么我们更新的时候想要生成新 DOM 树的话可以直接执行 render 函数一样的逻辑

2.思路:我们新创建的节点在转化成新链表的时候我们可以添加一个属性 alternate,把这个属性的指针指向老的 Fiber 节点。

3.思路:dom diff old有,new 没 删除 new有,old没 添加 new 有,old有 修改

执行过程:

首先,在 React Fiber 架构中,我们有一个旧的根节点(保存在 currentRoot)作为参考。当我们在第一步创建新的根节点时,我们可以将其 alternate 属性指向 currentRoot,表示它们是相互对应的。

接下来,在处理新根节点的下一个子节点时,我们需要找到旧节点对应的下一个节点。我们可以通过 currentRoot 的 child 属性找到旧节点的子节点。类似地,当处理新树的兄弟节点时,我们可以通过 sibling 属性在旧树中查找相应的节点。

通过这种方式,我们可以逐步创建新的 Fiber 树,并正确地为每个节点的 alternate 属性赋值,使得新的 Fiber 树中的每个节点都与旧树的相应节点对应。这样,我们就可以进行下一步的差异比较逻辑了。

Day5:

01diff-更新children

目标:在diff过程张遇到type不一致时,删除旧的,创建新的。之前已经实现了新节点的创建,第五天主要学习旧节点的删除

思路:主要逻辑是判断节点类型不一致时,将其存入一个待删除deletions数组,在commit时从其父节点下删除。

02diff-删除多余的老节点

目标:新节点短于旧的时,多余的节点删除。

思路:reconcilChildren循环完children之后如果还有oldFiber则说明还有多余的节点,将它加入到deletions数组中,后续统一删除。

 03处理一些边界情况

此处省略......

Day6

01实现usestate

useState 是 React 中的一个钩子,它允许函数组件拥有状态。

思路:

在 mini-React 中,我们为每个函数组件维护了一个 stateHooks 数组,用于存储该组件的所有状态。当 useState 函数被调用时,它会首先检查是否存在旧的钩子(oldHook)。如果存在旧钩子,则使用旧钩子中存储的状态值作为初始值;否则,使用传入的初始值作为初始状态。

useState 函数返回一个数组,包含状态值和一个更新状态的函数(setState)。通过解构赋值,我们可以将数组元素分别赋值给变量,方便使用。

当调用 setState 函数时,它会将更新操作添加到钩子的 queue 队列中,并触发组件的重新渲染。在下一次渲染时,React 会从 queue 队列中获取所有的更新操作,并应用于相应的状态。

02批量执行action

在 useState 的实现中,我们添加了对 action 队列的处理。

思路:

当调用 setState 函数时,传入的更新操作(action)会被添加到钩子的 queue 队列中。这个队列会按照添加的顺序保存所有的更新操作。

在下一次组件渲染时,mini-react会遍历并执行 queue 队列中的所有操作,以此更新状态。通过批量执行这些操作,可以在一个渲染周期内实现批量更新状态的效果,而不是每次调用 setState 都触发一次渲染。

03提前检测减少不必要的更新

思路:

在 performWorkOfUnit 函数中,我们添加了对是否需要继续进行处理的判断。

在每次处理一个单元(unit)时,我们会检查当前处理的节点类型是否与之前处理的节点类型一致。如果不一致,意味着需要进行更新操作,因为节点类型的改变通常需要重新计算和渲染。

然而,如果当前处理的节点类型与之前处理的节点类型一致,我们可以跳过更新操作,从而减少不必要的计算和渲染,以提升性能。

Day7

01实现useEffect

我们先来了解一下useEffect

useEffect 是 React 中的一个钩子函数,用于处理组件中的副作用.

  1. useEffect 函数接受两个参数:副作用函数和依赖项数组。副作用函数会在组件渲染完成后执行,并可以根据依赖项的变化来决定是否再次执行。
  2. 副作用函数在组件首次渲染时会执行一次,并在组件的每次渲染完成后(包括首次渲染)根据依赖项的变化来决定是否重新执行。如果依赖项数组为空,则副作用函数只会在组件首次渲染时执行一次,并且不会再有后续执行。
  3. 依赖项可以是任何值的数组,可以为空数组,也可以不传。当传入一个非空数组时,React 会比较前后两次渲染时的依赖项数组的值是否发生变化。如果发生变化,则会重新执行副作用函数;如果依赖项数组的值没有变化,则副作用函数不会被重新执行。
  4. useEffect 函数可以返回一个函数,用于清理(cleanup)副作用。当组件将要被销毁时,React 会调用这个清理函数,以便进行一些清理操作,例如取消订阅、清除定时器等

思路:

  1. 把`effectHooks`存在`fiber`上。
  2.  每一个`effectHook`会有`callback(副作用函数)`、`deps(依赖项数组)`属性。
  3. 在dom挂载后,遍历fiber,再依次执行callback。

02实现cleanup

思路:在副作用函数被执行的时候来保存,在调用所有的 effect 副作用之前调用。遍历 Fiber 节点 ,拿到当前节点之前的节点(alternate)

mini-react代码链接:BrandyDIVE/mini-react (github.com)