写点什么

前端一面常见 react 面试题(持续更新中)

  • 2023-02-27
    浙江
  • 本文字数:6826 字

    阅读完需:约 22 分钟

React 组件中怎么做事件代理?它的原理是什么?

React 基于 Virtual DOM 实现了一个 SyntheticEvent 层(合成事件层),定义的事件处理器会接收到一个合成事件对象的实例,它符合 W3C 标准,且与原生的浏览器事件拥有同样的接口,支持冒泡机制,所有的事件都自动绑定在最外层上。


在 React 底层,主要对合成事件做了两件事:


  • 事件委派: React 会把所有的事件绑定到结构的最外层,使用统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部事件监听和处理函数。

  • 自动绑定: React 组件中,每个方法的上下文都会指向该组件的实例,即自动绑定 this 为当前组件。

如何有条件地向 React 组件添加属性?

对于某些属性,React 非常聪明,如果传递给它的值是虚值,可以省略该属性。例如:


var InputComponent = React.createClass({  render: function () {    var required = true;    var disabled = false;    return <input type="text" disabled={disabled} required={required} />;  },});
复制代码


渲染结果:


<input type="text" required>
复制代码


另一种可能的方法是:


var condition = true;var component = <div value="foo" {...(condition && { disabled: true })} />;
复制代码

react 的渲染过程中,兄弟节点之间是怎么处理的?也就是 key 值不一样的时候

通常我们输出节点的时候都是 map 一个数组然后返回一个ReactNode,为了方便react内部进行优化,我们必须给每一个reactNode添加key,这个key prop在设计值处不是给开发者用的,而是给 react 用的,大概的作用就是给每一个reactNode添加一个身份标识,方便 react 进行识别,在重渲染过程中,如果 key 一样,若组件属性有所变化,则react只更新组件对应的属性;没有变化则不更新,如果 key 不一样,则 react 先销毁该组件,然后重新创建该组件

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

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

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

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

react 旧版生命周期函数

初始化阶段


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

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

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

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

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


运行中状态


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

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

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

  • render:组件重新描绘

  • componentDidUpdate:组件已经更新


销毁阶段


  • componentWillUnmount:组件即将销毁


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

diff 算法是怎么运作

每一种节点类型有自己的属性,也就是 prop,每次进行 diff 的时候,react 会先比较该节点类型,假如节点类型不一样,那么 react 会直接删除该节点,然后直接创建新的节点插入到其中,假如节点类型一样,那么会比较 prop 是否有更新,假如有 prop 不一样,那么 react 会判定该节点有更新,那么重渲染该节点,然后在对其子节点进行比较,一层一层往下,直到没有子节点

在 Redux 中使用 Action 要注意哪些问题?

在 Redux 中使用 Action 的时候, Action 文件里尽量保持 Action 文件的纯净,传入什么数据就返回什么数据,最妤把请求的数据和 Action 方法分离开,以保持 Action 的纯净。

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

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


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

在生命周期中的哪一步你应该发起 AJAX 请求

我们应当将 AJAX 请求放到 componentDidMount 函数中执行,主要原因有下


  • React 下一代调和算法 Fiber 会通过开始或停止渲染的方式优化应用性能,其会影响到 componentWillMount 的触发次数。对于 componentWillMount 这个生命周期函数的调用次数会变得不确定,React 可能会多次频繁调用 componentWillMount。如果我们将 AJAX 请求放到 componentWillMount 函数中,那么显而易见其会被触发多次,自然也就不是好的选择。

  • 如果我们将AJAX 请求放置在生命周期的其他函数中,我们并不能保证请求仅在组件挂载完毕后才会要求响应。如果我们的数据请求在组件挂载之前就完成,并且调用了setState函数将数据添加到组件状态中,对于未挂载的组件则会报错。而在 componentDidMount 函数中进行 AJAX 请求则能有效避免这个问题

React 中的 setState 批量更新的过程是什么?

调用 setState 时,组件的 state 并不会立即改变, setState 只是把要修改的 state 放入一个队列, React 会优化真正的执行时机,并出于性能原因,会将 React 事件处理程序中的多次React 事件处理程序中的多次 setState 的状态修改合并成一次状态修改。 最终更新只产生一次组件及其子组件的重新渲染,这对于大型应用程序中的性能提升至关重要。


this.setState({  count: this.state.count + 1    ===>    入队,[count+1的任务]});this.setState({  count: this.state.count + 1    ===>    入队,[count+1的任务,count+1的任务]});                                         合并 state,[count+1的任务]                                         执行 count+1的任务
复制代码


需要注意的是,只要同步代码还在执行,“攒起来”这个动作就不会停止。(注:这里之所以多次 +1 最终只有一次生效,是因为在同一个方法中多次 setState 的合并动作不是单纯地将更新累加。比如这里对于相同属性的设置,React 只会为其保留最后一次的更新)。

Redux 内部原理 内部怎么实现 dispstch 一个函数的

redux-thunk中间件作为例子,下面就是thunkMiddleware函数的代码


// 部分转为ES5代码,运行middleware函数会返回一个新的函数,如下:return ({ dispatch, getState }) => {    // next实际就是传入的dispatch    return function (next) {        return function (action) {            // redux-thunk核心            if (typeof action === 'function') {                 return action(dispatch, getState, extraArgument);            }            return next(action);        };    };}
复制代码


redux-thunk库内部源码非常的简单,允许action是一个函数,同时支持参数传递,否则调用方法不变


  • redux创建Store:通过combineReducers函数合并reducer函数,返回一个新的函数combination(这个函数负责循环遍历运行reducer函数,返回全部state)。将这个新函数作为参数传入createStore函数,函数内部通过 dispatch,初始化运行传入的combination,state 生成,返回 store 对象

  • redux中间件:applyMiddleware函数中间件的主要目的就是修改dispatch函数,返回经过中间件处理的新的dispatch函数

  • redux使用:实际就是再次调用循环遍历调用reducer函数,更新state

高阶组件的应用场景

权限控制


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


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

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

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

什么原因会促使你脱离 create-react-app 的依赖

当你想去配置 webpack 或 babel presets。

React diff 算法的原理是什么?

实际上,diff 算法探讨的就是虚拟 DOM 树发生变化后,生成 DOM 树更新补丁的方式。它通过对比新旧两株虚拟 DOM 树的变更差异,将更新补丁作用于真实 DOM,以最小成本完成视图更新。 具体的流程如下:


  • 真实的 DOM 首先会映射为虚拟 DOM;

  • 当虚拟 DOM 发生变化后,就会根据差距计算生成 patch,这个 patch 是一个结构化的数据,内容包含了增加、更新、移除等;

  • 根据 patch 去更新真实的 DOM,反馈到用户的界面上。

  • 一个简单的例子:


import React from 'react'export default class ExampleComponent extends React.Component {  render() {    if(this.props.isVisible) {       return <div className="visible">visbile</div>;    }     return <div className="hidden">hidden</div>;  }}
复制代码


这里,首先假定 ExampleComponent 可见,然后再改变它的状态,让它不可见 。映射为真实的 DOM 操作是这样的,React 会创建一个 div 节点。


<div class="visible">visbile</div>
复制代码


当把 visbile 的值变为 false 时,就会替换 class 属性为 hidden,并重写内部的 innerText 为 hidden。这样一个生成补丁、更新差异的过程统称为 diff 算法。


diff 算法可以总结为三个策略,分别从树、组件及元素三个层面进行复杂度的优化:


策略一:忽略节点跨层级操作场景,提升比对效率。(基于树进行对比)


这一策略需要进行树比对,即对树进行分层比较。树比对的处理手法是非常“暴力”的,即两棵树只对同一层次的节点进行比较,如果发现节点已经不存在了,则该节点及其子节点会被完全删除掉,不会用于进一步的比较,这就提升了比对效率。


策略二:如果组件的 class 一致,则默认为相似的树结构,否则默认为不同的树结构。(基于组件进行对比)


在组件比对的过程中:


  • 如果组件是同一类型则进行树比对;

  • 如果不是则直接放入补丁中。


只要父组件类型不同,就会被重新渲染。这也就是为什么 shouldComponentUpdate、PureComponent 及 React.memo 可以提高性能的原因。


策略三:同一层级的子节点,可以通过标记 key 的方式进行列表对比。(基于节点进行对比)


元素比对主要发生在同层级中,通过标记节点操作生成补丁。节点操作包含了插入、移动、删除等。其中节点重新排序同时涉及插入、移动、删除三个操作,所以效率消耗最大,此时策略三起到了至关重要的作用。通过标记 key 的方式,React 可以直接移动 DOM 节点,降低内耗。

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

第二个参数是一个函数,该函数会在 setState 函数调用完成并且组件开始重渲染时调用,可以用该函数来监听渲染是否完成。


this.setstate(  {    username: "有课前端网",  },  () => console.log("re-rendered success. "));
复制代码

React 组件的构造函数有什么作用?它是必须的吗?

构造函数主要用于两个目的:


  • 通过将对象分配给 this.state 来初始化本地状态

  • 将事件处理程序方法绑定到实例上


所以,当在 React class 中需要设置 state 的初始值或者绑定事件时,需要加上构造函数,官方 Demo:


class LikeButton extends React.Component {  constructor() {    super();    this.state = {      liked: false    };    this.handleClick = this.handleClick.bind(this);  }  handleClick() {    this.setState({liked: !this.state.liked});  }  render() {    const text = this.state.liked ? 'liked' : 'haven\'t liked';    return (      <div onClick={this.handleClick}>        You {text} this. Click to toggle.      </div>    );  }}ReactDOM.render(  <LikeButton />,  document.getElementById('example'));
复制代码


构造函数用来新建父类的 this 对象;子类必须在 constructor 方法中调用 super 方法;否则新建实例时会报错;因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super 方法;子类就得不到 this 对象。


注意:


  • constructor () 必须配上 super(), 如果要在 constructor 内部使用 this.props 就要 传入 props , 否则不用

  • JavaScript 中的 bind 每次都会返回一个新的函数, 为了性能等考虑, 尽量在 constructor 中绑定事件

React 中 refs 干嘛用的?

Refs 提供了一种访问在render方法中创建的 DOM 节点或者 React 元素的方法。在典型的数据流中,props 是父子组件交互的唯一方式,想要修改子组件,需要使用新的pros重新渲染它。凡事有例外,某些情况下咱们需要在典型数据流外,强制修改子代,这个时候可以使用 Refs。咱们可以在组件添加一个 ref 属性来使用,该属性的值是一个回调函数,接收作为其第一个参数的底层 DOM 元素或组件的挂载实例。


class UnControlledForm extends Component {  handleSubmit = () => {    console.log("Input Value: ", this.input.value);  };  render() {    return (      <form onSubmit={this.handleSubmit}>        <input type="text" ref={(input) => (this.input = input)} />        <button type="submit">Submit</button>      </form>    );  }}
复制代码


请注意,input 元素有一个ref属性,它的值是一个函数。该函数接收输入的实际 DOM 元素,然后将其放在实例上,这样就可以在 handleSubmit 函数内部访问它。 经常被误解的只有在类组件中才能使用 refs,但是refs也可以通过利用 JS 中的闭包与函数组件一起使用。


function CustomForm({ handleSubmit }) {  let inputElement;  return (    <form onSubmit={() => handleSubmit(inputElement.value)}>      <input type="text" ref={(input) => (inputElement = input)} />      <button type="submit">Submit</button>    </form>  );}
复制代码

react-router4 的核心

  • 路由变成了组件

  • 分散到各个页面,不需要配置 比如<link> <route></route>


用户头像

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

还未添加个人简介

评论

发布
暂无评论
前端一面常见react面试题(持续更新中)_前端_夏天的味道123_InfoQ写作社区