JSX的理解

当 Facebook 第一次发布 React 时,他们还引入了一种新的 JS 方言 JSX,将原始 HTML 模板嵌入到 JS 代码中。JSX 代码本身不能被浏览器读取,必须使用Babel和webpack等工具将其转换为传统的JS。很多开发人员就能无意识使用 JSX,因为它已经与 React 结合在一起了。

  • 可以将HTML语言直接写在JavaScript语言之中,不加任何引号,这就是JSX的语法,它允许HTML与JavaScript的混写。
  • JSX允许直接在模板插入JavaScript变量。如果这个变量是一个数组,则会展开这个数组的所有成员。
  • 防注入攻击:React DOM在渲染之前默认会过滤所有传入的值。它可以确保应用不会被注入攻击。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止XSS(跨站脚本攻击)。
  • Babel转译器会把JSX转换成一个名为React.createElement()的方法调用。

React 的生命周期方法有哪些

可分为三个阶段:挂载阶段、更新阶段和卸载阶段。同时,React还提供了钩子函数的方式(如useEffect钩子)来完成与生命周期相关的操作。

挂载阶段:

  • constructor,组件实例化时调用,用于初始化状态和绑定方法。
  • static getDerivedStateFromProps,在渲染之前调用,用于根据新的属性值计算并返回一个新的状态。
  • render,渲染组件的内容。
  • componentDidMount,组件挂载后调用,可以进行异步操作、订阅事件等。

更新阶段:

  • static getDerivedStateFromProps,在渲染之前调用,用于根据新的属性值计算并返回一个新的状态。
  • shouldComponentUpdate,在渲染之前调用,用于决定是否重新渲染组件,默认返回true。
  • render:渲染组件的内容。
  • getSnapshotBeforeUpdate:在最终将内容渲染到DOM之前调用,用于获取DOM更新前的快照。
  • componentDidUpdate:组件更新后调用,可以进行DOM操作、发起网络请求等。

卸载阶段:

  • componentWillUnmount:组件卸载前调用,可以进行清理操作,如取消订阅、清除定时器等。

其他:

  • static getDerivedStateFromError:在子组件渲染过程中,如果发生错误,会调用该方法,返回一个新的状态。
  • componentDidCatch,在子组件渲染过程中,如果发生错误,会调用该方法,用于记录错误信息或上报错误。

React的Hooks详解

Hook 是一些可以让你在函数组件里”钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用——这使得你不使用 class 也能使用 React。React 内置了一些像 useState 这样的 Hook。你也可以创建你自己的 Hook 来复用不同组件之间的状态逻辑。

React提供了多个常用的Hooks,用于在函数组件中管理状态、处理副作用和访问React的上下文等。

  • useState:用于在函数组件中添加状态。
  • useEffect:用于在函数组件中执行副作用操作。
  • useContext:用于在函数组件中访问React的上下文。
  • useReducer:用于在函数组件中使用Reducer模式来管理状态。
  • useRef:用于在函数组件中创建可变的引用。
  • useMemo:用于在函数组件中缓存计算的值。
  • useCallback:用于在函数组件中缓存函数。
  • useLayoutEffect:类似于useEffect,但在DOM更新之后同步执行。
  • useImperativeHandle:用于在函数组件中自定义外部组件实例的暴露。
  • useDebugValue:用于在自定义Hooks中显示自定义的调试值。

React和Vue.js的相似性和差异性是什么

相似性:

  • 组件化:React和Vue都推崇组件化的开发方式,允许开发者将UI拆分成独立、可复用的组件。
  • 虚拟DOM:两者都使用了虚拟DOM来提高渲染性能,通过最小化需要实际更新的DOM元素来提升性能。
  • 响应式数据绑定:虽然实现方式不同,但React和Vue都提供了响应式数据绑定的机制,使得数据和UI能够自动同步。
  • 单向数据流:React和Vue都倾向于使用单向数据流(尽管Vue在某些情况下也支持双向绑定)。

区别:

  • 性能优化:React:通过Fiber架构改进了渲染性能,使得React在处理大型应用时更加高效。Vue:通过虚拟DOM的优化和一些内置的指令(如v-once)提供了一些性能优化手段。
  • react数据不可变,Vue主要是基于是数据可变的响应式思想,当属性变化的时候,响应式的更新对应的虚拟dom。
  • 编译&写法:React:思路是all in js,通过js来生成html,所以设计了jsx,还有通过js来操作css,社区的styled-component、jss等。Vue:把html,css,js组合到一起,用各自的处理方式,Vue有单文件组件,可以把html、css、js写到一个文件中,html提供了模板引擎来处理。

React的一些主要优点

  • 提高了应用程序的性能
  • 可以方便地在客户端和服务器端使用
  • 由于有了JSX,代码的可读性提高了
  • 使用React,编写UI测试用例变得非常容易

如何理解React State不可变性的原则

在 React 中,不可变性是指数据一旦被创建,就不能被修改。React 推崇使用不可变数据的原则,这意味着在更新数据时,应该创建新的数据对象而不是直接修改现有的数据。

  • 数据一旦创建就不能被修改:在 React 中,组件的状态(state)和属性(props)应该被视为不可变的。一旦创建了状态或属性对象,就不应该直接修改它们的值。这样可以确保组件的数据在更新时是不可变的,从而避免意外的数据改变和副作用。
  • 创建新的数据对象:当需要更新状态或属性时,应该创建新的数据对象。这可以通过使用对象展开运算符、数组的 concat()、slice() 等方法,或者使用不可变数据库(如Immutable.js、Immer 等)来创建新的数据副本。
  • 比较数据变化:React 使用 Virtual DOM 来比较前后两个状态树的差异,并仅更新需要更新的部分。通过使用不可变数据,React 可以更高效地进行比较,因为它可以简单地比较对象引用是否相等,而不必逐个比较对象的属性。
  • 性能优化:使用不可变数据可以带来性能上的优势。由于 React 可以更轻松地比较前后状态的差异,可以减少不必要的重新渲染和组件更新,提高应用的性能和响应性。

不可变性的原则在 React 中有以下好处:

  • 简化数据变更追踪:由于数据不可变,可以更轻松地追踪数据的变化。这样可以更好地理解代码的行为和数据的流动。
  • 避免副作用:可变数据容易引发副作用和难以追踪的 bug。通过使用不可变数据,可以避免许多与副作用相关的问题。
  • 方便的历史记录和回滚:不可变数据使得记录和回滚应用状态的历史变得更容易。可以在不改变原始数据的情况下,创建和保存不同时间点的数据快照

React Fiber

是React 16版本中引入的一种新的协调引擎,旨在解决旧版React在处理大型应用时的性能和灵活性问题。Fiber的核心设计理念包括增量渲染、可中断更新和优先级控制,这些特性使得React能够更好地管理组件树的更新和渲染过程。

Fiber的设计理念和核心特性:

  • 增量渲染‌:Fiber将渲染任务拆分成多个小任务,每次处理一部分任务,完成后将控制权交回浏览器,再继续处理剩余任务。这种方式避免了主线程的长时间占用,提升了页面的响应速度。
  • 可中断更新‌:Fiber允许渲染过程在必要时被中断,以便优先处理高优先级的任务(如用户输入)。这通过将渲染工作分解为多个小任务并在主线程空闲时执行这些任务来实现。
  • 优先级控制‌:Fiber为每个更新任务分配优先级,确保高优先级的任务能够快速得到处理,而不会被低优先级的任务所阻塞。这通过React内部的”优先级队列”来实现‌。

Fiber的工作原理:Fiber通过任务切片(Time Slicing)将渲染任务分解为多个小任务,这些小任务在浏览器空闲时逐步执行。使用requestIdleCallback或scheduler调度渲染任务,确保高优先级的任务能够优先处理。此外,Fiber使用双缓冲机制(Double Buffering),维护两棵Fiber树:当前树和工作进行树,更新完成后交换角色,确保渲染的连贯性‌。

将当前时间分片(5ms)结束后,若任务还没执行完,将任务暂时挂起到任务队列(记录当前fiber节点的位置),让出主线程去执行用户输入/ui渲染/网络等高优先级操作,如果高优先级操作产生高优先级的任务,此时需要马上响应,将高优先级放到任务队列顶层,等到下一次事件循环时取出顶层任务(高优先级任务or挂起的任务)继续执行。按以上机制循环往复,直到执行完毕。(每个任务都有自己的过期时间,如果到了过期时间他也会变成一个类似高优先级任务,在下一个时间片(5ms)优先执行。

diff算法

在React中,diff算法是用来比较新旧虚拟DOM(Virtual DOM)的差异,并生成更新补丁以最小化dom操作,从而提高性能。

    1. 树的比较,React使用一种称为”深度优先”的策略来比较两个虚拟DOM树。这意味着它会从根节点开始,逐层向下比较,直到找到不同的部分。
    1. 不同类型的元素,当比较两个根节点时,如果它们的类型不同(例如,从<div>变为了<span>),React会销毁旧树并建立新树。这是因为不同类型的元素代表了完全不同的DOM结构。
    1. 类型相同但属性或子元素不同,如果两个元素的类型相同,React会比较这两个元素的其他属性。如果一个元素有新的属性或者属性值发生了变化,React会更新该DOM元素的属性。
    1. 子元素的比较,对于子元素的比较,React使用”key”属性来提高效率。带有key的列表元素可以更快地识别哪些项被添加、删除或移动了。如果没有key,React将使用一种启发式方法来最小化DOM操作。
  • 优化策略。批量更新:将多个小的更新合并成一次大的更新,减少重绘和重排。选择性渲染:使用shouldComponentUpdate或React Hooks中的React.memo来避免不必要的渲染。纯组件和函数:确保组件是无副作用的纯函数,只根据props和state计算结果。

代码的优化策略:

  • 根据diff算法的设计原则,应该尽量避秒跨层级的节点移动
  • 通过设置唯一的key进行优化
  • 尽量减少组件层级的深度,因为过深的层级会加深遍历深度,带来性能问题
  • 设置shouldComponentUpdate或者React.memo等减少组件diff次数

参考

React需要同时维护两棵虚拟DOM树:一棵表示当前的DOM结构,另一棵在React状态变更将要重新渲染时生成。React通过比较这两棵树的差异,决定是否需要修改DOM结构,以及如何修改。这种算法称作Diff算法。

Diff算法会对新旧两棵树做深度优先遍历,遍历到一个节点,就将新旧两棵树作比较,并且只对同一级别的元素进行比较

React diff算法具体策略:

  • tree diff:tree diff主要针对的是React dom节点跨层级的操作。由于跨层级的DOM移动操作较少,所以React diff算法的tree diff没有针对此种操作进行深入比较,只是简单进行了删除和创建操作。
  • component diff:component diff是专门针对更新前后的同一层级间的React组件比较的diff 算法。 - 如果是同一类型的组件,按照原策略继续比较 Virtual DOM 树(例如继续比较组件props和组件里的子节点及其属性)即可。 - 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点,即销毁原组件,创建新组件。 - 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切知道这点,那么就可以节省大量的 diff 运算时间。因此,React 允许用户通过 shouldComponentUpdate()来判断该组件是否需要进行 diff 算法分析。
  • element diff:element diff是专门针对同一层级的所有节点(包括元素节点和组件节点)的diff算法。当节点处于同一层级时,diff 提供了 3 种节点操作,分别为 INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。

key机制:

  • key的作用:当同一层级的某个节点添加了对于其他同级节点唯一的key属性,当它在当前层级的位置发生了变化后。react diff算法通过新旧节点比较后,如果发现了key值相同的新旧节点,就会执行移动操作(然后依然按原策略深入节点内部的差异对比更新),而不会执行原策略的删除旧节点,创建新节点的操作。这无疑大大提高了React性能和渲染效率。
  • key的工作流程:首先,对新集合中的节点进行循环遍历 for (name in nextChildren),通过唯一的 key 判断新旧集合中是否存在相同的节点 if (prevChild === nextChild),如果存在相同节点,则进行移动操作,但在移动前需要将当前节点在旧集合中的位置与 lastIndex 进行比较 if (child.mountIndex < lastIndex),否则不执行该操作。

key使用注意事项:

  • 如果遍历的列表子节是作为纯展示,而不涉及到列表元素顺序的动态变更,那使用index作为key还是没有问题的。
  • key只是针对同一层级的节点进行了diff比较优化,而跨层级的节点互相之间的key值没有影响。
  • 大部分情况下,通过遍历的同一层级的使用了key属性的元素节点其节点类型是相同的(比如都是span元素或者同一个组件)。如果存在新旧集合中,相同的key值所对应的节点类型不同(比如从span变成div),这相当于完全替换了旧节点,删除了旧节点,创建了新节点。
  • key值在比较之前都会被执行toString()操作,所以尽量不要使用object类型的值作为key,会导致同一层级出现key值相同的节点。key值重复的同一类型的节点或组件很可能出现拷贝重复内部子元素的问题。

React和Vue的异同点

相同点:

  • 组件化
  • 函数式编程
  • 虚拟dom等

不同点:

  • 设计理念,React组件就是函数、编写组件的语法是JSX,本质就是Javascript;Vue内置了很多黑魔法,比如单文件组件、指令等。
  • 组件存在的形式
  • 数据可变性
  • diff优化策略
  • 响应式原理,vue(数据劫持+发布/订阅模式),react(状态驱动+单项数据流)

react hooks的实现原理

  • Hooks 是一些特殊的函数,如 useState、useEffect 等,它们允许在函数组件中”钩入” React 的特性。例如,useState 允许函数组件拥有状态,而 useEffect 处理副作用。
  • 通过链表存储 Hooks,在 React 内部,每个函数组件对应一个 Fiber 节点,Fiber 节点的 memoizedState 属性指向一个单向链表,链表中的每个节点代表一个 Hook。这种结构确保了 Hooks 的调用顺序,从而在每次渲染时正确地关联状态。
  • 使用闭包保存状态,Hooks 利用 JavaScript 的闭包特性,在组件的多次渲染之间保持状态。例如,useState 返回的状态值和更新函数通过闭包与组件的渲染周期关联,从而在状态更新时触发重新渲染。

react hooks的好处

  • 代码的组织
  • 代码的复用
  • 状态的传递

react 18并发渲染的实现原理

  • 时间切片
  • 优先级控制

为什么react 18之前没有自动批处理

参考

react 架构

React 并发模式到底是个啥?
react fiber 到底有多细
从中断机制看 React Fiber 技术
React的并发悖论
深入理解 React 并发模式(Concurrent Mode):原理、优势与实践
React源码学习00: React架构变化&渲染流程
React - setState 原理

在React中 调用 setState 之后发生了什么

  1. 状态更新请求:setState接收一个新的状态对象或一个返回状态对象的函数,告诉React”我要更新状态啦”。
  2. 状态合并:React会把你传入的新状态和当前状态进行合并,而不是直接替换。这就好比你给手机充电,新充的电会加到原来的电量上,而不是完全替换。
  3. 异步批量更新:React并不会在你调用setState之后立即更新DOM,而是会把多个setState调用合并成一次更新。这样做是为了提高性能,避免频繁的DOM操作。
  4. 重新渲染:当React决定要更新时,它会调用组件的render方法,生成新的虚拟DOM。
  5. Diff算法:React会用Diff算法对比新旧虚拟DOM的差异,找出需要更新的部分。
  6. DOM更新:最后,React会把计算出的差异应用到实际的DOM上,完成页面的更新。

fiber到底有多细

为什么需要react fiber

React性能优化

  • 使用React.memo、useMemo和useCallback减少不必要的渲染
  • 懒加载组件:使用React.lazy和Suspense实现组件的按需加载,减少初始加载时间
  • 使用状态提升或Context API避免prop层层传递:当组件层级较深时,避免通过props一层层传递数据,提高组件性能和可维护性
  • 避免不必要的状态和计算:只保存必要的状态,避免在render方法中进行复杂计算。
  • 并发渲染:使用useTransition、useDeferredValue启用并发渲染