跳到主要内容

谈谈React相关原理

谈谈React diff算法​

diff算法基于三个约定:1、key相同、2、type相同 3、同层级

否则都算是新节点

React diff算法原理​

diff 算法 虚拟 DOM 树发生变化后,通过对比新旧虚拟DOM 树的变更差异,将更新最小差异更新到真实 DOM,只针对改变的部分进行更新,而不用整体重新渲染,以最小成本完成视图更新

  • react会通过map数据结构存储旧的fiber节点,如果在遍历新旧节点的过程,发现能复用,则从map数据结构中删除旧节点

从源码层面分析:

在react-reconciler这个包,主要核心逻辑是diff算法、 构建虚拟 DOM 树等

  • 首先diff的入口 是 reconcileChildren 会判断,如果首次渲染,current 空 null (即没有旧的Fiber树),就通过 mountChildFibers 创建子节点的 Fiber 实例。如果不是首次渲染,就调用 reconcileChildFibers去做 diff

首次渲染会通过 mountChildFibers 创建子节点的 Fiber 实例

更新渲染时会调用 reconcileChildFibers 方法进行diff 操作

  • React Diff核心逻辑 是reconcileChildFibers这个方法,会进行同级比较
    • 在这个方法中,React会比较新旧子节点的同级节点,检查它们的类型和属性是否相同
  • 节点类型不一样,那么React会直接删除该节点,然后直接创建新的节点插入到其中
  • 如果两个节点类型相同,属性也相同,但是子节点不同,那么对子节点进行递归比较
    • 对于多节点会通过reconcileChildrenArray方法来比较

1、如果新旧节点的key相同,属性也相同,那么这个节点可以进行复用

React 与 Vue 的 diff 算法 的区别?​

相同点:

  • 都用key做为唯一标识,会根据key判断元素是否要删除
  • 遍历前都会根据老的节点构建一个map,方便根据key快速查找
  • vue和React的diff算法都是只对同级节点进行对比,不做跨层级比较

不同点:

  • Vue diff使用双向链表, 从两头向中间进行遍历

  • React diff使用了单向链表,从左到右进行遍历

  • 对静态节点的处理不一样。

    • 由于 Vue 是通过 template 模版进行编译的,所以在编译的时候可以很好对静态节点进行分析然后进行打补丁标记,然后在 Diff 的时候,Vue2 是判断如果是静态节点则跳过过循环对比,而 Vue3 则是把整个静态节点进行提升处理,Diff 的时候是不过进入循环的,所以 Vue3 比 Vue2 的 Diff 性能更高效
    • React 因为是通过 JSX 进行编译的,是无法进行静态节点分析的

    Vue 是不提倡使用 JSX的,使用也没有问题,Vue 使用了 JSX 就没办法静态编译优化了

    JSX本质是js语言,是动态的,很难做静态分析,所以无法做编译时优化,React的性能优化集中在运行时进行处理

    vue template是静态的声明,可以在编译阶段优化

谈谈React18 严格模式有什么特点​

  • 1、react18 严格模式下,useEffect在开发环境React会渲染执行2次,确保开发者可以提前发现问题、铺垫新功能,如离屏渲染,未来的离屏渲染需要执行多次的
    • 在fiber架构下,render执行的次数是不确定的
  • 2、检测一些废弃方法的警告

如何解决useEffect会执行两次​

  • 注意:正确的问题不是“怎么样让 useEffect 执行一次”,而是“怎样修复我的 useEffect,让它在(重复)挂载之后正常工作”

    • 也可以理解,毕竟在 React 的未来版本中做离屏渲染的时候 useEffect 肯定会多次执行的。 而且,即使是当前版本,在做页面的前进后退也会面临触发多次 useEffect
    • 解决办法其实就是解决 重复挂载卸载之后 应用正常工作了

    异步请求数据​

    异步请求数据,写在useEffect会执行多次,导致多次请求

    • 缓存接口返回的数据,下次请求的时候如果已经有缓存数据了就直接用,无须再次发起请求

    图表多次渲染​

    由于渲染了2次,导致创建了2个实例,出现了2个图表

    • 通过缓存可以解决,写一个缓存函数,比如将图表的id存到缓存中,如果id存在,则无需重复执行,id不存在,则执行渲染图表

谈谈React fiber​

  • 可以使虚拟dom可以进行增量式渲染,可以把一个大的渲染任务分为很多小阶段,按照优先级来执行这些任务分片,优先级高的会先执行

  • fiber可以打断diff算法

    • 渲染过程中可以中断当前任务,处理更高优先级的任务,然后再回来继续之前中断的任务
  • 合理分配CPU资源,从而提高用户的响应速度

fiber之前是怎么样的?​

  • React会递归对比虚拟DOM,找出变动的节点,然后同步更新这些节点

    • 这个过程中,React会一直占用着浏览器资源,

      • 1、会导致用户触发事件得不到响应
      • 2、导致掉帧,用户可能会感觉到卡顿
    • 遍历递归过程中,执行栈会越来越深,就会很卡,而且不能中断,因为中断了,恢复就很难了

    • 递归太消耗性能

谈谈React渲染流程​

react 的渲染流程是 render + commit。

  • render 阶段实现 vdom 虚拟DOM 转 fiber树以及reconcile调度处理

    • vdom 虚拟DOM 转 fiber树

    • reconcile调度处理:可以更好的管理任务,安排任务的执行, 实现渲染任务的优先级,reconcile调度支持任务的中断,当更高优先级的任务进入任务队列时,它可以打断当前正在执行的任务

  • commit 阶段更新dom,执行effect副作用逻辑

    • 执行增删改dom节点,更新ref,执行副作用
      • 更新ref的理解:组件dom发生变化,需要更新对应的ref的引用,来确保ref指向对应的dom节点
    • 完成所有更新之后,渲染阶段结束

谈谈什么是受控组件​

  • 将组件完全交给state状态管理控制,不允许直接修改组件的状态state
  • 非受控组件是值 组件的状态 不受React状态state来控制,由 DOM 元素本身来管理

谈谈React中如何避免不必要的渲染?​

  • shouldComponentUpdate和PureComponent
    • 在React类组件中,可以利用shouldComponentUpdate或者PureComponent来减少因父组件更新而触发子组件的render,从而达到目的。shouldComponentUpdate来决定是否组件是否重新渲染,如果不希望组件重新渲染,返回false即可。
  • 利用高阶组件
    • 在函数组件中,并没有shouldComponentUpdate这个生命周期,可以利用高阶组件, 将渲染逻辑、状态修改封装在高阶组件内部去处理,可以减少render渲染次数
  • 使用React.memo配合useMemo或者useCallback
    • React.memo是React16新的一个API,用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与PureComponent十分类似,但不同的是,React.memo只能用于函数组件

谈谈React Hook​

React Hook实现原理​

hooks的实现原理 是 用了闭包 + 链表,通过链表来保存组件的状态,通过下标索引来读取对应的数据信息

  • hooks的实现原理 是 用了闭包 + 链表

    • 通过闭包环境特性,可以用来保存组件状态,使react组件每次渲染的时候,组件内部状态 保持着之前引用关系,可以保持拿到之前的状态,这样就可以保持这个状态的连续性
    • 通过闭包来保存状态 (闭包保存当前 Hook 的索引)
    • 每次组件渲染时,React 会重新执行这些 Hook,通过闭包保持当前hooks的索引, 确保状态的一致性
  • React Hooks 是用链表来保存这些hooks

    • 通过下标索引来读取对应的hooks数据状态信息
    • 如useState出现的顺序来确定每个state的key的
  • hooks 链表是保存在fiber节点上

    • 链表的每个节点就是表示一个fiber
    • fiber:是React核心算法的重新实现,fiber的出现让每个节点都有了实例,拥有了保存状态的能力
    • fiber就是时间切片,合理分配CPU资源,从而提高用户的响应速度

React-Hooks解决的问题​

  • 组件之间复用状态逻辑很难
    • 没有hooks之前,复用状态逻辑,使用高阶组件方式,react-redux中的connect,react-router中的withRouter,嵌套层级比较多,使代码难以理解难以维护
  • 通用业务逻辑的抽取,进行业务逻辑复用
  • class的this指向复杂(需要bind函数,箭头函数去解决),函数组件不涉及this的问题
    • 函数组件 不是类组件,没有实例的概念,调用组件不再是创建类的实例,而是把函数执行,所以函数组件不会存在this的问题
  • hooks也解决的高阶组件的问题,比如hooks属性可以重命名(解构出来重命名),不会造成嵌套地狱 、属性名冲突难以溯源问题,维护性高

React Hook的使用限制有哪些?​

React Hook的使用限制有哪些?​

  • 只能在函数内部最外层调用hooks

React Hooks 的限制主要有两条:

  • 不要在循环、不要在条件或嵌套函数中调用 Hook;

    • 因为hooks要保证相同的顺序进行渲染
  • 保证顺序

    • 使用的是单链表来存储状态的,比如useState出现的顺序来确定每个state的key的
    • 通过下标索引来读取对应的hooks的状态
    • 所以要保证这个链表顺序一致,hooks的结果才能正确

谈谈useState的更新机制​

useState的更新机制 内部进行了性能优化, 相同值不会重新渲染

  • 1、不会立即更新状态和视图,而是加入到队列中
  • 2、当上下文代码都执行处理完毕之后,会让更新队列中的任务,统一渲染/更新一次 【批处理执行一次】

谈谈setState原理​

setState 什么时候是同步,什么时候是异步?​

  • 在react18和react16中,setState是同步还是异步,是有一些区别的
    • react18中,无论在什么地方执行setState,它都是异步的 【这是react18的新特性 批处理】
    • react16中,setState 在合成事件和钩子函数【周期函数】中是异步的,setState在原生事件 【setTimeout、手动获取DOM元素】中是同步的

为什么setState需要异步的

react会进行有意的等待,为了提高性能,减少渲染次数, 放在一个队列里进行合并的操作,用 enqueueUpdate方法执行state更新

谈谈react的事件机制​

  • React基于浏览器的事件机制自身实现了一套事件机制,包括事件注册、事件的合成、事件冒泡等

  • React16之前版本(包含16.x)合成事件:

    • 事件委托到document, 部分事件还是会绑定到当前元素, 如focus 和 blur
      • 使用事件委托,避免大量的事件监听,减少了内存消耗
    • 存在react事件和原生事件的映射关系
      • 将合成事件映射到对应的原生事件上
    • 事件池机制
      • 使用事件池机制管理合成事件对象的创建和销毁
  • React17后的合成事件:

    • 事件委托到root
    • react的 captrue捕获阶段的合成事件会提前到原生事件的capture阶段执行
    • 移除了事件池的机制
    • 事件有优先级

为什么react要自己实现一套事件机制?

  • 各种浏览器的行为统一,兼容不同浏览器事件的差异
  • 多平台适配

谈谈react的高阶组件​

React高阶组件,它描述的便是接受React组件作为输入,输出一个新的React组件的组件

  • 在之前class类组件的时候,会经常使用高阶组件来做代码逻辑复用,现在有了hooks,可以解决高阶组件的缺点,以及对于代码复用效率会更高

使用场景:

  • 代码逻辑重用

  • 路由权限控制

    • 高阶组件可以接受React组件作为参数,经过一系列处理之后 【权限的判断操作】,返回新的React组件
  • 属性代理

    • 被处理组件的 props属性和新的 props属性一起传递给新组件

缺点:

  • 难以溯源。如果原始组件A通过好几个HOC的构造,最终生成了组件B,不知道哪个属性来自于哪个HOC,需要翻看每个HOC才知道各自做了什么事情,使用了什么属性。
  • props属性名的冲突。某个属性可能被多个HOC重复使用。
  • 子组件接受数据不够灵活 :对数据的处理上:在高阶组件中,目标组件对于数据的获取没有主动权,数据的分发逻辑/处理逻辑全部在高阶组件的内部, 即目标组件接受的数据是高阶组件内部规定好的,不能有选择性的进行处理,不够灵活

谈谈react的错误捕获处理​

在react中,如果在渲染阶段出现异常, 整个组件树都会被卸载掉,因此直接会出现空白页面直接就白屏了

  • 通过处理组件的错误边界,及时组件报错了,也不会直接导致页面空白,可以打印错误信息,并且渲染出指定的错误页面

  • 错误的边界是需要用到class component类组件实现

    • 一个class组件中定义了state getDerivedStateFromError()或componentDidCatch()这2个生命周期方法的任意一个(或2个)时候,那么它就会变成一个错误边界

    当抛出错误后,使用state getDerivedStateFromError()渲染备用UI,使用componentDidCatch()打印错误信息

  • 可使用react-error-boundary库处理,对于组件的错误处理可以更加的强大

谈谈React context​

  • 可以通过创建Context上下文来实现组件之间的通信

context的缺点:

  • 可能会导致组件【子组件】多余的更新 【 context 重复渲染导致的性能问题】
  • 一个context对象,只要其中一个属性更新,所有的子组件只要用到context对象都会更新而重新render渲染
    • 可以对 context value 使用 useMemo,对子组件使用 React.memo 包裹,来减少组件渲染次数

谈谈useEffecft的理解​

  • useEffect 是异步的,会在浏览器渲染结束之后才异步执行的副作用,因此副作用是放在useEffect的回调是更合适的
    • useLayoutEffect同步的,会在DOM改变之后同步调用副作用,缺点就是会阻塞代码,比如阻塞视图的更新
  • 对于某些“副作用”的渲染,比如异步接口请求,事件绑定等操作我们通常都放在 useEffect 中执行

React18的useEffect会执行两次问题?

  • 这个是 React18 useEffect 新特性,仅在开发模式(“development”)下,且使用了严格模式(“Strict Mode”)下,会两次执行 hooks,生产环境(“production”)模式下和原来一样,仅执行一次

  • 之所以执行两次,是为了模拟立即卸载组件和重新挂载组件

  • 也是为了以后 React的新功能做铺垫