写点什么

react 高频面试题总结(一)

  • 2022 年 8 月 02 日
  • 本文字数:10132 字

    阅读完需:约 33 分钟

React Hook 的使用限制有哪些?

React Hooks 的限制主要有两条:


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

  • 在 React 的函数组件中调用 Hook。


那为什么会有这样的限制呢?Hooks 的设计初衷是为了改进 React 组件的开发模式。在旧有的开发模式下遇到了三个问题。


  • 组件之间难以复用状态逻辑。过去常见的解决方案是高阶组件、render props 及状态管理框架。

  • 复杂的组件变得难以理解。生命周期函数与业务逻辑耦合太深,导致关联部分难以拆分。

  • 人和机器都很容易混淆类。常见的有 this 的问题,但在 React 团队中还有类难以优化的问题,希望在编译优化层面做出一些改进。


这三个问题在一定程度上阻碍了 React 的后续发展,所以为了解决这三个问题,Hooks 基于函数组件开始设计。然而第三个问题决定了 Hooks 只支持函数组件。


那为什么不要在循环、条件或嵌套函数中调用 Hook 呢?因为 Hooks 的设计是基于数组实现。在调用时按顺序加入数组中,如果使用循环、条件或嵌套函数很有可能导致数组取值错位,执行错误的 Hook。当然,实质上 React 的源码里不是数组,是链表。


这些限制会在编码上造成一定程度的心智负担,新手可能会写错,为了避免这样的情况,可以引入 ESLint 的 Hooks 检查插件进行预防。

react16 版本的 reconciliation 阶段和 commit 阶段是什么

  • reconciliation 阶段包含的主要工作是对 current tree 和 new tree 做 diff 计算,找出变化部分。进行遍历、对比等是可以中断,歇一会儿接着再来。

  • commit 阶段是对上一阶段获取到的变化部分应用到真实的 DOM 树中,是一系列的 DOM 操作。不仅要维护更复杂的 DOM 状态,而且中断后再继续,会对用户体验造成影响。在普遍的应用场景下,此阶段的耗时比 diff 计算等耗时相对短。

state 是怎么注入到组件的,从 reducer 到组件经历了什么样的过程

通过 connect 和 mapStateToProps 将 state 注入到组件中:


import { connect } from 'react-redux'import { setVisibilityFilter } from '@/reducers/Todo/actions'import Link from '@/containers/Todo/components/Link'
const mapStateToProps = (state, ownProps) => ({ active: ownProps.filter === state.visibilityFilter})
const mapDispatchToProps = (dispatch, ownProps) => ({ setFilter: () => { dispatch(setVisibilityFilter(ownProps.filter)) }})
export default connect( mapStateToProps, mapDispatchToProps)(Link)复制代码
复制代码


上面代码中,active 就是注入到 Link 组件中的状态。 mapStateToProps(state,ownProps)中带有两个参数,含义是∶


  • state-store 管理的全局状态对象,所有都组件状态数据都存储在该对象中。

  • ownProps 组件通过 props 传入的参数。


reducer 到组件经历的过程:


  • reducer 对 action 对象处理,更新组件状态,并将新的状态值返回 store。

  • 通过 connect(mapStateToProps,mapDispatchToProps)(Component)对组件 Component 进行升级,此时将状态值从 store 取出并作为 props 参数传递到组件。


高阶组件实现源码∶


import React from 'react'import PropTypes from 'prop-types'
// 高阶组件 contect export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => { class Connect extends React.Component { // 通过对context调用获取store static contextTypes = { store: PropTypes.object }
constructor() { super() this.state = { allProps: {} } }
// 第一遍需初始化所有组件初始状态 componentWillMount() { const store = this.context.store this._updateProps() store.subscribe(() => this._updateProps()); // 加入_updateProps()至store里的监听事件列表 }
// 执行action后更新props,使组件可以更新至最新状态(类似于setState) _updateProps() { const store = this.context.store; let stateProps = mapStateToProps ? mapStateToProps(store.getState(), this.props) : {} // 防止 mapStateToProps 没有传入 let dispatchProps = mapDispatchToProps ? mapDispatchToProps(store.dispatch, this.props) : { dispatch: store.dispatch } // 防止 mapDispatchToProps 没有传入 this.setState({ allProps: { ...stateProps, ...dispatchProps, ...this.props } }) }
render() { return <WrappedComponent {...this.state.allProps} /> } } return Connect}复制代码
复制代码

Redux 请求中间件如何处理并发

使用 redux-Saga redux-saga 是一个管理 redux 应用异步操作的中间件,用于代替 redux-thunk 的。它通过创建 Sagas 将所有异步操作逻辑存放在一个地方进行集中处理,以此将 react 中的同步操作与异步操作区分开来,以便于后期的管理与维护。 redux-saga 如何处理并发:


  • takeEvery


可以让多个 saga 任务并行被 fork 执行。


import {    fork,    take} from "redux-saga/effects"
const takeEvery = (pattern, saga, ...args) => fork(function*() { while (true) { const action = yield take(pattern) yield fork(saga, ...args.concat(action)) }})复制代码
复制代码


  • takeLatest


takeLatest 不允许多个 saga 任务并行地执行。一旦接收到新的发起的 action,它就会取消前面所有 fork 过的任务(如果这些任务还在执行的话)。在处理 AJAX 请求的时候,如果只希望获取最后那个请求的响应, takeLatest 就会非常有用。


import {    cancel,    fork,    take} from "redux-saga/effects"
const takeLatest = (pattern, saga, ...args) => fork(function*() { let lastTask while (true) { const action = yield take(pattern) if (lastTask) { yield cancel(lastTask) // 如果任务已经结束,则 cancel 为空操作 } lastTask = yield fork(saga, ...args.concat(action)) }})复制代码
复制代码


**

React 与 Vue 的 diff 算法有何不同?

diff 算法是指生成更新补丁的方式,主要应用于虚拟 DOM 树变化后,更新真实 DOM。所以 diff 算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁。


React 的 diff 算法,触发更新的时机主要在 state 变化与 hooks 调用之后。此时触发虚拟 DOM 树变更遍历,采用了深度优先遍历算法。但传统的遍历方式,效率较低。为了优化效率,使用了分治的方式。将单一节点比对转化为了 3 种类型节点的比对,分别是树、组件及元素,以此提升效率。


  • 树比对:由于网页视图中较少有跨层级节点移动,两株虚拟 DOM 树只对同一层次的节点进行比较。

  • 组件比对:如果组件是同一类型,则进行树比对,如果不是,则直接放入到补丁中。

  • 元素比对:主要发生在同层级中,通过标记节点操作生成补丁,节点操作对应真实的 DOM 剪裁操作。


以上是经典的 React diff 算法内容。自 React 16 起,引入了 Fiber 架构。为了使整个更新过程可随时暂停恢复,节点与树分别采用了 FiberNode 与 FiberTree 进行重构。fiberNode 使用了双链表的结构,可以直接找到兄弟节点与子节点。整个更新过程由 current 与 workInProgress 两株树双缓冲完成。workInProgress 更新完成后,再通过修改 current 相关指针指向新节点。


Vue 的整体 diff 策略与 React 对齐,虽然缺乏时间切片能力,但这并不意味着 Vue 的性能更差,因为在 Vue 3 初期引入过,后期因为收益不高移除掉了。除了高帧率动画,在 Vue 中其他的场景几乎都可以使用防抖和节流去提高响应性能。

React 事件机制

<div onClick={this.handleClick.bind(this)}>点我</div>复制代码
复制代码


React 并不是将 click 事件绑定到了 div 的真实 DOM 上,而是在 document 处监听了所有的事件,当事件发生并且冒泡到 document 处的时候,React 将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗,还能在组件挂在销毁时统一订阅和移除事件。


除此之外,冒泡到 document 上的事件也不是原生的浏览器事件,而是由 react 自己实现的合成事件(SyntheticEvent)。因此如果不想要是事件冒泡的话应该调用 event.preventDefault()方法,而不是调用 event.stopProppagation()方法。 JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了 document 上。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。


另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault


实现合成事件的目的如下:


  • 合成事件首先抹平了浏览器之间的兼容问题,另外这是一个跨浏览器原生事件包装器,赋予了跨浏览器开发的能力;

  • 对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题。但是对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象。

请说岀 React 从 EMAScript5 编程规范到 EMAScript6 编程规范过程中的几点改变。

主要改变如下。(1)创建组件的方法不同。EMAScript5 版本中,定义组件用 React.createClass。EMAScript6 版本中,定义组件要定义组件类,并继承 Component 类。(2)定义默认属性的方法不同。EMAScript5 版本中,用 getDefaultProps 定义默认属性。EMAScript6 版本中,为组件定义 defaultProps 静态属性,来定义默认属性。(3)定义初始化状态的方法不同。EMAScript5 版本中,用 getInitialState 定义初始化状态。EMAScript6 版本中,在构造函数中,通过 this. state 定义初始化状态。注意:构造函数的第一个参数是属性数据,一定要用 super 继承。(4)定义属性约束的方法不同。EMAScript5 版本中,用 propTypes 定义属性的约束。EMAScript6 版本中,为组件定义 propsTypes 静态属性,来对属性进行约束。(5)使用混合对象、混合类的方法不同。EMAScript5 版本中,通过 mixins 继承混合对象的方法。EMAScript6 版本中,定义混合类,让混合类继承 Component 类,然后让组件类继承混合类,实现对混合类方法的继承。(6)绑定事件的方法不同。EMAScript5 版本中,绑定的事件回调函数作用域是组件实例化对象。EMAScript6 版本中,绑定的事件回调函数作用域是 null。(7)父组件传递方法的作用域不同。EMAScript5 版本中,作用域是父组件。 EMAScript6 版本中,变成了 null。(8)组件方法作用域的修改方法不同。EMAScript5 版本中,无法改变作用域。EMAScript6 版本中,作用域是可以改变的。

为何 React 事件要自己绑定 this

在 React 源码中,当具体到某一事件处理函数将要调用时,将调用 invokeGuardedCallback 方法。


function invokeGuardedCallback(name, func, a) {  try {    func(a);  } catch (x) {    if (caughtError === null) {      caughtError = x;    }  }}
复制代码


事件处理函数是直接调用的,并没有指定调用的组件,所以不进行手动绑定的情况下直接获取到的 this 是不准确的,所以我们需要手动将当前组件绑定到 this 上

shouldComponentUpdate 有什么用?为什么它很重要?

组件状态数据或者属性数据发生更新的时候,组件会进入存在期,视图会渲染更新。在生命周期方法 should ComponentUpdate 中,允许选择退出某些组件(和它们的子组件)的和解过程。和解的最终目标是根据新的状态,以最有效的方式更新用户界面。如果我们知道用户界面的某一部分不会改变,那么没有理由让 React 弄清楚它是否应该更新渲染。通过在 shouldComponentUpdate 方法中返回 false, React 将让当前组件及其所有子组件保持与当前组件状态相同。

React 最新的⽣命周期是怎样的?

React 16 之后有三个⽣命周期被废弃(但并未删除)


  • componentWillMount

  • componentWillReceiveProps

  • componentWillUpdate


官⽅计划在 17 版本完全删除这三个函数,只保留 UNSAVE_前缀的三个函数,⽬的是为了向下兼容,但是对于开发者⽽⾔应该尽量避免使⽤他们,⽽是使⽤新增的⽣命周期函数替代它们。


⽬前 React16.8+的⽣命周期分为三个阶段,分别是挂载阶段、更新阶段、卸载阶段。


挂载阶段:


  • constructor:构造函数,最先被执⾏,我们通常在构造函数⾥初始化 state 对象或者给⾃定义⽅法绑定 this;

  • getDerivedStateFromProps:static getDerivedStateFromProps(nextProps, prevState),这是个静态⽅法,当我们接收到新的属性想去修改我们 state, 可以使⽤getDerivedStateFromProps

  • render:render 函数是纯函数,只返回需要渲染的东⻄,不应该包含其它的业务逻辑,可以返回原⽣的 DOM、React 组件、Fragment、Portals、字符串和数字、 Boolean 和 null 等内容;

  • componentDidMount:组件装载之后调⽤,此时我们可以获取到 DOM 节点并操作,⽐如对 canvas,svg 的操作,服务器请求,订阅都可以写在这个⾥⾯,但是记得在 componentWillUnmount 中取消订阅;


更新阶段:


  • getDerivedStateFromProps: 此⽅法在更新个挂载阶段都可能会调⽤;

  • shouldComponentUpdate:shouldComponentUpdate(nextProps, nextState),有两个参数 nextProps 和 nextState,表示新的属性和变化之后的 state,返回⼀个布尔值,true 表示会触发重新渲染,false 表示不会触发重新渲染,默认返回 true,我们通常利⽤此⽣命周期来优化 React 程序性能;

  • render:更新阶段也会触发此⽣命周期;

  • getSnapshotBeforeUpdate:getSnapshotBeforeUpdate(prevProps, prevState),这个⽅法在 render 之后,componentDidUpdate 之前调⽤,有两个参数 prevProps 和 prevState,表示之前的属性和之前的 state,这个函数有⼀个返回值,会作为第三个参数传给 componentDidUpdate,如果你不想要返回值,可以返回 null,此⽣命周期必须与 componentDidUpdate 搭配使⽤;

  • componentDidUpdate:componentDidUpdate(prevProps, prevState, snapshot),该⽅法在 getSnapshotBeforeUpdate⽅法之后被调⽤,有三个参数 prevProps,prevState,snapshot,表示之前的 props,之前的 state,和 snapshot。第三个参数是 getSnapshotBeforeUpdate 返回的,如果触发某些回调函数时需要⽤到 DOM 元素的状态,则将对⽐或计算的过程迁移⾄getSnapshotBeforeUpdate,然后在 componentDidUpdate 中统⼀触发回调或更新状态。


卸载阶段:


-componentWillUnmount:当我们的组件被卸载或者销毁了就会调⽤,我们可以在这个函数⾥去清除⼀些定时器,取消⽹络请求,清理⽆效的 DOM 元素等垃圾清理⼯作。


总结:


  • componentWillMount:在渲染之前执行,用于根组件中的 App 级配置;

  • componentDidMount:在第一次渲染之后执行,可以在这里做 AJAX 请求,DOM 的操作或状态更新以及设置事件监听器;

  • componentWillReceiveProps:在初始化 render 的时候不会执行,它会在组件接受到新的状态(Props)时被触发,一般用于父组件状态更新时子组件的重新渲染

  • shouldComponentUpdate:确定是否更新组件。默认情况下,它返回 true。如果确定在 state 或 props 更新后组件不需要在重新渲染,则可以返回 false,这是一个提高性能的方法;

  • componentWillUpdate:在 shouldComponentUpdate 返回 true 确定要更新组件之前件之前执行;

  • componentDidUpdate:它主要用于更新 DOM 以响应 props 或 state 更改;

  • componentWillUnmount:它用于取消任何的网络请求,或删除与组件关联的所有事件监听器。

React Hooks 在平时开发中需要注意的问题和原因

(1)不要在循环,条件或嵌套函数中调用 Hook,必须始终在 React 函数的顶层使用 Hook


这是因为 React 需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数。一旦在循环或条件分支语句中调用 Hook,就容易导致调用顺序的不一致性,从而产生难以预料到的后果。


(2)使用 useState 时候,使用 push,pop,splice 等直接更改数组对象的坑


使用 push 直接更改数组无法获取到新值,应该采用析构方式,但是在 class 里面不会有这个问题。代码示例:


function Indicatorfilter() {  let [num,setNums] = useState([0,1,2,3])  const test = () => {    // 这里坑是直接采用push去更新num    // setNums(num)是无法更新num的    // 必须使用num = [...num ,1]    num.push(1)    // num = [...num ,1]    setNums(num)  }return (    <div className='filter'>      <div onClick={test}>测试</div>        <div>          {num.map((item,index) => (              <div key={index}>{item}</div>          ))}      </div>    </div>  )}
class Indicatorfilter extends React.Component<any,any>{ constructor(props:any){ super(props) this.state = { nums:[1,2,3] } this.test = this.test.bind(this) }
test(){ // class采用同样的方式是没有问题的 this.state.nums.push(1) this.setState({ nums: this.state.nums }) }
render(){ let {nums} = this.state return( <div> <div onClick={this.test}>测试</div> <div> {nums.map((item:any,index:number) => ( <div key={index}>{item}</div> ))} </div> </div>
) }}复制代码
复制代码


(3)useState 设置状态的时候,只有第一次生效,后期需要更新状态,必须通过 useEffect


TableDeail 是一个公共组件,在调用它的父组件里面,我们通过 set 改变 columns 的值,以为传递给 TableDeail 的 columns 是最新的值,所以 tabColumn 每次也是最新的值,但是实际 tabColumn 是最开始的值,不会随着 columns 的更新而更新:


const TableDeail = ({    columns,}:TableData) => {    const [tabColumn, setTabColumn] = useState(columns) }
// 正确的做法是通过useEffect改变这个值const TableDeail = ({ columns,}:TableData) => { const [tabColumn, setTabColumn] = useState(columns) useEffect(() =>{setTabColumn(columns)},[columns])}
复制代码
复制代码


(4)善用 useCallback


父组件传递给子组件事件句柄时,如果我们没有任何参数变动可能会选用 useMemo。但是每一次父组件渲染子组件即使没变化也会跟着渲染一次。


(5)不要滥用 useContext


可以使用基于 useContext 封装的状态管理工具。

什么是 React 的 refs?为什么它们很重要

refs 允许你直接访问 DOM 元素或组件实例。为了使用它们,可以向组件添加个 ref 属性。如果该属性的值是一个回调函数,它将接受底层的 DOM 元素或组件的已挂载实例作为其第一个参数。可以在组件中存储它。


export class App extends Component {  showResult() {    console.log(this.input.value);  }  render() {    return (      <div>        <input type="text" ref={(input) => (this.input = input)} />        <button onClick={this.showResult.bind(this)}>展示结果</button>      </div>    );  }}
复制代码


如果该属性值是一个字符串, React 将会在组件实例化对象的 refs 属性中,存储一个同名属性,该属性是对这个 DOM 元素的引用。可以通过原生的 DOM API 操作它。


export class App extends Component {  showResult() {    console.log(this.refs.username.value);  }  render() {    return (      <div>        <input type="text" ref="username" />        <button onClick={this.showResu1t.bind(this)}>展示结果</button>      </div>    );  }}
复制代码

使用 React 有何优点

  • 只需查看 render 函数就会很容易知道一个组件是如何被渲染的

  • JSX 的引入,使得组件的代码更加可读,也更容易看懂组件的布局,或者组件之间是如何互相引用的

  • 支持服务端渲染,这可以改进 SEO 和性能

  • 易于测试

  • React 只关注 View 层,所以可以和其它任何框架(如 Backbone.js, Angular.js)一起使用

对 React-Fiber 的理解,它解决了什么问题?

React V15 在渲染时,会递归比对 VirtualDOM 树,找出需要变动的节点,然后同步更新它们, 一气呵成。这个过程期间, React 会占据浏览器资源,这会导致用户触发的事件得不到响应,并且会导致掉帧,导致用户感觉到卡顿


为了给用户制造一种应用很快的“假象”,不能让一个任务长期霸占着资源。 可以将浏览器的渲染、布局、绘制、资源加载(例如 HTML 解析)、事件响应、脚本执行视作操作系统的“进程”,需要通过某些调度策略合理地分配 CPU 资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。


所以 React 通过 Fiber 架构,让这个执行过程变成可被中断。“适时”地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:


  • 分批延时对 DOM 进行操作,避免一次性操作大量 DOM 节点,可以得到更好的用户体验;

  • 给浏览器一点喘息的机会,它会对代码进行编译优化(JIT)及进行热代码优化,或者对 reflow 进行修正。


核心思想: Fiber 也称协程或者纤程。它和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程),它只是一种控制流程的让出机制。让出 CPU 的执行权,让 CPU 能在这段时间执行其他的操作。渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。

constructor

答案是:在 constructor 函数里面,需要用到props的值的时候,就需要调用 super(props)
复制代码


  1. class 语法糖默认会帮你定义一个 constructor,所以当你不需要使用 constructor 的时候,是可以不用自己定义的

  2. 当你自己定义一个 constructor 的时候,就一定要写 super(),否则拿不到 this

  3. 当你在 constructor 里面想要使用 props 的值,就需要传入 props 这个参数给 super,调用 super(props),否则只需要写 super()

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 Router 时,如何获取当前页面的路由或浏览器中地址栏中的地址?

在当前组件的 props 中,包含 location 属性对象,包含当前页面路由地址信息,在 match 中存储当前路由的参数等数据信息。可以直接通过 this .props 使用它们。

React 中的 setState 和 replaceState 的区别是什么?

(1)setState() setState()用于设置状态对象,其语法如下:


setState(object nextState[, function callback])复制代码
复制代码


  • nextState,将要设置的新状态,该状态会和当前的 state 合并

  • callback,可选参数,回调函数。该函数会在 setState 设置成功,且组件重新渲染后调用。


合并 nextState 和当前 state,并重新渲染组件。setState 是 React 事件处理函数中和请求回调函数中触发 UI 更新的主要方法。


(2)replaceState() replaceState()方法与 setState()类似,但是方法只会保留 nextState 中状态,原 state 不在 nextState 中的状态都会被删除。其语法如下:


replaceState(object nextState[, function callback])复制代码
复制代码


  • nextState,将要设置的新状态,该状态会替换当前的 state。

  • callback,可选参数,回调函数。该函数会在 replaceState 设置成功,且组件重新渲染后调用。


总结: setState 是修改其中的部分状态,相当于 Object.assign,只是覆盖,不会减少原来的状态。而 replaceState 是完全替换原来的状态,相当于赋值,将原来的 state 替换为另一个对象,如果新状态属性减少,那么 state 中就没有这个状态了。

用户头像

还未添加个人签名 2022.07.31 加入

还未添加个人简介

评论

发布
暂无评论
react高频面试题总结(一)_React_helloworld1024fd_InfoQ写作社区