前端常考 react 面试题(持续更新中)
react diff 算法
我们知道 React 会维护两个虚拟 DOM,那么是如何来比较,如何来判断,做出最优的解呢?这就用到了 diff 算法
diff 算法的作用
计算出 Virtual DOM 中真正变化的部分,并只针对该部分进行原生 DOM 操作,而非重新渲染整个页面。
传统 diff 算法
通过循环递归对节点进行依次对比,算法复杂度达到
O(n^3)
,n 是树的节点数,这个有多可怕呢?——如果要展示 1000 个节点,得执行上亿次比较。。即便是 CPU 快能执行 30 亿条命令,也很难在一秒内计算出差异。
React 的 diff 算法
什么是调和?
将 Virtual DOM 树转换成 actual DOM 树的最少操作的过程 称为 调和 。
什么是 React diff 算法?
diff
算法是调和的具体实现。
diff 策略
React 用 三大策略 将
O(n^3)
杂度 转化为O(n)
复杂度
策略一(tree diff):
Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计
同级比较,既然 DOM 节点跨层级的移动操作少到可以忽略不计,那么 React 通过 updateDepth 对 Virtual DOM 树进行层级控制,也就是同一层,在对比的过程中,如果发现节点不在了,会完全删除不会对其他地方进行比较,这样只需要对树遍历一次就 OK 了
策略二(component diff):
拥有相同类的两个组件 生成相似的树形结构,
拥有不同类的两个组件 生成不同的树形结构。
策略三(element diff):
对于同一层级的一组子节点,通过唯一 id 区分。
tree diff
React 通过 updateDepth 对 Virtual DOM 树进行层级控制。
对树分层比较,两棵树 只对同一层次节点 进行比较。如果该节点不存在时,则该节点及其子节点会被完全删除,不会再进一步比较。
只需遍历一次,就能完成整棵 DOM 树的比较。
那么问题来了,如果 DOM 节点出现了跨层级操作,diff 会咋办呢?
答:diff 只简单考虑同层级的节点位置变换,如果是跨层级的话,只有创建节点和删除节点的操作。
如上图所示,以 A 为根节点的整棵树会被重新创建,而不是移动,因此 官方建议不要进行 DOM 节点跨层级操作,可以通过 CSS 隐藏、显示节点,而不是真正地移除、添加 DOM 节点
component diff
React 对不同的组件间的比较,有三种策略
同一类型的两个组件,按原策略(层级比较)继续比较 Virtual DOM 树即可。
同一类型的两个组件,组件 A 变化为组件 B 时,可能 Virtual DOM 没有任何变化,如果知道这点(变换的过程中,Virtual DOM 没有改变),可节省大量计算时间,所以 用户 可以通过
shouldComponentUpdate()
来判断是否需要 判断计算。不同类型的组件,将一个(将被改变的)组件判断为
dirty component
(脏组件),从而替换 整个组件的所有节点。
注意:如果组件 D 和组件 G 的结构相似,但是 React 判断是 不同类型的组件,则不会比较其结构,而是删除 组件 D 及其子节点,创建组件 G 及其子节点。
element diff
当节点处于同一层级时,diff 提供三种节点操作:删除、插入、移动。
插入:组件 C 不在集合(A,B)中,需要插入
删除:
组件 D 在集合(A,B,D)中,但 D 的节点已经更改,不能复用和更新,所以需要删除 旧的 D ,再创建新的。
组件 D 之前在 集合(A,B,D)中,但集合变成新的集合(A,B)了,D 就需要被删除。
移动:组件 D 已经在集合(A,B,C,D)里了,且集合更新时,D 没有发生更新,只是位置改变,如新集合(A,D,B,C),D 在第二个,无须像传统 diff,让旧集合的第二个 B 和新集合的第二个 D 比较,并且删除第二个位置的 B,再在第二个位置插入 D,而是 (对同一层级的同组子节点) 添加唯一 key 进行区分,移动即可。
diff 的不足与待优化的地方
尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,会影响 React 的渲染性能
react-router4 的核心
路由变成了组件
分散到各个页面,不需要配置 比如
<link> <route></route>
什么是 React Fiber?
Fiber 是 React 16 中新的协调引擎或重新实现核心算法。它的主要目标是支持虚拟 DOM 的增量渲染。React Fiber 的目标是提高其在动画、布局、手势、暂停、中止或重用等方面的适用性,并为不同类型的更新分配优先级,以及新的并发原语。React Fiber 的目标是增强其在动画、布局和手势等领域的适用性。它的主要特性是增量渲染:能够将渲染工作分割成块,并将其分散到多个帧中。
如何 React.createElement ?
上述代码如何使用 React.createElement
来实现:
如何创建 refs
Refs 是使用 React.createRef()
创建的,并通过 ref
属性附加到 React 元素。在构造组件时,通常将 Refs
分配给实例属性,以便可以在整个组件中引用它们。
或者这样用:
这三个点(...)在 React 干嘛用的?
...
在 React(使用 JSX)代码中做什么?它叫什么?
这个叫扩展操作符号或者展开操作符,例如,如果this.props
包含a:1
和b:2
,则
等价于下面内容:
扩展符号不仅适用于该用例,而且对于创建具有现有对象的大多数(或全部)属性的新对象非常方便,在更新state
咱们就经常这么做:
参考 前端进阶面试题详细解答
如何配置 React-Router 实现路由切换
(1)使用<Route>
组件
路由匹配是通过比较 <Route>
的 path 属性和当前地址的 pathname 来实现的。当一个 <Route>
匹配成功时,它将渲染其内容,当它不匹配时就会渲染 null。没有路径的 <Route>
将始终被匹配。
(2)结合使用 <Switch>
组件和 <Route>
组件
<Switch>
用于将 <Route>
分组。
<Switch>
不是分组 <Route>
所必须的,但他通常很有用。 一个 <Switch>
会遍历其所有的子 <Route>
元素,并仅渲染与当前地址匹配的第一个元素。
(3)使用 <Link>、 <NavLink>、<Redirect>
组件
<Link>
组件来在你的应用程序中创建链接。无论你在何处渲染一个<Link>
,都会在应用程序的 HTML 中渲染锚(<a>
)。
是一种特殊类型的 当它的 to 属性与当前地址匹配时,可以将其定义为"活跃的"。
当我们想强制导航时,可以渲染一个<Redirect>
,当一个<Redirect>
渲染时,它将使用它的 to 属性进行定向。
useEffect 与 useLayoutEffect 的区别
(1)共同点
运用效果: useEffect 与 useLayoutEffect 两者都是用于处理副作用,这些副作用包括改变 DOM、设置订阅、操作定时器等。在函数组件内部操作副作用是不被允许的,所以需要使用这两个函数去处理。
使用方式: useEffect 与 useLayoutEffect 两者底层的函数签名是完全一致的,都是调用的 mountEffectImpl 方法,在使用上也没什么差异,基本可以直接替换。
(2)不同点
使用场景: useEffect 在 React 的渲染过程中是被异步调用的,用于绝大多数场景;而 useLayoutEffect 会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。也正因为是同步处理,所以需要避免在 useLayoutEffect 做计算量较大的耗时任务从而造成阻塞。
使用效果: useEffect 是按照顺序执行代码的,改变屏幕像素之后执行(先渲染,后改变 DOM),当改变屏幕内容时可能会产生闪烁;useLayoutEffect 是改变屏幕像素之前就执行了(会推迟页面显示的事件,先改变 DOM 后渲染),不会产生闪烁。useLayoutEffect 总是比 useEffect 先执行。
在未来的趋势上,两个 API 是会长期共存的,暂时没有删减合并的计划,需要开发者根据场景去自行选择。React 团队的建议非常实用,如果实在分不清,先用 useEffect,一般问题不大;如果页面有异常,再直接替换为 useLayoutEffect 即可。
react 最新版本解决了什么问题,增加了哪些东西
React 16.x 的三大新特性 Time Slicing、Suspense、 hooks
Time Slicing(解决 CPU 速度问题)使得在执行任务的期间可以随时暂停,跑去干别的事情,这个特性使得 react 能在性能极其差的机器跑时,仍然保持有良好的性能
Suspense (解决网络 IO 问题) 和 lazy 配合,实现异步加载组件。 能暂停当前组件的渲染, 当完成某件事以后再继续渲染,解决从 react 出生到现在都存在的「异步副作用」的问题,而且解决得非的优雅,使用的是 T 异步但是同步的写法,这是最好的解决异步问题的方式
提供了一个内置函数 componentDidCatch,当有错误发生时,可以友好地展示 fallback 组件; 可以捕捉到它的子元素(包括嵌套子元素)抛出的异常; 可以复用错误组件。
(1)React16.8 加入 hooks,让 React 函数式组件更加灵活,hooks 之前,React 存在很多问题:
在组件间复用状态逻辑很难
复杂组件变得难以理解,高阶组件和函数组件的嵌套过深。
class 组件的 this 指向问题
难以记忆的生命周期
hooks 很好的解决了上述问题,hooks 提供了很多方法
useState 返回有状态值,以及更新这个状态值的函数
useEffect 接受包含命令式,可能有副作用代码的函数。
useContext 接受上下文对象(从 React.createContext 返回的值)并返回当前上下文值,
useReducer useState 的替代方案。接受类型为 (state,action)=> newState 的 reducer,并返回与 dispatch 方法配对的当前状态。
useCalLback 返回一个回忆的 memoized 版本,该版本仅在其中一个输入发生更改时才会更改。纯函数的输入输出确定性 o useMemo 纯的一个记忆函数 o useRef 返回一个可变的 ref 对象,其 Current 属性被初始化为传递的参数,返回的 ref 对象在组件的整个生命周期内保持不变。
useImperativeMethods 自定义使用 ref 时公开给父组件的实例值
useMutationEffect 更新兄弟组件之前,它在 React 执行其 DOM 改变的同一阶段同步触发
useLayoutEffect DOM 改变后同步触发。使用它来从 DOM 读取布局并同步重新渲染
(2)React16.9
重命名 Unsafe 的生命周期方法。新的 UNSAFE_前缀将有助于在代码 review 和 debug 期间,使这些有问题的字样更突出
废弃 javascrip:形式的 URL。以 javascript:开头的 URL 非常容易遭受攻击,造成安全漏洞。
废弃"Factory"组件。 工厂组件会导致 React 变大且变慢。
act()也支持异步函数,并且你可以在调用它时使用 await。
使用 <React.ProfiLer> 进行性能评估。在较大的应用中追踪性能回归可能会很方便
(3)React16.13.0
支持在渲染期间调用 setState,但仅适用于同一组件
可检测冲突的样式规则并记录警告
废弃 unstable_createPortal,使用 CreatePortal
将组件堆栈添加到其开发警告中,使开发人员能够隔离 bug 并调试其程序,这可以清楚地说明问题所在,并更快地定位和修复错误。
React.forwardRef 有什么用
forwardRef
使用
forwardRef
(forward
在这里是「传递」的意思)后,就能跨组件传递ref
。在例子中,我们将
inputRef
从Form
跨组件传递到MyInput
中,并与input
产生关联
useImperativeHandle
除了「限制跨组件传递
ref
」外,还有一种「防止ref
失控的措施」,那就是useImperativeHandle
,他的逻辑是这样的:既然「ref失控」
是由于「使用了不该被使用的 DOM 方法」(比如appendChild
),那我可以限制「ref
中只存在可以被使用的方法」。用useImperativeHandle
修改我们的 MyInput 组件:
现在,Form
组件中通过inputRef.current
只能取到如下数据结构:
就杜绝了
「开发者通过ref取到DOM后,执行不该被使用的API,出现ref失控」
的情况
为了防止错用/滥用导致
ref
失控,React 限制「默认情况下,不能跨组件传递ref」
为了破除这种限制,可以使用
forwardRef
。为了减少
ref
对DOM
的滥用,可以使用useImperativeHandle
限制ref
传递的数据结构。
React 高阶组件、Render props、hooks 有什么区别,为什么要不断迭代
这三者是目前 react 解决代码复用的主要方式:
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数。
render props 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术,更具体的说,render prop 是一个用于告知组件需要渲染什么内容的函数 prop。
通常,render props 和高阶组件只渲染一个子节点。让 Hook 来服务这个使用场景更加简单。这两种模式仍有用武之地,(例如,一个虚拟滚动条组件或许会有一个 renderltem 属性,或是一个可见的容器组件或许会有它自己的 DOM 结构)。但在大部分场景下,Hook 足够了,并且能够帮助减少嵌套。
(1)HOC 官方解释∶
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
简言之,HOC 是一种组件的设计模式,HOC 接受一个组件和额外的参数(如果需要),返回一个新的组件。HOC 是纯函数,没有副作用。
HOC 的优缺点∶
优点∶ 逻辑服用、不影响被包裹组件的内部逻辑。
缺点∶ hoc 传递给被包裹组件的 props 容易和被包裹后的组件重名,进而被覆盖
(2)Render props 官方解释∶
"render prop"是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术
具有 render prop 的组件接受一个返回 React 元素的函数,将 render 的渲染逻辑注入到组件内部。在这里,"render"的命名可以是任何其他有效的标识符。
由此可以看到,render props 的优缺点也很明显∶
优点:数据共享、代码复用,将组件内的 state 作为 props 传递给调用者,将渲染逻辑交给调用者。
缺点:无法在 return 语句外访问数据、嵌套写法不够优雅
(3)Hooks 官方解释∶
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。通过自定义 hook,可以复用代码逻辑。
以上可以看出,hook 解决了 hoc 的 prop 覆盖的问题,同时使用的方式解决了 render props 的嵌套地狱的问题。hook 的优点如下∶
使用直观;
解决 hoc 的 prop 重名问题;
解决 render props 因共享数据 而出现嵌套地狱的问题;
能在 return 之外使用数据的问题。
需要注意的是:hook 只能在组件顶层使用,不可在分支语句中使用。、
为什么 React 元素有一个 $$typeof 属性
目的是为了防止 XSS 攻击。因为 Synbol 无法被序列化,所以 React 可以通过有没有 $$typeof 属性来断出当前的 element 对象是从数据库来的还是自己生成的。
如果没有 $$typeof 这个属性,react 会拒绝处理该元素。
在 React 的古老版本中,下面的写法会出现 XSS 攻击:
react router
React Router 提供一个 routerWillLeave 生命周期钩子,这使得 React 组件可以拦截正在发生的跳转,或在离开 route 前提示用户。routerWillLeave 返回值有以下两种:
return
false
取消此次跳转
return
返回提示信息,在离开 route 前提示用户进行确认。
高阶组件
高阶函数:如果一个函数接受一个或多个函数作为参数或者返回一个函数就可称之为高阶函数。
高阶组件:如果一个函数 接受一个或多个组件作为参数并且返回一个组件 就可称之为 高阶组件。
react 中的高阶组件
React 中的高阶组件主要有两种形式:属性代理和反向继承。
属性代理 Proxy
操作
props
抽离
state
通过
ref
访问到组件实例用其他元素包裹传入的组件
WrappedComponent
反向继承
会发现其属性代理和反向继承的实现有些类似的地方,都是返回一个继承了某个父类的子类,只不过属性代理中继承的是 React.Component
,反向继承中继承的是传入的组件 WrappedComponent
。
反向继承可以用来做什么:
1.操作 state
高阶组件中可以读取、编辑和删除WrappedComponent
组件实例中的state
。甚至可以增加更多的state
项,但是非常不建议这么做因为这可能会导致state
难以维护及管理。
2.渲染劫持(Render Highjacking)
条件渲染通过 props.isLoading 这个条件来判断渲染哪个组件。
修改由 render() 输出的 React 元素树
为什么使用 jsx 的组件中没有看到使用 react 却需要引入 react?
本质上来说 JSX 是React.createElement(component, props, ...children)
方法的语法糖。在 React 17 之前,如果使用了 JSX,其实就是在使用 React, babel
会把组件转换为 CreateElement
形式。在 React 17 之后,就不再需要引入,因为 babel
已经可以帮我们自动引入 react。
useEffect(fn, []) 和 componentDidMount 有什么差异
useEffect
会捕获props
和 state。所以即便在回调函数里,你拿到的还是初始的 props 和 state。如果想得到“最新”的值,可以使用 ref。
react 中这两个生命周期会触发死循环
componentWillUpdate
生命周期在shouldComponentUpdate
返回 true 后被触发。在这两个生命周期只要视图更新就会触发,因此不能再这两个生命周期中使用 setState。否则会导致死循环
在 React 中如何避免不必要的 render?
React 基于虚拟 DOM 和高效 Diff 算法的完美配合,实现了对 DOM 最小粒度的更新。大多数情况下,React 对 DOM 的渲染效率足以业务日常。但在个别复杂业务场景下,性能问题依然会困扰我们。此时需要采取一些措施来提升运行性能,其很重要的一个方向,就是避免不必要的渲染(Render)。这里提下优化的点:
shouldComponentUpdate 和 PureComponent
在 React 类组件中,可以利用 shouldComponentUpdate 或者 PureComponent 来减少因父组件更新而触发子组件的 render,从而达到目的。shouldComponentUpdate 来决定是否组件是否重新渲染,如果不希望组件重新渲染,返回 false 即可。
利用高阶组件
在函数组件中,并没有 shouldComponentUpdate 这个生命周期,可以利用高阶组件,封装一个类似 PureComponet 的功能
使用 React.memo
React.memo 是 React 16.6 新的一个 API,用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似,但不同的是, React.memo 只能用于函数组件。
对 React-Intl 的理解,它的工作原理?
React-intl 是雅虎的语言国际化开源项目 FormatJS 的一部分,通过其提供的组件和 API 可以与 ReactJS 绑定。
React-intl 提供了两种使用方法,一种是引用 React 组件,另一种是直接调取 API,官方更加推荐在 React 项目中使用前者,只有在无法使用 React 组件的地方,才应该调用框架提供的 API。它提供了一系列的 React 组件,包括数字格式化、字符串格式化、日期格式化等。
在 React-intl 中,可以配置不同的语言包,他的工作原理就是根据需要,在语言包之间进行切换。
新版生命周期
在新版本中,React 官方对生命周期有了新的 变动建议:
使用
getDerivedStateFromProps
替换componentWillMount;
使用
getSnapshotBeforeUpdate
替换componentWillUpdate;
避免使用
componentWillReceiveProps
;
其实该变动的原因,正是由于上述提到的
Fiber
。首先,从上面我们知道 React 可以分成reconciliation
与commit
两个阶段,对应的生命周期如下:
reconciliation
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
commit
componentDidMount
componentDidUpdate
componentWillUnmount
在
Fiber
中,reconciliation
阶段进行了任务分割,涉及到 暂停 和 重启,因此可能会导致reconciliation
中的生命周期函数在一次更新渲染循环中被 多次调用 的情况,产生一些意外错误
新版的建议生命周期如下:
使用建议:
在
constructor
初始化state
;在
componentDidMount
中进行事件监听,并在componentWillUnmount
中解绑事件;在
componentDidMount
中进行数据的请求,而不是在componentWillMount
;需要根据
props
更新state
时,使用getDerivedStateFromProps(nextProps, prevState)
;旧 props 需要自己存储,以便比较;
可以在 componentDidUpdate 监听 props 或者 state 的变化,例如:
在 componentDidUpdate 使用 setState 时,必须加条件,否则将进入死循环;
getSnapshotBeforeUpdate(prevProps, prevState)可以在更新之前获取最新的渲染数据,它的调用是在 render 之后, update 之前;
shouldComponentUpdate: 默认每次调用 setState,一定会最终走到 diff 阶段,但可以通过 shouldComponentUpdate 的生命钩子返回 false 来直接阻止后面的逻辑执行,通常是用于做条件渲染,优化渲染的性能。
评论