原理
MiniReact-实现基本的React操作
如何理解VDOM、VNode、JSX?
都是JS对象对相应的概念的抽象表示
DOM 与 VDOM
DOM与VDOM
DOM提供结构化方式来表达网页中的内容和结构,每一个DOM节点都是DOM树上的一个节点
VDOM是JS对象,是对DOM的轻量级抽象表示。VDOM的出现是为了优化页面性能
直接操作DOM较慢,每次更新都需要重绘与重排;DOM中每次的修改会直接反映在页面上,对性能开销有即时性、高要求;基础DOM更新适合简单的静态、低频率更新的应用
VDOM在内存中修改,速度更快;而VDOM的更新通过diff算法进行按需更新,以及基于queue的批处理更新;VDOM更适合动态交互密集的应用,如SPA应用,可使其有明显的性能和用户体验
fiber架构包括哪些部分?
包括三部分
调度器 scheduler
将任务转为较小的工作单元,以及排列优先级
- 工作单元 fiber Node (Fiber节点)
- 优先级
协调器 reconciler
通过diff进行新老Node比对、更新、插入、删除
渲染器 renderer
在数据结构的层面上,通过链表关系的构建,在每一个Node上都构建出old、sibling、children、father等指针,形成灵活的树结构,既便于diff搜索与后续操作,也有助于性能提高
如何理解fiber架构?
核心思想就一点:碎片化处理;这种碎片化主要体现在两个方面:
- 时间上的碎片化:非阻塞 & 碎片化时间
- 任务上的碎片化:可中断 & 碎片化任务
很像暗时间中提到的概念,合理利用起来浏览器的暗时间作为算法,设置对应的链表和VDOM作为数据结构
如何实现一个 Function Component 的过程是怎样的?你能从创建组件的基本结构、接收 props、以及渲染内容等方面来详细讲解吗?
FunctionComponent,即jsx jsx实际上是一种语法规范,当jsx编写完毕后,会通过React的createElement类似的接口转换为React调用,实际上也是一种便于后续VDOM操作的便捷表达方式,结构会转变为VNode对象 接收的props也是会转为VNode对象的属性,以对象的形式表示 内容渲染上,FC不同于原生标签,会通过type属性筛选并进行特殊的操作,即执行当前函数,并返回VNode
- Function Component 可以使用 React Hooks 来管理状态和副作用
- 函数组件可以更方便地复用和组合
- 可以通过
React.memo来优化组件的性能
能否详细讲解一下
useState的实现原理,包括状态是如何存储和更新的?另外,如何实现状态缓存和批处理的机制呢?
useState 的实现原理主要依赖于闭包和发布订阅模式。在组件的外层作用域中,React 会维护一个对象数组,每个对象对应一个 useState 调用,存储当前状态值及其索引。在状态更新时,React 遍历这个数组,根据索引找到对应的状态对象并更新其值,这也是为什么 Hooks 不能使用 if 语句来改变调用顺序。
关于批处理机制,React 在事件处理函数中会自动将多次状态更新合并。当事件处理结束时,React 会批量处理所有在此期间触发的状态更新,从而减少组件的重新渲染次数。此外,在异步操作(如网络请求)完成后,React 也会合并状态更新,进一步优化渲染性能。通过这些机制,React 能够高效地管理状态变化,提高整体应用的响应速度。
能否详细讲解一下
useEffect的实现原理,包括它如何管理副作用的执行?另外,你能解释一下如何处理深度(depth)清理的问题吗?
useEffect和useState类似,都是在外层作用域上维护一个effect数组,里边包括了callback和depth 通过协调器监听depth是否发生变化,并在合适的时机执行callback
- 调度执行:协调器在合适的时机(如组件卸载或依赖变化时)来调度执行副作用和清理函数
- 函数清理:每次副作用执行之前,React 会调用上一次副作用返回的清理函数,以防止内存泄漏或副作用叠加
详细讲解一下如何实现最简化的 Redux 状态管理逻辑?
机制:发布订阅 概念:
- store:包含initialState、dispatch、subscribe
- 一个HOC,内部包含initialState & dispatch & subscribe,外部块作用域维护一个listenner数组,根据subscribe回调,将其放入listenner中
- dispatch:执行reducer并遍历listener数组
- subscribe:监听 ( 订阅 )
- reducer:纯函数,返回状态
- action:包含type以及callback
方法总结: 收集——存储——寻找
- 复杂问题 && 源码可以通过画图来理清思路, 不断找到问题以及突破点
- 尝试debug代码, 通过代码执行流程来不断逼近问题点
- 根据已有条件, 思考问题, 寻找问题, 解决问题
- 基于已有代码的优化 > 基于新增功能的优化 > 不优化
- 实现功能时专注于实现功能, 重构时专注于重构
1——动态创建VDOM && VNode, 实现页面的动态渲染
VDOM是基于Js对象对DOM进行抽象描述, 最后根据React自身协调渲染, 来达成从抽象到具体的过程。 VNode是VDOM的子集, 是其中的一部分, 同样是基于JS对象来进行有选择地创建, 最后添加到VDOM上
2——添加JSX
基于递归的VNode创建的问题? 出现的原因?
在VNode节点层次很深的时候, 算法时间会过长, 导致页面卡顿
- 在当前的render函数中, 由于Js本身的单线程, 加上函数为同步阻塞执行, 当运算量提高就一定会造成卡顿
- 当 VNode 节点层次很深时,递归处理可能导致算法的时间复杂度增加,因为每一层递归都需要进行一些操作,如创建节点、处理属性等。
- 递归可能会导致堆栈溢出。每一层递归都需要在调用栈上分配一帧,而调用栈的深度是有限的。大量嵌套的节点会导致递归深度增加,最终可能触发堆栈溢出错误。
耗时长, 性能低
- 频繁的 DOM 操作和深层递归, 会出现重复创建和销毁:递归过程中,每次创建一个节点,然后递归处理其子节点。这样的递归可能导致大量的节点创建和销毁操作,而这些操作本身是具有一定开销的。可能会增加浏览器的重绘和重排成本。
出现的原因: 在创建的时候是直接操作DOM, 而直接操作DOM的性能开销是很大的。无优化手段。
- 这一点需要注意一下。实际上,React 中的 Virtual DOM 机制是为了优化直接操作实际 DOM 的性能开销而引入的。通过虚拟 DOM,React 可以在内存中构建整个节点树,然后一次性更新实际 DOM,减少了直接操作 DOM 的频率,从而提高了性能。
优化策略:
- 批量处理: 减少实际的 DOM 操作次数,例如通过文档片段(DocumentFragment)进行批量操作。而不是每次创建节点都立即进行 DOM 操作,可以先在内存中构建节点树,然后一次性添加到 DOM 中。
- 使用 Key: 在进行递归时,使用唯一的 Key 可以帮助 React 更有效地比较新旧节点,从而减少 DOM 操作。这有助于确保只有需要更新的节点进行了操作。
- 虚拟 DOM Diff 算法: 使用更高效的虚拟 DOM Diff 算法来比较新旧节点,找出需要更新的部分,减少不必要的操作。
- 内存优化: 考虑节点的缓存、复用等优化策略,以减少不必要的对象创建和销毁。
JSX的转换
浏览器并不认识JSX, 需要借助第三方工具将JSX转为JS
- vite
- webpack
- babel 使用vite可以无需配置的情况下将js转为jsx
JSX的本质
jsx本质上就是通过VNode的createElement创建的节点, 将其封装为jsx的格式, 通过语法解析调用
JSX(JavaScript XML)是一种语法糖,它提供了一种更直观、更易读的方式来描述虚拟 DOM 结构。
在 React 应用中,JSX 元素实际上会被编译为对 React.createElement 函数的调用,从而创建虚拟节点(VNode)。
jsx的语法糖会隐式调用通过import导入的React默认导出对象的createElement()方法, 所以在intoJSX文件夹引入的core中修改React.js的createElement()或者createTextNode()的代码都在使用jsx语法糖的地方显现, 比如log一条信息, 那么jsx调用了多少次, 就会相应的打印多少次
3——任务调度器基础
实现任务调度器
- 用于解决DOM树规模增加时渲染的卡顿
- 基本思路: 将大任务拆分为多个task
- 实现: 采用requestIdleCallback
利用分治的思想, 将DOM树的渲染拆分为多个子任务, 子任务负责渲染部分的DOM树
具体每次一个task中实现最少一个DOM渲染, 然后进行判断是否还有剩余时间, 如有, 则继续渲染
数据结构采用链表可以更加方便和适宜地解决问题, 因为DOM是有序的, 子任务必须保证顺序, 而链表的特性可以满足这几点
对于DOM树, 采用深度优先遍历, 将返回结果生成为链表
方法一:
先将DOM全部遍历, 再处理链表
方法二:
一边遍历DOM, 一边建立链表
4——实现最简fiber架构
实现基本的任务调度器
- 数据结构: 链表
- 算法: DOM遍历和链表建立同时进行
requestIdleCallback 是一种在浏览器的空闲时段执行任务的机制,但它并不保证任务会在固定的时间内完成,也可能会因为时间片连续不足而造成渲染中途的延迟。这可能导致用户感知到的卡顿或延迟。reqeustIdleCallback()的运行时间不确定, 可能会因为时间片连续不足造成渲染中途的延迟, 如何解决?
- 任务拆分: 将大任务拆分成小任务,每个小任务耗时较短。这样可以确保在每个空闲时间片内都能完成一部分工作,提高渲染的连续性。这也是 Fiber 架构的一种核心思想。
- 优先级调整: 在任务拆分的基础上,为任务设置不同的优先级。确保高优先级任务先执行,提高用户感知的流畅度。React 中的 Scheduler 就采用了这种策略。
- 使用 Web Workers: 将一些计算密集型或耗时较长的任务放到 Web Workers 中执行,以保持主线程的空闲状态。这样可以提高整体性能,避免主线程被占用过久。
- 性能监控和调整: 使用浏览器开发者工具中的性能监控工具,分析任务执行的时间分布,找出耗时较长的任务,并进行优化。
Fiber架构
定义: Fiber架构是React 中用于实现协调更新的一种算法和数据结构
目的: Fiber 架构的引入是为了解决 React 在处理大型应用时可能出现的性能瓶颈和阻塞
作用: 使得 React 能够更好地处理复杂的渲染、布局和动画, 并保持较高的性能
概念:
- Fiber 节点: 在 Fiber 架构中,每个 React 元素都对应一个 Fiber 节点。这些节点构成了一个虚拟的树结构,称为 Fiber 树。每个 Fiber 节点都包含有关元素的信息,如类型、props、状态等。
- 工作单元: 在 Fiber 架构中,任务被分解为可中断的小单元,称为工作单元。这些工作单元可以在浏览器的空闲时段执行,以提高响应性和用户体验。
- 调度器: Fiber 架构引入了调度器,负责决定在何时执行工作单元。调度器使用优先级来确定哪些工作单元应该首先执行。这样可以确保高优先级的任务优先执行,提高用户感知的流畅度。
- 协调器: 协调器是 Fiber 架构的核心部分,负责协调更新过程。它能够判断哪些部分需要更新,生成新的 Fiber 树,并将更新推送到渲染阶段。
- 渲染器: 渲染器负责将协调器生成的 Fiber 树转换为实际的 DOM
- 双缓存技术: Fiber 架构采用了一种双缓存技术,通过交替使用两个 Fiber 树,来实现无阻塞的渲染。在每次更新时,都会生成一个新的 Fiber 树,然后将新的 Fiber 树与上一次渲染使用的 Fiber 树进行比较,以确定哪些部分需要更新。
- 中断与恢复: Fiber 架构支持任务的中断与恢复,即任务可以在执行过程中被中断,然后在下一个空闲时段继续执行。这有助于避免长时间的任务阻塞主线程。
为什么要用requestIdleCallback
这个API相比较其它同类型的, 如messageChannel, requestAnimationFrame, setTimeout, Promise, MutationObserver, 其特点十分明显。
reqeustIdleCallback
requestIdleCallback基于Browser runtime, 其执行运行在空闲时间段, 不会阻塞主线程 核心概念: 空闲时间, 非实时 特点 优点
- 空闲时执行
- 不会影响到用户交互和体验, 提高资源利用率
- 可设置优先级
- 这点特性对于及时响应十分重要
- 可中断&&恢复
- 若空闲时间结束, 会暂停当前任务并在下一个时间片内继续执行, 能够防止长时间运行的任务阻塞主线程
- 与浏览器渲染同步
- 任务执行不会影响到浏览器渲染性能 缺点
- 空闲时间不确定 任务的执行时间、设备性能不确定, 可利用的空闲时间不确定
- 执行时间限制 浏览器设定了最大的执行事件, 超出会中断其执行
- 执行频率不稳定 取决于浏览器的空闲状态
- 不适用于实时任务 不适用于需要实时性的任务, 例如动画渲染或用户输入响应; 主要适用于非实时性的任务, 如后台数据同步、缓存更新等 应用场景
- 数据同步
- 缓存更新
- 性能检测
- 优化web worker线程执行效率
- 图像处理 缩略图, 调整大小
- 日志上传
- 数据预加载
- 代码块动态分割
messageChannel
基于事件的异步通信API. 支持双端的信息通讯 核心概念: 通道, 端口 特点 优点
- 异步双向通讯
- 无循环引用 消息传递是异步的,不会导致直接的引用关系。
- 页面间通讯&&web worker通讯 更高效的数据共享和多线程并发支持
- 避免同源限制 能在不同源之间传递消息
- 替代定时器 缺点
- 性能开销大 频繁创建和使用messageChannel性能开销会较大
- 安全隐患 由于能够跨源, 可能会引入漏洞
- 内存泄漏隐患 通道需要关闭
- 异步通讯无法保证传递消息和接收消息的顺序一致 应用场景:
- 跨窗口通讯(iframe, 弹出窗口)
- web worker通讯
- 避免循环引用
- *实现异步调度, 避免阻塞主线程
reqeustAnimationFrame
基于Broser runtime, 用于执行动画, 在每一帧渲染前执行回调. 设计目的: 更加有效地执行动画操作, 避免使用 setTimeout 或 setInterval 时可能出现的一些问题
核心概念: 动画逻辑, 浏览器绘制机制
特点:
- 与浏览器渲染同步 在浏览器的渲染循环中执行, 与浏览器渲染同步
- 状态自适应 页面处于非激活状态时会自动暂停
- 资源节省 浏览器会自动优化其执行 缺点
- 定时器不精确 由于页面被挂起、最小化或处于非激活状态, 其会被暂停或者减慢
- CSS动画冲突 transform可能导致其逻辑被阻塞
- 性能开销 并不会考虑主线程的繁忙程度, 若执行逻辑复杂、较多, 则会使得主线程繁忙, 掉帧
- 需要手动管理 应用场景:
- 动画效果 与浏览器的渲染同步,提供了更加流畅和高效的动画效果
- 页面滚动 避免滚动事件频繁触发而引起性能问题
- 替代定时器 确保逻辑与浏览器的渲染同步
- canvas渲染 用于触发下一帧的渲染,以实现流畅的 Canvas 动画
- 用户输入响应 更新相关的页面元素,以提供更快的响应速度
- 模拟滑动 轮播图、图表等使用场景中, 可以用于实现平滑的滑动效果
- 性能监控 定期执行性能监控逻辑, 例如计算帧率、测量渲染时间等,以优化页面性能
- *任务调度 在每一帧之间执行任务,避免阻塞主线程
MutationObserver
异步观察DOM树结构变化, 观测节点及其属性的变化并响应 核心概念: 变化类型, 节点类型, DOM树 特点: 优点
- 异步执行 会在浏览器完成当前帧的渲染之后执行
- 多目标, 多类型支持 可以同时观察多个不同的目标节点, 同时监视多种不同类型的DOM变化
- 全捕获 多个变化发生在同一帧内仍旧可以全部捕获到
- 可观察子树 配置 subtree: true可以观测到子树及其节点变化
- 原始值缓存 可以获取变化前的属性值, 跟踪变化
- 不受同源限制 可以观察不同源的iframe或其他上下文中的DOM变化。 缺点
- 性能开销 在大量DOM变化时会产生较高的性能开销
- 异步执行 无法确保顺序与及时响应
- 无法监听所有属性 主要监听DOM树, 样式则需要使用getComputedStyle
- 内存泄漏隐患 需要正确调用disconnect
- 代码较为复杂 应用场景
- 自定义组件 监听组件内部的变化
- 表单验证 检测并更新表单
- 自动保存 监听文本变化以实现自动保存
- 无障碍支持
- 动态加载 监听新内容插入并执行回调
- 路由导航 监听路由DOM结构, 触发相应的路由逻辑
- 广告检测
- *数据绑定 实时数据绑定
5 统一提交
搞明白问题的原因, 找到问题的原因就解决了一半 别人在遇到问题时是怎么解决的, 我遇到类似的问题我是否也可以用他的解决思路来解决我的问题 issue
- 当前渲染在会使得用户看到渲染一半的DOM solution
- 统一提交渲染, 在执行完链表操作后一次性提交, 关闭原本的分段提交; 分段提交会产生断落 / 分层
6 函数组件function component
重构伴随着开发周期, 看似会增加一些代码量, 但实际上会减少花费的时间与精力
Task
- 支持Function Component
- 处理props的type类型
- 处理function component 和非function component
- 函数&&嵌套函数的create
- DOM创建
- 添加到视图: while循环处理父级查找
7 点击事件 && 更新Props
Task
点击事件
- updateprops方法中根据传递的props的key的startsWith判断是否以on开头
- 对相应的dom添加事件绑定
更新props
- 获取新的DOM树——通过链表查找
- 初次render时, 获取对root的引用, 设为currentRoot;
- 另创建update函数, 其workUnit的dom和props为currentRoot的dom和props
- 找到旧DOM节点
- 通过props中设置altenate指针指向上一个节点实现
- props更新
- 对prevFiber和nextFiber的props进行遍历, 根据不同的情况设置更新或删除
8-11 更新children
Task
diff更新: 标签不一致时的创建与删除 Stage1: 删除第一层不同的老节点
- 删除旧节点, 创建新节点
- 对旧节点的删除
- 创建删除列表以批量删除, 在新老fiber的type不一致时(即需要创建新的时)push入列表;
- 在每一次渲染任务提交前进行旧DOM删除
Stage2: 删除多余的老节点(当新节点数量 < 老节点) 当前代码只能处理一层的原因: reconcileChildren中的遍历是针对新节点的children节点进行遍历
- 对链表关系进行分析, 基于oldFiber.sibling可以依次获取到oldFiber.parent的多余节点
- 针对oldFiber(老节点)进行while循环push加入到删除列表中, 并更新链表
Stage3: 针对边界情况考虑 当逻辑与出现在表达式{condition && Component}时
- 标签在不同的位置, 有不同的报错; 但本质上都指向一个问题——传递boolean(false)对VDOM && props diff的影响
- 传入false实际上是向fiber添加了child属性为false的children, 则根据添加这一操作可以过滤掉问题
- 在钩链关系中, 如果传入了false, 则需要在children层对链表的sibling进行跨越, 即忽略掉false带来的影响
Stage4: 优化代码 更新子组件时, 发生了不必要的子组件更新(全部的子组件都更新了一遍)——性能浪费
- 发生的原因: 渲染逻辑是执行整颗树
- 解决方法: 渲染部分树
- 确定开始位置: 当前组件
- 确定结束位置: 当前组件的sibling节点
12 实现useState
为什么useState的返回值要用数组接收?
- 数组的解构是有序的, 对象是无序的
- 多次调用useState, 对象需要重新命名
为什么Hooks组件需要在函数最外层作用域中定义和使用?
- 源码中按照index来指定Hook对应的组件, 以顺序指定; 如果被包在if等语句中, 不能保证语句块执行, 即顺序可能会错乱
实现步骤
- 定义useState函数, 满足useState的要求: 接收一个initial值, 数组形式返回state和setState方法; 其中setState集成赋值和视图更新两个功能;
- 设置新值, 获取旧值: 根据函数组件的原理, 在函数内部根据wipFiber(即函数本身的VDOM), 依照链表alternate获取上一个节点的state, 并以此来设置新节点的state;
- 在状态列表中指定状态: 为其新增属性: stateHooks集合, 并依据index来指定具体的state
13 useState action优化
React中使用useState()不会立即赋值和更新视图, React对其做了优化: 在每次的变化进行缓存, 在某些时刻进行批量处理, 即视图和数据是异步的;
- fiber中对stateHook属性额外设置queue, 用于存放setState时传递的action callback
- 对入队操作进行优化, 支持setState传递原始值——优化思路是逻辑复用, 将原始值进行包装, 包装为函数, 而函数本身已经拥有对应的处理
- 对渲染进行优化: 根据传递的action最终返回的值与现有的state值进行比较, 若相同则不进行处理
14 useEffect
重点: useEffect的执行时机; 执行时机和depth参数的关系 执行时机: DOM及其链表关系创建完之后, 即commitWork之后, 此时拥有完整的链表关系 功能编写:
- 核心为两个函数:
- useEffect函数用于收集effectHook描述对象, 将其绑定到wipFiber中
- commitEffect用于从dom root递归查找所有满足条件的节点, 并执行回调
- 一个函数组件中同时有多个useEffect实现:
- effectHook的基础上设置列表, 储存多个effectHook callback; 进入FC时列表初始化; 进入useEffect时push
- commitEffect相对应的单个callback调用改为循环调用
15 useEffect cleanup
cleanup本身用于清空副作用
- 在调用useEffect之前进行调用, 获取上一次调用的值
- 当depth的length为0, 即传递[]时, cleanup不会执行
功能编写:
- 存储: 声明在effectHook中, commitEffect时在执行callback的同时拿到返回的函数
- 调用:
- 找到位置: 在run之前;
- 赋值: callback执行时
- 编写cleanUp函数 && 在内部effect执行函数前执行: 递归依次检测, 有hooks就调用; 核心是执行的时oldFiber的hook