众所周知,React是目前互联网大厂使用最多的前端框架,在面试过程中,面试官也更喜欢有React开发经验的同学。本文总结了React中一些重要的特性,同时也是在平时的面试中经常被问到的一些问题。希望对各位前端开发同学有所帮助,如果觉得还不错请多多点赞收藏。 目录 如何理解 Rect React与Vue的异同 如何理解虚拟 DOM React 生命周期 React 状态复用的方式 如何理解 React Hooks React Diff 算法 React fiber 架构的理解 react 性能优化的手段 setState 是同步的还是异步的 React 合成事件 如何理解 Rect react 是什么? react 是一个用于构建用户界面的 javascript 库。 为什么会出现 react 这样的框架 在我们早期的前端开发中,我们通过 JavaScript 来手动操作 DOM 以达到和用户交互的目的,但是原生的 DOM 操作繁琐并且还会存在兼容性问题。因此在我们前端开发中出现 jquery 这样的框架,来帮助我们简化 DOM 操作,同时解决了各种兼容性问题。但是随着我们的项目变得越来越复杂,我们发现,光有 DOM 操作远远不够,我们还要有应用结构,因此我们开始探讨前端组件化的问题,通过组件化的方式来让我们的项目结构变得更加清晰,同时也引入了 MVX 的概念。接着大家发现 MVX 的数据绑定依然需要我们手动监听 model 的变化,然后去做各种 DOM 操作,数据到视图层的映射依然繁琐。于是大家就开始推崇 MVVM 的数据绑定,于是就出现了例如 Angular,React 和 Vue 这样的前端框架。 react 的特点 react具有声明式,组件化以及一次学习,随处编写的特点,因此 react 特别适合用于构建快速响应的大型 web 应用 React与Vue的异同 相同点: 使用虚拟 DOM 提供响应式和组件化的视图组件 将注意力集中保持在核心库,而将其他功能如路由、全局状态管理交给相关的库 不同点: 视图更新 在react中,当某个组件的状态发生变化的时候,会以该组件为根,重新渲染整个组件树,如果要避免不必要的子组件重渲染,我们需要在所有可能的地方使用 PureComponent 或者是 shouldComponentUpdate 方法。 在 Vue 应用中,组件的依赖是在渲染过程中自动追踪的,所以vue能精确知晓哪个组件确实需要被重渲染。Vue的特点可以让开发者不再考虑此类优化,从而更好地专注于应用本身 HTML 和 CSS 在 react 中,everything is JavaScript,不仅是 html 可以用 jsx 表示,连 css 也有 js 的解决方案。 Vue 的整体思想是拥抱经典的 Web 技术,并在其上进行扩展。使用 html,css 和 js 来构建模板。 在这个方面我觉得 vue 是在适应开发者,而 react 想要改变开发者。 同时,在组件作用域内的 css 上,react 是使用的 css-in-js 的解决方案或者是 css modules。而 vue 使用 scoped 来控制 css 规模 vue 的路由管理和状态管理由官方维护,而 react 则是把这些问题交给社区,创建了一个更为分散的生态系统,因此react的生态系统相对于 vue 更加繁荣。 原生渲染 由于两者都是使用的虚拟 dom 技术,因此都可以构建原生的安卓或者 ios 应用,例如 react-native 和 weex 如何理解虚拟 DOM 虚拟 dom 是什么 虚拟 DOM 是对 DOM 的抽象,本质上是 JavaScript 对象 为什么需要虚拟dom 首先,我们知道在前端性能优化中,有一个很重要的一项就是尽可能少的操作 DOM,不仅仅是 DOM 操作相对较慢,更是因为频繁的 DOM 操作会造成浏览器的回流或者重绘,这些都会对我们的性能造成影响。因此我们需要一个抽象,在patch过程中,尽量一次将差异更新到 DOM 中。 其次,现代前端框架的一个基本要求就是无须手动操作DOM,这样可以大大提高开发效率。 最后,虚拟DOM可以更好地实现跨平台,用于编写原生应用。 React 生命周期 React < 16.3 组件的生命周期包含挂载或者更新组件过程中被触发的一系列方法。这些方法可以在组件渲染UI之前或者之后触发。以下是 React 16.3 之前的生命周期 挂载生命周期 constructor(props):初始化 props 和 State。 componentWillMount(): 一旦属性被获取并且也初始化了 State,该方法将会被触发。该方法是在 DOM 被渲染之前触发的,并且可以用来初始化第三方脚本库、启动动画、请求数据,以及其他可能需要在组件被渲染之前执行的额外步骤。还可以在该方法中触发 setState 方法,在组件被初次渲染之前修改组件的 State。 在组件被渲染完毕之前调用 setState 方法将不会启动更新生命周期。在组件渲染完毕之后调用 setState 方法就会启动更新生命周期。 如果用户在 componentWillMount 方法中定义的回调函数内调用了 setState 方法,那么它将会在组件被渲染完成之后被触发,并且会启动更新生命周期。 render():只要父组件 render 被调用,其所有子组件的 render 都会被调用 componentDidMount():组件被渲染完毕之后触发,可以获取到 DOM。 componentWillUnmount():组件被卸载之前触发,在这里可以清除一些后台进程。 更新生命周期 更新生命周期是当组件 State 发生变化或者从父组件接收到新的属性时触发的一系列方法。 更新生命周期会在每次调用 setState 方法后启动。调用 setState 方法过程内部的更新生命周期将会引发无限递归循环。因此只能在 componentWillReceiveProps 方法内部调用 setState 方法,它允许组件属性被更新后更新 State。 更新生命周期包括以下几个方法: componentWillReceiveProps(nextProps): 仅当新的属性被传递给了组件后才会调用。这也是唯一可以调用 setState 方法的地方。 shouldComponentUpdate(nextProps, nextState): 更新生命周期的门卫,一个可以取消更新操作的谓词。该方法可以通过只允许执行必须的更新操作来改进性能。 componentWillUpdate(nextProps, nextState): 只在组件更新之前触发。和 componentWillMount 类似,只是它会在每次更新操作之前被触发。 componentDidUpdate(prevProps, prevState): 只在更新操作发生后,调用 render 方法之后触发。类似 componentDidMount 方法,不过它会在每次更新之后触发。 React >= 16.4 React 大于等于 16.4 的生命周期图谱如下: 还有一些不常用的生命周期钩子: 这里我们着重介绍 getDerivedStateFromProps生命周期。需要注意的是,React 16.3 的版本中 getDerivedStateFromProps 的触发范围和 16.4^ 是不同的,主要区别在 setState 和 forceUpdate 时会不会触发。具体可以看生命周期图。 该生命周期的使用场景主要有两个: 无条件的根据 prop 来更新内部 state,也就是只要有传入 prop 值,就更新 state。 只有 props 值和 state 不同时才更新 state。 具体示例可以参考官方文档。 React 状态复用的方式 mixin mixin之间的相互依赖会形成依赖链,当我们改动其中一个mixin的状态的时候,很可能会直接影响其他的mixin,不利于代码维护 不同mixin的之间可能会产生命名冲突 增加组件的复杂性,当我们引入的mixin较多时,会让代码的逻辑变得十分复杂,难以维护。 高阶组件 HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难。 render props render props是一项通过props来告知组件需要渲染什么内容的技术,它的使用场景是什么呢? 很多时候我们渲染一个组件,但是它的逻辑和数据却依赖于父组件,这种情况下我们可以把那部分可以复用的逻辑抽取在父组件中,并且在父组件暴露一个参数 render 来接收渲染子组件的方法,并且通过这个方法把子组件所依赖的数据传给它,这种方式就是render props。 render props的使用思路是控制反转,子组件通过render props的方式定义好渲染自身的函数,父组件再把子组件需要的数据传给这个函数,子组件拿到数据后可以随意的渲染自身。 hooks 减少状态逻辑复用的风险,mixin引入的状态逻辑会相互覆盖,HOC也可能会出现 props 覆盖 避免层级嵌套,让组件更易于理解 使用函数代替 class ,更符合 react 函数式理念 问题 只能在顶层使用 hooks 只在 React 函数中使用 hooks 如何理解 React Hooks 是什么 hook 是 react16.8 的新增特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 react 特性 解决了什么问题 react解决了我们在使用和维护 react 中经常遇到的问题 组件之间的状态和逻辑复用 以前使用 render props 和 高阶组件。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。 复杂组件变得难以理解 我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。例如,组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。 难以理解的 class class 组件中经常需要手动去绑定 this,因此需要我们去理解 js 中 this 的工作原理,增加了学习成本 有哪些 useState useEffect useRef useContext useMemo useCallback React Diff 算法 React的 diff 策略: 在 Web UI 中,跨层级移动节点的操作很少,可以忽略不计 拥有相同类的两个组件会生成相似的树形结构,拥有不同类的两个组件会生成不同的树形结构 对于同一层级的一组子节点,可以通过唯一 id 进行区分 基于以上策略,React 分别对 Tree diff、Component diff 以及 Element diff 进行了优化。 tree dif 在 tree diff 的时候,react 只会对两颗树的同一层级的节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。 如果出现了跨层级的移动,React会删掉该节点以及其所有的子节点。然后在新的节点下创建节点。 如下图,A 节点(包括其子节点)整个被移动到 D 节点下,由于 React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。此时,React diff 的执行情况:create A -> create B -> create C -> delete A。 只有删除、创建操作,没有移动操作 component diff React 是基于组件构建的应用,因此在进行 diff 时,react 遵循以下策略: 如果是同一类型的组件,则按照原策略继续比较 DOM tree 如果是不同类型的组件,react 会替换整个组件下的所有子节点 对于同一类型的组件,其虚拟dom可能并没有发生变化,因此react提供shouldComponentUpdate来让我们判断该组件是否需要diff。 如下图,当 component D 改变为 component G 时,即使这两个 component 结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除 component D,重新创建 component G 以及其子节点。 element diff 当节点处于同一层级的时候,react diff 提供了三种节点操作,移动、删除、插入。 当节点没有 key 的时候,在对比新老虚拟DOM的时候,会出现频繁的创建,删除节点的操作,不能很好的做到节点复用,影响 diff 的效率。 因此我们在对同一层级的子元素进行比较的时候,需要添加唯一的key进行区分。 有了 key 之后,react diff 在同层级比较的时候, 首先会初始化 lastIndex 和 nextIndex,两者都为 0. 遍历新的虚拟dom集合,找到新的集合中的每个节点在老集合中的位置 oldIndex, 如果 oldIndex >= lastIndex,则该节点保持不动,并更新 lastIndex = Math.max(lastIndex, oldIndex),然后更新该节点的位置为 nextIndex,nextIndex ++ 如果 oldIndex < lastIndex,则移动该节点至 nextIndex 的位置,同时更新 lastIndex = Math.max(lastIndex, oldIndex),nextIndex ++ 如果老集合中没有找到节点,说明节点是新增的,则会创建新节点插入到 nextIndex 的位置 当完成新集合中所有节点 diff 时,最后还需要对老集合进行遍历,判断是否存在新集合中没有但老集合中仍存在的节点,发现存在这样的节点 D,因此删除节点 D,到此 diff 全部完成。 当然,React diff 还是存在些许不足与待优化的地方,如下图所示,若新集合的节点更新为:D、A、B、C,与老集合对比只有 D 节点移动,而 A、B、C 仍然保持原有的顺序,理论上 diff 应该只需对 D 执行移动操作,然而由于 D 在老集合的位置是最大的,导致其他节点的 _mountIndex < lastIndex,造成 D 没有执行移动操作,而是 A、B、C 全部移动到 D 节点后面的现象。 优化 react diff 算法 例如以下两个新旧数组,React的算***把 a, b, c 移动到他们的相应的位置 + 1共三步操作,而inferno.js则是直接将d移动到最前端这一步操作.其中一个核心的思想就是利用LIS(最长递增子序列)的思想做动态规划,找到最小的移动次数. * A: [a b c d] * B: [d a b c] React fiber 架构的理解 fiber 之前的 react存在的问题: 由于 js 引擎和页面渲染引擎两个线程是互斥的,当其中一个线程执行的时候,另一个线程只能挂起等待。 如果 js 线程长时间占用了主线程,那么我们渲染层面的更新就不得不长时间的等待,界面长时间不更新,会导致页面响应变慢,用户可能会感觉卡顿。 这也正是 react15 所面临的问题,当 react 在渲染组件时,从开始到渲染完成整个过程是一气呵成的,无法中断。 如果组件较大,那么 js 线程会一直执行,然后等到整棵 vDOM 树计算完成后,才会交给渲染线程,这就有可能出现卡顿的现象。 因此 facebook 提出了 react fiber架构 fiber 把渲染更新过程拆分为多个子任务,其中优先级高的先执行,并且每次只做其中的一小部分,做在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间进行页面的渲染。等浏览器忙完之后有剩余时间,再继续之前 React 未完成的任务,是一种合作式调度。 上述实现的方式是window.requestIdleCallback() 方法,该方法在浏览器空闲时段内调用排队的函数 会导致多次调用某些生命周期钩子 react把更新分为两个阶段 reconciliation 这个阶段的更新是可以被打断的,主要涉及的生命周期: componentWillMount componentWillReceiveProps shouldComponentUpdate componentWillUpdate commit 这个阶段的更新是不能被打断的 componentDidUpdate componentDidMount componentWillUnmount react 性能优化的手段 避免不必要的 render,主要是使用 shouldComponentUpdate 和 PureComponent 或者 Reac.memo 实现,pureComponent 只做浅层比较,因此可以使用 immutable 的方式进行更新 使用 useMemo 或 useCallback 缓存变量或者函数 使用 Suspense 或者 lazy 进行组件的懒加载 import React, { Suspense } from 'react';const OtherComponent = React.lazy(() => import('./OtherComponent'));function MyComponent() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <OtherComponent /> </Suspense> </div> );} setState 是同步的还是异步的 主要取决于 react 当前是否处于 批量更新的模式,如果是,则会把更新存在一个队列里面,如果不是,则会直接执行此次更新。 setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。 setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。 setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。 React 合成事件 React 合成事件(SyntheticEvent)是 React 模拟原生 DOM 事件所有能力的一个事件对象。React 的合成事件它并不是绑定在 DOM 元素本身,而是统一绑定在 document 上。这样做主要有三个目的 进行浏览器兼容,实现更好的跨平台 事件对象可能会被频繁创建和回收,使用事件池来进行统一管理,达到性能优化的目的 合成事件与原生事件的执行顺序:先执行原生事件,再处理 React 合成事件 合成事件与原生事件的区别 命名方式不同 事件处理函数写法不同 阻止默认行为方式不同