写点什么

腾讯前端经典 react 面试题汇总

作者:beifeng1996
  • 2022-10-28
    浙江
  • 本文字数:7250 字

    阅读完需:约 24 分钟

概述一下 React 中的事件处理逻辑。

为了解决跨浏览器兼容性问题, React 会将浏览器原生事件( Browser Native Event)封装为合成事件( Synthetic Event)并传入设置的事件处理程序中。这里的合成事件提供了与原生事件相同的接口,不过它们屏蔽了底层浏览器的细节差异,保证了行为的一致性。另外, React 并没有直接将事件附着到子元素上,而是以单一事件监听器的方式将所有的事件发送到顶层进行处理(基于事件委托原理)。这样 React 在更新 DOM 时就不需要考虑如何处理附着在 DOM 上的事件监听器,最终达到优化性能的目的。

如果用索引值作为 key 会出现什么样的问题

  • 若对数据进行逆序添加,逆序删除等破坏顺序的操作

  • 则会产生没有必要的真实 DOM 更新,界面想过看不出区别,但是效力低,性能不好

  • 如果结构中还包含输入类的 DOM

  • 会产生错误的 DOM 更新===》界面会有问题


如果不存在对数据的逆序添加 逆序删除等破坏顺序操作,仅用于渲染展示,用 index 作为 key 也没有问题

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> )}
复制代码

React Portal 有哪些使用场景

  • 在以前, react 中所有的组件都会位于 #app 下,而使用 Portals 提供了一种脱离 #app 的组件

  • 因此 Portals 适合脱离文档流(out of flow) 的组件,特别是 position: absolute 与 position: fixed 的组件。比如模态框,通知,警告,goTop 等。


以下是官方一个模态框的示例,可以在以下地址中测试效果


<html>  <body>    <div id="app"></div>    <div id="modal"></div>    <div id="gotop"></div>    <div id="alert"></div>  </body></html>
复制代码


const modalRoot = document.getElementById('modal');
class Modal extends React.Component { constructor(props) { super(props); this.el = document.createElement('div'); }
componentDidMount() { modalRoot.appendChild(this.el); }
componentWillUnmount() { modalRoot.removeChild(this.el); }
render() { return ReactDOM.createPortal( this.props.children, this.el, ); }}
复制代码


React Hooks 当中的 useEffect 是如何区分生命周期钩子的


useEffect 可以看成是componentDidMountcomponentDidUpdatecomponentWillUnmount三者的结合。useEffect(callback, [source])接收两个参数,调用方式如下


useEffect(() => {   console.log('mounted');
return () => { console.log('willUnmount'); } }, [source]);
复制代码


生命周期函数的调用主要是通过第二个参数[source]来进行控制,有如下几种情况:


  • [source]参数不传时,则每次都会优先调用上次保存的函数中返回的那个函数,然后再调用外部那个函数;

  • [source]参数传[]时,则外部的函数只会在初始化时调用一次,返回的那个函数也只会最终在组件卸载时调用一次;

  • [source]参数有值时,则只会监听到数组中的值发生变化后才优先调用返回的那个函数,再调用外部的函数。

createElement 和 cloneElement 有什么区别?

createElement 是 JSX 被转载得到的,在 React 中用来创建 React 元素(即虚拟 DOM)的内容。cloneElement 用于复制元素并传递新的 props。

redux 有什么缺点

  • 一个组件所需要的数据,必须由父组件传过来,而不能像flux中直接从store取。

  • 当一个组件相关数据更新时,即使父组件不需要用到这个组件,父组件还是会重新render,可能会有效率影响,或者需要写复杂的shouldComponentUpdate进行判断。

描述事件在 React 中的处理方式。

为了解决跨浏览器兼容性问题, React 中的事件处理程序将传递 SyntheticEvent 的实例,它是跨浏览器事件的包装器。这些 SyntheticEvent 与你习惯的原生事件具有相同的接口,它们在所有浏览器中都兼容。React 实际上并没有将事件附加到子节点本身。而是通过事件委托模式,使用单个事件监听器监听顶层的所有事件。这对于性能是有好处的。这也意味着在更新 DOM 时, React 不需要担心跟踪事件监听器。


参考:前端react面试题详细解答

redux 中间件

中间件提供第三方插件的模式,自定义拦截 action -> reducer 的过程。变为 action -> middlewares -> reducer 。这种机制可以让我们改变数据流,实现如异步 action ,action 过 滤,日志输出,异常报告等功能


常见的中间件:


  • redux-logger:提供日志输出;

  • redux-thunk:处理异步操作;

  • redux-promise: 处理异步操作;

  • actionCreator 的返回值是 promise

在 React 中元素( element)和组件( component)有什么区别?

简单地说,在 React 中元素(虛拟 DOM)描述了你在屏幕上看到的 DOM 元素。换个说法就是,在 React 中元素是页面中 DOM 元素的对象表示方式。在 React 中组件是一个函数或一个类,它可以接受输入并返回一个元素。注意:工作中,为了提高开发效率,通常使用 JSX 语法表示 React 元素(虚拟 DOM)。在编译的时候,把它转化成一个 React. createElement 调用方法。

如何使用 4.0 版本的 React Router?

React Router 4.0 版本中对 hashHistory 做了迁移,执行包安装命令 npm install react-router-dom 后,按照如下代码进行使用即可。


import { HashRouter, Route, Redirect, Switch } from " react-router-dom";class App extends Component {  render() {    return (      <div>        <Switch>          <Route path="/list" componen t={List}></Route>          <Route path="/detail/:id" component={Detail}>            {" "}          </Route>          <Redirect from="/ " to="/list">            {" "}          </Redirect>        </Switch>      </div>    );  }}const routes = (  <HashRouter>    <App> </App>  </HashRouter>);render(routes, ickt);
复制代码

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 来判断元素与本地状态的关联关系。

componentWillReceiveProps 调用时机

  • 已经被废弃掉

  • 当 props 改变的时候才调用,子组件第二次接收到 props 的时候

如何用 React 构建( build)生产模式?

通常,使用 Webpack 的 DefinePlugin 方法将 NODE ENV 设置为 production。这将剥离 propType 验证和额外的警告。除此之外,还可以减少代码,因为 React 使用 Uglify 的 dead-code 来消除开发代码和注释,这将大大减少包占用的空间。

react 旧版生命周期函数

初始化阶段


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

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

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

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

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


运行中状态


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

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

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

  • render:组件重新描绘

  • componentDidUpdate:组件已经更新


销毁阶段


  • componentWillUnmount:组件即将销毁

在 React 中遍历的方法有哪些?

(1)遍历数组:map && forEach


import React from 'react';
class App extends React.Component { render() { let arr = ['a', 'b', 'c', 'd']; return ( <ul> { arr.map((item, index) => { return <li key={index}>{item}</li> }) } </ul> ) }}
class App extends React.Component { render() { let arr = ['a', 'b', 'c', 'd']; return ( <ul> { arr.forEach((item, index) => { return <li key={index}>{item}</li> }) } </ul> ) }}

复制代码


(2)遍历对象:map && for in


class App extends React.Component {  render() {    let obj = {      a: 1,      b: 2,      c: 3    }    return (      <ul>        {          (() => {            let domArr = [];            for(const key in obj) {              if(obj.hasOwnProperty(key)) {                const value = obj[key]                domArr.push(<li key={key}>{value}</li>)              }            }            return domArr;          })()        }      </ul>    )  }}
// Object.entries() 把对象转换成数组class App extends React.Component { render() { let obj = { a: 1, b: 2, c: 3 } return ( <ul> { Object.entries(obj).map(([key, value], index) => { // item是一个数组,把item解构,写法是[key, value] return <li key={key}>{value}</li> }) } </ul> ) }}

复制代码

React-Router 的实现原理是什么?

客户端路由实现的思想:


  • 基于 hash 的路由:通过监听hashchange事件,感知 hash 的变化

  • 改变 hash 可以直接通过 location.hash=xxx

  • 基于 H5 history 路由:

  • 改变 url 可以通过 history.pushState 和 resplaceState 等,会将 URL 压入堆栈,同时能够应用 history.go() 等 API

  • 监听 url 的变化可以通过自定义事件触发实现


react-router 实现的思想:


  • 基于 history 库来实现上述不同的客户端路由实现思想,并且能够保存历史记录等,磨平浏览器差异,上层无感知

  • 通过维护的列表,在每次 URL 发生变化的回收,通过配置的 路由路径,匹配到对应的 Component,并且 render

React 16.X 中 props 改变后在哪个生命周期中处理

在 getDerivedStateFromProps 中进行处理。


这个生命周期函数是为了替代componentWillReceiveProps存在的,所以在需要使用componentWillReceiveProps时,就可以考虑使用getDerivedStateFromProps来进行替代。


两者的参数是不相同的,而getDerivedStateFromProps是一个静态函数,也就是这个函数不能通过 this 访问到 class 的属性,也并不推荐直接访问属性。而是应该通过参数提供的 nextProps 以及 prevState 来进行判断,根据新传入的 props 来映射到 state。


需要注意的是,如果 props 传入的内容不需要影响到你的 state,那么就需要返回一个 null,这个返回值是必须的,所以尽量将其写到函数的末尾:


static getDerivedStateFromProps(nextProps, prevState) {    const {type} = nextProps;    // 当传入的type发生变化的时候,更新state    if (type !== prevState.type) {        return {            type,        };    }    // 否则,对于state不进行任何操作    return null;}
复制代码

为什么虚拟 dom 会提高性能

虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要 的 dom 操作,从而提高性能


具体实现步骤如下:


  1. 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树, 插到文档当中;

  2. 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记 录两棵树差异;

  3. 把 2 所记录的差异应用到步骤 1 所构建的真正的 DOM 树上,视图就更新了。

setState 方法的第二个参数有什么用?使用它的目的是什么?

它是一个回调函数,当 setState 方法执行结束并重新渲染该组件时调用它。在工作中,更好的方式是使用 React 组件生命周期之——“存在期”的生命周期方法,而不是依赖这个回调函数。


export class App extends Component {  constructor(props) {    super(props);    this.state = {      username: "雨夜清荷",    };  }  render() {    return <div> {this.state.username}</div>;  }  componentDidMount() {    this.setstate(      {        username: "有课前端网",      },      () => console.log("re-rendered success. ")    );  }}
复制代码

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

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


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


用户头像

beifeng1996

关注

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

还未添加个人简介

评论

发布
暂无评论
腾讯前端经典react面试题汇总_React_beifeng1996_InfoQ写作社区