写点什么

社招前端二面必会 react 面试题及答案

作者:beifeng1996
  • 2023-05-19
    浙江
  • 本文字数:8070 字

    阅读完需:约 26 分钟

高阶组件的应用场景

权限控制


利用高阶组件的 条件渲染 特性可以对页面进行权限控制,权限控制一般分为两个维度:页面级别 和 页面元素级别


// HOC.js    function withAdminAuth(WrappedComponent) {        return class extends React.Component {            state = {                isAdmin: false,            }            async componentWillMount() {                const currentRole = await getCurrentUserRole();                this.setState({                    isAdmin: currentRole === 'Admin',                });            }            render() {                if (this.state.isAdmin) {                    return <WrappedComponent {...this.props} />;                } else {                    return (<div>您没有权限查看该页面,请联系管理员!</div>);                }            }        };    }
// 使用// pages/page-a.js class PageA extends React.Component { constructor(props) { super(props); // something here... } componentWillMount() { // fetching data } render() { // render page with data } } export default withAdminAuth(PageA);
复制代码


可能你已经发现了,高阶组件其实就是装饰器模式在 React 中的实现:通过给函数传入一个组件(函数或类)后在函数内部对该组件(函数或类)进行功能的增强(不修改传入参数的前提下),最后返回这个组件(函数或类),即允许向一个现有的组件添加新的功能,同时又不去修改该组件,属于 包装模式(Wrapper Pattern) 的一种。


什么是装饰者模式:在不改变对象自身的前提下在程序运行期间动态的给对象添加一些额外的属性或行为


可以提高代码的复用性和灵活性


再对高阶组件进行一个小小的总结:


  • 高阶组件 不是组件 一个把某个组件转换成另一个组件的 函数

  • 高阶组件的主要作用是 代码复用

  • 高阶组件是 装饰器模式在 React 中的实现


封装组件的原则


封装原则


1、单一原则:负责单一的页面渲染


2、多重职责:负责多重职责,获取数据,复用逻辑,页面渲染等


3、明确接受参数:必选,非必选,参数尽量设置以_开头,避免变量重复


4、可扩展:需求变动能够及时调整,不影响之前代码


5、代码逻辑清晰


6、封装的组件必须具有高性能,低耦合的特性


7、组件具有单一职责:封装业务组件或者基础组件,如果不能给这个组件起一个有意义的名字,证明这个组件承担的职责可能不够单一,需要继续抽组件,直到它可以是一个独立的组件即可

了解 redux 吗?

  • redux 是一个应用数据流框架,主要解决了组件之间状态共享问题,原理是集中式管理,主要有三个核心方法:action store reduce

  • 工作流程 view 调用 store 的 dispatch 接受 action 传入的 store,reduce 进行 state 操作

  • view 通过 store 提供的 getState 获取最新的数据

  • redux 的优点:

  • 新增的 state 对状态的管理更加明确

  • 流程更加规范,减少手动编写代码,提高编码效率

  • redux 的缺点:

  • 当数据更新是有时候组件不需要,也要重新绘制,影响效率

react hooks,它带来了那些便利

  • 代码逻辑聚合,逻辑复用

  • HOC 嵌套地狱

  • 代替 class


React 中通常使用 类定义 或者 函数定义 创建组件:


在类定义中,我们可以使用到许多 React 特性,例如 state、 各种组件生命周期钩子等,但是在函数定义中,我们却无能为力,因此 React 16.8 版本推出了一个新功能 (React Hooks),通过它,可以更好的在函数定义组件中使用 React 特性。


好处:


  1. 跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;

  2. 类定义更为复杂


  • 不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;

  • 时刻需要关注 this 的指向问题;

  • 代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;


  1. 状态与 UI 隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。


注意:


  • 避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定;

  • 只有 函数定义组件 和 hooks 可以调用 hooks,避免在 类组件 或者 普通函数 中调用;

  • 不能在 useEffect 中使用 useState,React 会报错提示;

  • 类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存;


重要钩子


  1. 状态钩子 (useState): 用于定义组件的 State,其到类定义中 this.state 的功能;


// useState 只接受一个参数: 初始状态// 返回的是组件名和更改该组件对应的函数const [flag, setFlag] = useState(true);// 修改状态setFlag(false)
// 上面的代码映射到类定义中:this.state = { flag: true }const flag = this.state.flagconst setFlag = (bool) => { this.setState({ flag: bool, })}
复制代码


  1. 生命周期钩子 (useEffect):


类定义中有许多生命周期函数,而在 React Hooks 中也提供了一个相应的函数 (useEffect),这里可以看做 componentDidMount、componentDidUpdate 和 componentWillUnmount 的结合。


useEffect(callback, [source])接受两个参数


  • callback: 钩子回调函数;

  • source: 设置触发条件,仅当 source 发生改变时才会触发;

  • useEffect 钩子在没有传入[source]参数时,默认在每次 render 时都会优先调用上次保存的回调中返回的函数,后再重新调用回调;


useEffect(() => {    // 组件挂载后执行事件绑定    console.log('on')    addEventListener()
// 组件 update 时会执行事件解绑 return () => { console.log('off') removeEventListener() }}, [source]);

// 每次 source 发生改变时,执行结果(以类定义的生命周期,便于大家理解):// --- DidMount ---// 'on'// --- DidUpdate ---// 'off'// 'on'// --- DidUpdate ---// 'off'// 'on'// --- WillUnmount --- // 'off'
复制代码


通过第二个参数,我们便可模拟出几个常用的生命周期:


  • componentDidMount: 传入[]时,就只会在初始化时调用一次


const useMount = (fn) => useEffect(fn, [])
复制代码


  • componentWillUnmount: 传入[],回调中的返回的函数也只会被最终执行一次


const useUnmount = (fn) => useEffect(() => fn, [])
复制代码


  • mounted: 可以使用 useState 封装成一个高度可复用的 mounted 状态;


const useMounted = () => {    const [mounted, setMounted] = useState(false);    useEffect(() => {        !mounted && setMounted(true);        return () => setMounted(false);    }, []);    return mounted;}
复制代码


  • componentDidUpdate: useEffect 每次均会执行,其实就是排除了 DidMount 后即可;


const mounted = useMounted() useEffect(() => {    mounted && fn()})
复制代码


  1. 其它内置钩子:


  • useContext: 获取 context 对象

  • useReducer: 类似于 Redux 思想的实现,但其并不足以替代 Redux,可以理解成一个组件内部的 redux:

  • 并不是持久化存储,会随着组件被销毁而销毁;

  • 属于组件内部,各个组件是相互隔离的,单纯用它并无法共享数据;

  • 配合 useContext`的全局性,可以完成一个轻量级的 Redux;(easy-peasy)

  • useCallback: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;

  • useMemo: 用于缓存传入的 props,避免依赖的组件每次都重新渲染;

  • useRef: 获取组件的真实节点;

  • useLayoutEffect

  • DOM 更新同步钩子。用法与 useEffect 类似,只是区别于执行时间点的不同

  • useEffect 属于异步执行,并不会等待 DOM 真正渲染后执行,而 useLayoutEffect 则会真正渲染后才触发;

  • 可以获取更新后的 state;


  1. 自定义钩子(useXxxxx): 基于 Hooks 可以引用其它 Hooks 这个特性,我们可以编写自定义钩子,如上面的 useMounted。又例如,我们需要每个页面自定义标题:


function useTitle(title) {  useEffect(    () => {      document.title = title;    });}
// 使用:function Home() { const title = '我是首页' useTitle(title)
return ( <div>{title}</div> )}
复制代码

ref 是一个函数又有什么好处?

  • 方便 react 销毁组件、重新渲染的时候去清空 refs 的东西,防止内存泄露

简述 flux 思想

Flux 的最大特点,就是数据的"单向流动"。


  • 用户访问 View

  • View发出用户的 Action

  • Dispatcher 收到Action,要求 Store 进行相应的更新

  • Store 更新后,发出一个"change"事件

  • View 收到"change"事件后,更新页面

React 的虚拟 DOM 和 Diff 算法的内部实现

传统 diff 算法的时间复杂度是 O(n^3),这在前端 render 中是不可接受的。为了降低时间复杂度,react 的 diff 算法做了一些妥协,放弃了最优解,最终将时间复杂度降低到了 O(n)。


那么 react diff 算法做了哪些妥协呢?,参考如下:


  1. tree diff:只对比同一层的 dom 节点,忽略 dom 节点的跨层级移动


如下图,react 只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点不存在时,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。


这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。



这就意味着,如果 dom 节点发生了跨层级移动,react 会删除旧的节点,生成新的节点,而不会复用。


  1. component diff:如果不是同一类型的组件,会删除旧的组件,创建新的组件



  1. element diff:对于同一层级的一组子节点,需要通过唯一 id 进行来区分


  • 如果没有 id 来进行区分,一旦有插入动作,会导致插入位置之后的列表全部重新渲染

  • 这也是为什么渲染列表时为什么要使用唯一的 key。


参考 前端进阶面试题详细解答

key 的作用

是给每一个 vnode 的唯一 id,可以依靠 key,更准确,更快的拿到 oldVnode 中对应的 vnode 节点


<!-- 更新前 --><div>  <p key="ka">ka</p>  <h3 key="song">song</he></div>
<!-- 更新后 --><div> <h3 key="song">song</h3> <p key="ka">ka</p></div>
复制代码


如果没有 key,React 会认为 div 的第一个子节点由 p 变成 h3,第二个子节点由 h3 变成 p,则会销毁这两个节点并重新构造。


但是当我们用 key 指明了节点前后对应关系后,React 知道 key === "ka" 的 p 更新后还在,所以可以复用该节点,只需要交换顺序。


key 是 React 用来追踪哪些列表元素被修改、被添加或者被移除的辅助标志。


在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。在 React diff 算法中,React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重新渲染。同时,React 还需要借助 key 来判断元素与本地状态的关联关系。

React 性能优化

  • shouldCompoentUpdate

  • pureComponent 自带 shouldCompoentUpdate 的浅比较优化

  • 结合 Immutable.js 达到最优

react diff 算法

我们知道 React 会维护两个虚拟 DOM,那么是如何来比较,如何来判断,做出最优的解呢?这就用到了 diff 算法



diff 算法的作用


计算出 Virtual DOM 中真正变化的部分,并只针对该部分进行原生 DOM 操作,而非重新渲染整个页面。


传统 diff 算法


通过循环递归对节点进行依次对比,算法复杂度达到 O(n^3) ,n 是树的节点数,这个有多可怕呢?——如果要展示 1000 个节点,得执行上亿次比较。。即便是 CPU 快能执行 30 亿条命令,也很难在一秒内计算出差异。


React 的 diff 算法


  1. 什么是调和?


将 Virtual DOM 树转换成 actual DOM 树的最少操作的过程 称为 调和 。


  1. 什么是 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 对不同的组件间的比较,有三种策略


  1. 同一类型的两个组件,按原策略(层级比较)继续比较 Virtual DOM 树即可。

  2. 同一类型的两个组件,组件 A 变化为组件 B 时,可能 Virtual DOM 没有任何变化,如果知道这点(变换的过程中,Virtual DOM 没有改变),可节省大量计算时间,所以 用户 可以通过 shouldComponentUpdate() 来判断是否需要 判断计算。

  3. 不同类型的组件,将一个(将被改变的)组件判断为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 中,何为 state

State 和 props 类似,但它是私有的,并且完全由组件自身控制。State 本质上是一个持有数据,并决定组件如何渲染的对象。

ssr 原理是什么?

核心原理其实就是借助虚拟 DOM 来实现 react 代码能够在服务器运行的,node 里面可以执行 react 代码

传入 setState 函数的第二个参数的作用是什么?

该函数会在 setState 函数调用完成并且组件开始重渲染的时候被调用,我们可以用该函数来监听渲染是否完成:


this.setState(  { username: 'tylermcginnis33' },  () => console.log('setState has finished and the component has re-rendered.'))
复制代码


this.setState((prevState, props) => {  return {    streak: prevState.streak + props.count  }})
复制代码

类组件(Class component)和函数式组件(Functional component)之间有何不同

  • 类组件不仅允许你使用更多额外的功能,如组件自身的状态和生命周期钩子,也能使组件直接访问 store 并维持状态

  • 当组件仅是接收 props,并将组件自身渲染到页面时,该组件就是一个 '无状态组件(stateless component)',可以使用一个纯函数来创建这样的组件。这种组件也被称为哑组件(dumb components)或展示组件

useEffect 和 useLayoutEffect 的区别

useEffect


基本上 90%的情况下,都应该用这个,这个是在 render 结束后,你的 callback 函数执行,但是不会 block browser painting,算是某种异步的方式吧,但是 class 的 componentDidMount 和 componentDidUpdate 是同步的,在 render 结束后就运行,useEffect 在大部分场景下都比 class 的方式性能更好.


useLayoutEffect


这个是用在处理 DOM 的时候,当你的 useEffect 里面的操作需要处理 DOM,并且会改变页面的样式,就需要用这个,否则可能会出现出现闪屏问题, useLayoutEffect 里面的 callback 函数会在**DOM 更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,**阻塞了浏览器的绘制.

何为 JSX

JSX 是 JavaScript 语法的一种语法扩展,并拥有 JavaScript 的全部功能。JSX 生产 React "元素",你可以将任何的 JavaScript 表达式封装在花括号里,然后将其嵌入到 JSX 中。在编译完成之后,JSX 表达式就变成了常规的 JavaScript 对象,这意味着你可以在 if 语句和 for 循环内部使用 JSX,将它赋值给变量,接受它作为参数,并从函数中返回它。

在哪个生命周期中你会发出 Ajax 请求?为什么?

Ajax 请求应该写在组件创建期的第五个阶段,即 componentDidMount 生命周期方法中。原因如下。在创建期的其他阶段,组件尚未渲染完成。而在存在期的 5 个阶段,又不能确保生命周期方法一定会执行(如通过 shouldComponentUpdate 方法优化更新等)。在销毀期,组件即将被销毁,请求数据变得无意义。因此在这些阶段发岀 Ajax 请求显然不是最好的选择。在组件尚未挂载之前,Ajax 请求将无法执行完毕,如果此时发出请求,将意味着在组件挂载之前更新状态(如执行 setState),这通常是不起作用的。在 componentDidMount 方法中,执行 Ajax 即可保证组件已经挂载,并且能够正常更新组件。

为什么 React 并不推荐优先考虑使用 Context?

  • Context 目前还处于实验阶段,可能会在后面的发行版本中有很大的变化,事实上这种情况已经发生了,所以为了避免给今后升级带来大的影响和麻烦,不建议在 app 中使用 context。

  • 尽管不建议在 app 中使用 context,但是独有组件而言,由于影响范围小于 app,如果可以做到高内聚,不破坏组件树之间的依赖关系,可以考虑使用 context

  • 对于组件之间的数据通信或者状态管理,有效使用 props 或者 state 解决,然后再考虑使用第三方的成熟库进行解决,以上的方法都不是最佳的方案的时候,在考虑 context。

  • context 的更新需要通过 setState()触发,但是这并不是很可靠的,Context 支持跨组件的访问,但是如果中间的子组件通过一些方法不影响更新,比如 shouldComponentUpdate() 返回 false 那么不能保证 Context 的更新一定可以使用 Context 的子组件,因此,Context 的可靠性需要关注

react 旧版生命周期函数

初始化阶段


  • getDefaultProps:获取实例的默认属性

  • getInitialState:获取每个实例的初始化状态

  • componentWillMount:组件即将被装载、渲染到页面上

  • render:组件在这里生成虚拟的DOM节点

  • componentDidMount:组件真正在被装载之后


运行中状态


  • componentWillReceiveProps:组件将要接收到属性的时候调用

  • shouldComponentUpdate:组件接受到新属性或者新状态的时候(可以返回 false,接收数据后不更新,阻止render调用,后面的函数不会被继续执行了)

  • componentWillUpdate:组件即将更新不能修改属性和状态

  • render:组件重新描绘

  • componentDidUpdate:组件已经更新


销毁阶段


  • componentWillUnmount:组件即将销毁

在 React 中如何处理事件

为了解决跨浏览器的兼容性问题,SyntheticEvent 实例将被传递给你的事件处理函数,SyntheticEvent是 React 跨浏览器的浏览器原生事件包装器,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation()preventDefault()。比较有趣的是,React 实际上并不将事件附加到子节点本身。React 使用单个事件侦听器侦听顶层的所有事件。这对性能有好处,也意味着 React 在更新 DOM 时不需要跟踪事件监听器。

展示组件(Presentational component)和容器组件(Container component)之间有何不同

展示组件关心组件看起来是什么。展示专门通过 props 接受数据和回调,并且几乎不会有自身的状态,但当展示组件拥有自身的状态时,通常也只关心 UI 状态而不是数据的状态。


容器组件则更关心组件是如何运作的。容器组件会为展示组件或者其它容器组件提供数据和行为(behavior),它们会调用 Flux actions,并将其作为回调提供给展示组件。容器组件经常是有状态的,因为它们是(其它组件的)数据源。


用户头像

beifeng1996

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
社招前端二面必会react面试题及答案_前端_beifeng1996_InfoQ写作社区