写点什么

前端二面高频 react 面试题集锦

  • 2023-02-23
    浙江
  • 本文字数:10333 字

    阅读完需:约 34 分钟

diff 虚拟 DOM 比较的规则

  • 【旧虚拟 DOM】 与 【新虚拟 DOM】中相同 key

  • 若虚拟 DOM 中的内容没有发生改变,直接使用旧的虚拟 DOM

  • 若虚拟 DOM 中的内容发生改变了,则生成新真实的 DOM,随后替换页面中之前的真实 DOM

  • 【旧虚拟 DOM】 中未找到 与 【新虚拟 DOM】相同的 key

  • 根据数据创建真实 DOM,随后渲染到页面

React-Router 4 怎样在路由变化时重新渲染同一个组件?

当路由变化时,即组件的 props 发生了变化,会调用 componentWillReceiveProps 等生命周期钩子。那需要做的只是: 当路由改变时,根据路由,也去请求数据:


class NewsList extends Component {  componentDidMount () {     this.fetchData(this.props.location);  }
fetchData(location) { const type = location.pathname.replace('/', '') || 'top' this.props.dispatch(fetchListData(type)) } componentWillReceiveProps(nextProps) { if (nextProps.location.pathname != this.props.location.pathname) { this.fetchData(nextProps.location); } } render () { ... }}
复制代码


利用生命周期 componentWillReceiveProps,进行重新 render 的预处理操作。

React 中 setState 的第二个参数作用是什么?

setState 的第二个参数是一个可选的回调函数。这个回调函数将在组件重新渲染后执行。等价于在 componentDidUpdate 生命周期内执行。通常建议使用 componentDidUpdate 来代替此方式。在这个回调函数中你可以拿到更新后 state 的值:


this.setState({    key1: newState1,    key2: newState2,    ...}, callback) // 第二个参数是 state 更新完成后的回调函数
复制代码

简述 react 事件机制

当用户在为 onClick 添加函数时,React 并没有将 Click 时间绑定在 DOM 上面


而是在 document 处监听所有支持的事件,当事件发生并冒泡至 document 处时,React 将事件内容封装交给中间层 SyntheticEvent(负责所有事件合成)


所以当事件触发的时候,对使用统一的分发函数 dispatchEvent 将指定函数执行。React 在自己的合成事件中重写了 stopPropagation 方法,将 isPropagationStopped 设置为 true,然后在遍历每一级事件的过程中根据此遍历判断是否继续执行。这就是 React 自己实现的冒泡机制

解释 React 中 render() 的目的。

每个 React 组件强制要求必须有一个 **render()**。它返回一个 React 元素,是原生 DOM 组件的表示。如果需要渲染多个 HTML 元素,则必须将它们组合在一个封闭标记内,例如 <form><group><div> 等。此函数必须保持纯净,即必须每次调用时都返回相同的结果。

Redux 中间件是什么?接受几个参数?柯里化函数两端的参数具体是什么?

Redux 的中间件提供的是位于 action 被发起之后,到达 reducer 之前的扩展点,换而言之,原本 view -→> action -> reducer -> store 的数据流加上中间件后变成了 view -> action -> middleware -> reducer -> store ,在这一环节可以做一些"副作用"的操作,如异步请求、打印日志等。


applyMiddleware 源码:


export default function applyMiddleware(...middlewares) {    return createStore => (...args) => {        // 利用传入的createStore和reducer和创建一个store        const store = createStore(...args)        let dispatch = () => {            throw new Error()        }        const middlewareAPI = {            getState: store.getState,            dispatch: (...args) => dispatch(...args)        }        // 让每个 middleware 带着 middlewareAPI 这个参数分别执行一遍        const chain = middlewares.map(middleware => middleware(middlewareAPI))        // 接着 compose 将 chain 中的所有匿名函数,组装成一个新的函数,即新的 dispatch        dispatch = compose(...chain)(store.dispatch)        return {            ...store,            dispatch        }    }}
复制代码


从 applyMiddleware 中可以看出∶


  • redux 中间件接受一个对象作为参数,对象的参数上有两个字段 dispatch 和 getState,分别代表着 Redux Store 上的两个同名函数。

  • 柯里化函数两端一个是 middewares,一个是 store.dispatch


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

React 中 refs 的作用是什么?有哪些应用场景?

Refs 提供了一种方式,用于访问在 render 方法中创建的 React 元素或 DOM 节点。Refs 应该谨慎使用,如下场景使用 Refs 比较适合:


  • 处理焦点、文本选择或者媒体的控制

  • 触发必要的动画

  • 集成第三方 DOM 库


Refs 是使用 React.createRef() 方法创建的,他通过 ref 属性附加到 React 元素上。


要在整个组件中使用 Refs,需要将 ref 在构造函数中分配给其实例属性:


class MyComponent extends React.Component {  constructor(props) {    super(props)    this.myRef = React.createRef()  }  render() {    return <div ref={this.myRef} />  }}
复制代码

React 的状态提升是什么?使用场景有哪些?

React 的状态提升就是用户对子组件操作,子组件不改变自己的状态,通过自己的 props 把这个操作改变的数据传递给父组件,改变父组件的状态,从而改变受父组件控制的所有子组件的状态,这也是 React 单项数据流的特性决定的。官方的原话是:共享 state(状态) 是通过将其移动到需要它的组件的最接近的共同祖先组件来实现的。 这被称为“状态提升(Lifting State Up)”。


概括来说就是将多个组件需要共享的状态提升到它们最近的父组件上在父组件上改变这个状态然后通过 props 分发给子组件。


一个简单的例子,父组件中有两个 input 子组件,如果想在第一个输入框输入数据,来改变第二个输入框的值,这就需要用到状态提升。


class Father extends React.Component {    constructor(props) {        super(props)        this.state = {            Value1: '',            Value2: ''        }    }    value1Change(aa) {        this.setState({            Value1: aa        })    }    value2Change(bb) {        this.setState({            Value2: bb        })    }    render() {        return (            <div style={{ padding: "100px" }}>                <Child1 value1={this.state.Value1} onvalue1Change={this.value1Change.bind(this)} />                                <Child2 value2={this.state.Value1} />            </div>        )    }}class Child1 extends React.Component {    constructor(props) {        super(props)    }    changeValue(e) {        this.props.onvalue1Change(e.target.value)    }    render() {        return (            <input value={this.props.Value1} onChange={this.changeValue.bind(this)} />        )    }}class Child2 extends React.Component {    constructor(props) {        super(props)    }    render() {        return (            <input value={this.props.value2} />        )    }}
ReactDOM.render( <Father />, document.getElementById('root'))
复制代码

react 强制刷新

component.forceUpdate() 一个不常用的生命周期方法, 它的作用就是强制刷新


官网解释如下


默认情况下,当组件的 state 或 props 发生变化时,组件将重新渲染。如果 render() 方法依赖于其他数据,则可以调用 forceUpdate() 强制让组件重新渲染。


调用 forceUpdate() 将致使组件调用 render() 方法,此操作会跳过该组件的 shouldComponentUpdate()。但其子组件会触发正常的生命周期方法,包括 shouldComponentUpdate() 方法。如果标记发生变化,React 仍将只更新 DOM。


通常你应该避免使用 forceUpdate(),尽量在 render() 中使用 this.props 和 this.state。


shouldComponentUpdate 在初始化 和 forceUpdate 不会执行

React 设计思路,它的理念是什么?

(1)编写简单直观的代码


React 最大的价值不是高性能的虚拟 DOM、封装的事件机制、服务器端渲染,而是声明式的直观的编码方式。react 文档第一条就是声明式,React 使创建交互式 UI 变得轻而易举。为应用的每一个状态设计简洁的视图,当数据改变时 React 能有效地更新并正确地渲染组件。 以声明式编写 UI,可以让代码更加可靠,且方便调试。


(2)简化可复用的组件


React 框架里面使用了简化的组件模型,但更彻底地使用了组件化的概念。React 将整个 UI 上的每一个功能模块定义成组件,然后将小的组件通过组合或者嵌套的方式构成更大的组件。React 的组件具有如下的特性∶


  • 可组合:简单组件可以组合为复杂的组件

  • 可重用:每个组件都是独立的,可以被多个组件使用

  • 可维护:和组件相关的逻辑和 UI 都封装在了组件的内部,方便维护

  • 可测试:因为组件的独立性,测试组件就变得方便很多。


(3) Virtual DOM


真实页面对应一个 DOM 树。在传统页面的开发模式中,每次需要更新页面时,都要手动操作 DOM 来进行更新。 DOM 操作非常昂贵。在前端开发中,性能消耗最大的就是 DOM 操作,而且这部分代码会让整体项目的代码变得难 以维护。React 把真实 DOM 树转换成 JavaScript 对象树,也就是 Virtual DOM,每次数据更新后,重新计算 Virtual DOM,并和上一次生成的 Virtual DOM 做对比,对发生变化的部分做批量更新。React 也提供了直观的 shouldComponentUpdate 生命周期回调,来减少数据变化后不必要的 Virtual DOM 对比过程,以保证性能。


(4)函数式编程


React 把过去不断重复构建 UI 的过程抽象成了组件,且在给定参数的情况下约定渲染对应的 UI 界面。React 能充分利用很多函数式方法去减少冗余代码。此外,由于它本身就是简单函数,所以易于测试。


(5)一次学习,随处编写


无论现在正在使用什么技术栈,都可以随时引入 React 来开发新特性,而不需要重写现有代码。


React 还可以使用 Node 进行服务器渲染,或使用 React Native 开发原生移动应用。因为 React 组件可以映射为对应的原生控件。在输出的时候,是输出 Web DOM,还是 Android 控件,还是 iOS 控件,就由平台本身决定了。所以,react 很方便和其他平台集成

React 的事件和普通的 HTML 事件有什么不同?

区别:


  • 对于事件名称命名方式,原生事件为全小写,react 事件采用小驼峰;

  • 对于事件函数处理语法,原生事件为字符串,react 事件为函数;

  • react 事件不能采用 return false 的方式来阻止浏览器的默认行为,而必须要地明确地调用preventDefault()来阻止默认行为。


合成事件是 react 模拟原生 DOM 事件所有能力的一个事件对象,其优点如下:


  • 兼容所有浏览器,更好的跨平台;

  • 将事件统一存放在一个数组,避免频繁的新增与删除(垃圾回收)。

  • 方便 react 统一管理和事务机制。


事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒泡绑定到 document 上,所以尽量避免原生事件与合成事件混用,如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到 document 上合成事件才会执行。

使用箭头函数(arrow functions)的优点是什么

  • 作用域安全:在箭头函数之前,每一个新创建的函数都有定义自身的 this 值(在构造函数中是新对象;在严格模式下,函数调用中的 this 是未定义的;如果函数被称为“对象方法”,则为基础对象等),但箭头函数不会,它会使用封闭执行上下文的 this 值。

  • 简单:箭头函数易于阅读和书写

  • 清晰:当一切都是一个箭头函数,任何常规函数都可以立即用于定义作用域。开发者总是可以查找 next-higher 函数语句,以查看 this 的值

Redux 中异步的请求怎么处理

可以在 componentDidmount 中直接进⾏请求⽆须借助 redux。但是在⼀定规模的项⽬中,上述⽅法很难进⾏异步流的管理,通常情况下我们会借助 redux 的异步中间件进⾏异步处理。redux 异步流中间件其实有很多,当下主流的异步中间件有两种 redux-thunk、redux-saga。


(1)使用 react-thunk 中间件


redux-thunk 优点:


  • 体积⼩: redux-thunk 的实现⽅式很简单,只有不到 20⾏代码

  • 使⽤简单: redux-thunk 没有引⼊像 redux-saga 或者 redux-observable 额外的范式,上⼿简单


redux-thunk 缺陷:


  • 样板代码过多: 与 redux 本身⼀样,通常⼀个请求需要⼤量的代码,⽽且很多都是重复性质的

  • 耦合严重: 异步操作与 redux 的 action 偶合在⼀起,不⽅便管理

  • 功能孱弱: 有⼀些实际开发中常⽤的功能需要⾃⼰进⾏封装


使用步骤:


  • 配置中间件,在 store 的创建中配置


import {createStore, applyMiddleware, compose} from 'redux';import reducer from './reducer';import thunk from 'redux-thunk'
// 设置调试工具const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;// 设置中间件const enhancer = composeEnhancers( applyMiddleware(thunk));
const store = createStore(reducer, enhancer);
export default store;
复制代码


  • 添加一个返回函数的 actionCreator,将异步请求逻辑放在里面


/**  发送get请求,并生成相应action,更新store的函数  @param url {string} 请求地址  @param func {function} 真正需要生成的action对应的actionCreator  @return {function} */// dispatch为自动接收的store.dispatch函数 export const getHttpAction = (url, func) => (dispatch) => {    axios.get(url).then(function(res){        const action = func(res.data)        dispatch(action)    })}
复制代码


  • 生成 action,并发送 action


componentDidMount(){    var action = getHttpAction('/getData', getInitTodoItemAction)    // 发送函数类型的action时,该action的函数体会自动执行    store.dispatch(action)}
复制代码


(2)使用 redux-saga 中间件


redux-saga 优点:


  • 异步解耦: 异步操作被被转移到单独 saga.js 中,不再是掺杂在 action.js 或 component.js 中

  • action 摆脱 thunk function: dispatch 的参数依然是⼀个纯粹的 action (FSA),⽽不是充满 “⿊魔法” thunk function

  • 异常处理: 受益于 generator function 的 saga 实现,代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理

  • 功能强⼤: redux-saga 提供了⼤量的 Saga 辅助函数和 Effect 创建器供开发者使⽤,开发者⽆须封装或者简单封装即可使⽤

  • 灵活: redux-saga 可以将多个 Saga 可以串⾏/并⾏组合起来,形成⼀个⾮常实⽤的异步 flow

  • 易测试,提供了各种 case 的测试⽅案,包括 mock task,分⽀覆盖等等


redux-saga 缺陷:


  • 额外的学习成本: redux-saga 不仅在使⽤难以理解的 generator function,⽽且有数⼗个 API,学习成本远超 redux-thunk,最重要的是你的额外学习成本是只服务于这个库的,与 redux-observable 不同,redux-observable 虽然也有额外学习成本但是背后是 rxjs 和⼀整套思想

  • 体积庞⼤: 体积略⼤,代码近 2000⾏,min 版 25KB 左右

  • 功能过剩: 实际上并发控制等功能很难⽤到,但是我们依然需要引⼊这些代码

  • ts⽀持不友好: yield⽆法返回 TS 类型


redux-saga 可以捕获 action,然后执行一个函数,那么可以把异步代码放在这个函数中,使用步骤如下:


  • 配置中间件


import {createStore, applyMiddleware, compose} from 'redux';import reducer from './reducer';import createSagaMiddleware from 'redux-saga'import TodoListSaga from './sagas'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;const sagaMiddleware = createSagaMiddleware()
const enhancer = composeEnhancers( applyMiddleware(sagaMiddleware));
const store = createStore(reducer, enhancer);sagaMiddleware.run(TodoListSaga)
export default store;
复制代码


  • 将异步请求放在 sagas.js 中


import {takeEvery, put} from 'redux-saga/effects'import {initTodoList} from './actionCreator'import {GET_INIT_ITEM} from './actionTypes'import axios from 'axios'
function* func(){ try{ // 可以获取异步返回数据 const res = yield axios.get('/getData') const action = initTodoList(res.data) // 将action发送到reducer yield put(action) }catch(e){ console.log('网络请求失败') }}
function* mySaga(){ // 自动捕获GET_INIT_ITEM类型的action,并执行func yield takeEvery(GET_INIT_ITEM, func)}
export default mySaga
复制代码


  • 发送 action


componentDidMount(){  const action = getInitTodoItemAction()  store.dispatch(action)}
复制代码

react 生命周期

初始化阶段:


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

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

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

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

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


运行中状态:


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

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

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

  • render:组件重新描绘

  • componentDidUpdate:组件已经更新


销毁阶段:


  • componentWillUnmount:组件即将销毁


shouldComponentUpdate 是做什么的,(react 性能优化是哪个周期函数?)


shouldComponentUpdate 这个方法用来判断是否需要调用 render 方法重新描绘 dom。因为 dom 的描绘非常消耗性能,如果我们能在 shouldComponentUpdate 方法中能够写出更优化的 dom diff 算法,可以极大的提高性能。


在 react17 会删除以下三个生命周期


componentWillMount,componentWillReceiveProps , componentWillUpdate

为什么 React 要用 JSX?

JSX 是一个 JavaScript 的语法扩展,或者说是一个类似于 XML 的 ECMAScript 语法扩展。它本身没有太多的语法定义,也不期望引入更多的标准。


其实 React 本身并不强制使用 JSX。在没有 JSX 的时候,React 实现一个组件依赖于使用 React.createElement 函数。代码如下:


class Hello extends React.Component {  render() {    return React.createElement(        'div',        null,         `Hello ${this.props.toWhat}`      );  }}ReactDOM.render(  React.createElement(Hello, {toWhat: 'World'}, null),  document.getElementById('root'));
复制代码


而 JSX 更像是一种语法糖,通过类似 XML 的描述方式,描写函数对象。在采用 JSX 之后,这段代码会这样写:


class Hello extends React.Component {  render() {    return <div>Hello {this.props.toWhat}</div>;  }}ReactDOM.render(  <Hello toWhat="World" />,  document.getElementById('root'));
复制代码


通过对比,可以清晰地发现,代码变得更为简洁,而且代码结构层次更为清晰。


因为 React 需要将组件转化为虚拟 DOM 树,所以在编写代码时,实际上是在手写一棵结构树。而 XML 在树结构的描述上天生具有可读性强的优势。


但这样可读性强的代码仅仅是给写程序的同学看的,实际上在运行的时候,会使用 Babel 插件将 JSX 语法的代码还原为 React.createElement 的代码。


总结: JSX 是一个 JavaScript 的语法扩展,结构类似 XML。JSX 主要用于声明 React 元素,但 React 中并不强制使用 JSX。即使使用了 JSX,也会在构建过程中,通过 Babel 插件编译为 React.createElement。所以 JSX 更像是 React.createElement 的一种语法糖。


React 团队并不想引入 JavaScript 本身以外的开发体系。而是希望通过合理的关注点分离保持组件开发的纯粹性。

React-Router 4 的 Switch 有什么用?

Switch 通常被用来包裹 Route,用于渲染与路径匹配的第一个子 <Route><Redirect>,它里面不能放其他元素。


假如不加 <Switch>


import { Route } from 'react-router-dom'
<Route path="/" component={Home}></Route><Route path="/login" component={Login}></Route>
复制代码


Route 组件的 path 属性用于匹配路径,因为需要匹配 /Home,匹配 /loginLogin,所以需要两个 Route,但是不能这么写。这样写的话,当 URL 的 path 为 “/login” 时,<Route path="/" /><Route path="/login" /> 都会被匹配,因此页面会展示 Home 和 Login 两个组件。这时就需要借助 <Switch> 来做到只显示一个匹配组件:


import { Switch, Route} from 'react-router-dom'
<Switch> <Route path="/" component={Home}></Route> <Route path="/login" component={Login}></Route></Switch>
复制代码


此时,再访问 “/login” 路径时,却只显示了 Home 组件。这是就用到了 exact 属性,它的作用就是精确匹配路径,经常与<Switch> 联合使用。只有当 URL 和该 <Route> 的 path 属性完全一致的情况下才能匹配上:


import { Switch, Route} from 'react-router-dom'
<Switch> <Route exact path="/" component={Home}></Route> <Route exact path="/login" component={Login}></Route></Switch>
复制代码

如何解决 props 层级过深的问题

  • 使用 Context API:提供一种组件之间的状态共享,而不必通过显式组件树逐层传递 props;

  • 使用 Redux 等状态库。


**

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 中其他的场景几乎都可以使用防抖和节流去提高响应性能。

什么是受控组件和非受控组件

  • 受状态控制的组件,必须要有 onChange 方法,否则不能使用 受控组件可以赋予默认值(官方推荐使用 受控组件) 实现双向数据绑定


class Input extends Component{    constructor(){        super();        this.state = {val:'100'}    }    handleChange = (e) =>{ //e是事件源        let val = e.target.value;        this.setState({val});    };    render(){        return (<div>            <input type="text" value={this.state.val} onChange={this.handleChange}/>            {this.state.val}        </div>)    }}
复制代码


  • 非受控也就意味着我可以不需要设置它的 state 属性,而通过 ref 来操作真实的 DOM


class Sum extends Component{    constructor(){        super();        this.state =  {result:''}    }    //通过ref设置的属性 可以通过this.refs获取到对应的dom元素    handleChange = () =>{        let result = this.refs.a.value + this.b.value;        this.setState({result});    };    render(){        return (            <div onChange={this.handleChange}>                <input type="number" ref="a"/>                {/*x代表的真实的dom,把元素挂载在了当前实例上*/}                <input type="number" ref={(x)=>{                    this.b = x;                }}/>                {this.state.result}            </div>        )    }}
复制代码

跨级组件的通信方式?

父组件向子组件的子组件通信,向更深层子组件通信:


  • 使用 props,利用中间组件层层传递,但是如果父组件结构较深,那么中间每一层组件都要去传递 props,增加了复杂度,并且这些 props 并不是中间组件自己需要的。

  • 使用 context,context 相当于一个大容器,可以把要通信的内容放在这个容器中,这样不管嵌套多深,都可以随意取用,对于跨越多层的全局数据可以使用 context 实现。


// context方式实现跨级组件通信 // Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据const BatteryContext = createContext();//  子组件的子组件 class GrandChild extends Component {    render(){        return (            <BatteryContext.Consumer>                {                    color => <h1 style={{"color":color}}>我是红色的:{color}</h1>                }            </BatteryContext.Consumer>        )    }}//  子组件const Child = () =>{    return (        <GrandChild/>    )}// 父组件class Parent extends Component {      state = {          color:"red"      }      render(){          const {color} = this.state          return (          <BatteryContext.Provider value={color}>              <Child></Child>          </BatteryContext.Provider>          )      }}
复制代码


用户头像

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

还未添加个人简介

评论

发布
暂无评论
前端二面高频react面试题集锦_前端_夏天的味道123_InfoQ写作社区