前端Reract面试题-------总结(经典)

本文详细介绍了React面试中的常见问题,涵盖组件初始化、React生命周期、组件间通信、优化技巧、路由配置、数据流管理等多个方面。文章通过实例解析了React的事件处理、状态管理和渲染优化,并探讨了如何在组件中进行事件代理、防止不必要的渲染,以及如何使用Redux进行状态管理。同时,文章还讨论了React Router的动态加载和数据流流程,提供了实际的应用场景和解决方案。
摘要由CSDN通过智能技术生成

1.React 中 keys 的作用是什么?

Keys 是 React 用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识。

render () {
  return (
   ‹ul›
     {
      this.state.list.map(({item, key}) =› {
        return ‹li key={key}›{item}‹/li›
      })
     }
    ‹/ul›
  )
}

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

2.当你调用 setState 的时候,发生了什么事?

将传递给 setState 的对象合并到组件的当前状态,这将启动一个和解的过程,构建一个新的 react 元素树,与上一个元素树进行对比( diff ),从而进行最小化的重渲染。

3.为什么setState 的参数是一个 callback 而不是一个对

因为 this.props 和 this.state 的更新可能是异步的,不能依赖它们的值去计算下一个 state

4.状态(state)和属性(props)之间有何区别

State 是一种数据结构,用于组件挂载时所需数据的默认值。

State 可能会随着时间的推移而发生突变,但多数时候是作为用户事件行为的结果。

Props(properties 的简写)则是组件的配置。props 由父组件传递给子组件,并且就子组件而言,props 是不可变的(immutable)。

组件不能改变自身的 props,但是可以把其子组件的 props 放在一起(统一管理)。Props 也不仅仅是数据--回调函数也可以通过 props 传递。

5.应该在 React 组件的何处发起 Ajax 请求?

在 React 组件中,应该在 componentDidMount 中发起网络请求。这个方法会在组件第一次“挂载”(被添加到 DOM)时执行,在组件的生命周期中仅会执行一次。

更重要的是,你不能保证在组件挂载之前 Ajax 请求已经完成,如果是这样,也就意味着你将尝试在一个未挂载的组件上调用 setState,这将不起作用。 在 componentDidMount 中发起网络请求将保证这有一个组件可以更新了。

6.React 中的三种构建组件的方式?

React.createClass()ES6 class 和无状态函数。

7.React 中 refs 的作用是什么?

Refs 是 React提供给我们的安全访问DOM` 元素或者某个组件实例的句柄。

我们可以为元素添加 ref 属性然后在回调函数中接受该元素在 DOM 树中的句柄,该值会作为回调函数的第一个参数返回:

class CustomForm 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 属性,该属性声明的回调函数会接收 input 对应的 DOM 元素,我们将其绑定到 this指针以便在其他的类函数中使用。

另外值得一提的是,refs 并不是类组件的专属,函数式组件同样能够利用闭包暂存其值:

function CustomForm({ handleSubmit }) {
  let inputElement;
  return (
    ‹form onSubmit={() =› handleSubmit(inputElement.value)}›
      ‹input type='text' ref={input =› (inputElement = input)} /›
      ‹button type='submit'›Submit‹/button›
    ‹/form›
  );
}

8.请描述下react diff 原理(常考,大厂必考)?

把树形结构按照层级分解,只比较同级元素。

给列表结构的每个单元添加唯一的 key 属性,方便比较。

React 只会匹配相同 class 的 component(这里面的 class 指的是组件的名字) 合并操作,调用 component 的 setState 方法的时候, React 将其标记为 dirty. 到每一个事件循环结束, React 检查所有标记 dirty 的 component 重新绘制. 选择性子树渲染。开发人员可以重写 shouldComponentUpdate 提高 diff 的性能。

9.说说React 优势?

1、React 速度很快:它并不直接对 DOM 进行操作,引入了一个叫做虚拟 DOM 的概念,安插在 javascript 逻辑和实际的 DOM 之间,性能好。

2、跨浏览器兼容:虚拟 DOM 帮助我们解决了跨浏览器问题,它为我们提供了标准化的 API,甚至在 IE8 中都是没问题的。

3、一切都是 component:代码更加模块化,重用代码更容易,可维护性高。

4、单向数据流:Flux 是一个用于在 JavaScript 应用中创建单向数据层的架构,它随着 React 视图库的开发而被 Facebook 概念化。

5、同构、纯粹的 javascript:因为搜索引擎的爬虫程序依赖的是服务端响应而不是 JavaScript 的执行,预渲染你的应用有助于搜索引擎优化。

6、兼容性好:比如使用 RequireJS 来加载和打包,而 Browserify 和 Webpack 适用于构建大型应用。它们使得那些艰难的任务不再让人望而生畏。

10.说说 react 生命周期函数?

初始化阶段:

getDefaultProps:获取实例的默认属性

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

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

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

componentDidMount:组件真正在被装载之后 运行中状态:

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

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

render:组件重新描绘

componentDidUpdate:组件已经更新

销毁阶段:

componentWillUnmount:组件即将销毁

11.React组件中怎么做事件代理理

虽然很多资料都说 React 的事件是会被代理到 document 上,但是我翻遍了官网,也没有找到相应的说明。

那么,有什么办法能够证明它吗?我想到了一个方法 → 通过 Chrome 浏览器的 Event Listeners 面板查看元素的绑定事件,具体的使用方法请参考官网文档。

从图中我们可以看到:

1.#child 元素绑定了两个点击事件,一个是通过 React 绑定的,一个是通过 addEventListener 绑定的。 2.通过 addEventListener 绑定的事件是真的绑定到 #child 元素上。 3.通过 React 绑定的事件,其实是代理绑定到 document 上。

React 模拟 DOM 事件冒泡机制

观察下面这个例子:#child 和 #parent 都绑定了一个点击事件。

由图中可以看出:点击 #child 的同时,也触发了 #parent 的点击事件,看起来“很像” DOM 的事件冒泡机制。然而,实际原理并非如此,因为按照 React 的事件代理,#child 和 #parent 绑定的事件本来就是代理到 document 上的。也就是说,只有当事件流冒泡到 document 上时,才会依次触发 document 上绑定的两个事件。

到此为止,我以为我终于搞明白这块了,后来我发现我还是错了。如果说 #child 和 #parent 的事件都代理到 document 上的话,那么在 Event Listeners 面板中,我们应该能看到 2 个绑定在 document 上的事件,但实际上只有 1 个,如下图所示。

因此,我们可以得出结论:并非 #child 和 #parent 的事件分别代理到 document 上,而是 React 在 document 上绑定了一个 dispatchEvent 函数(事件),在执行 dispatchEvent 的过程中,其内部会依次执行 #child 和 #parent 上绑定的事件。请注意,虽然 dispatchEvent 和代理到 document 上这两种方式的表现结果一样,但是其本质是有很大差别的,后边我们结合到 stopImmediatePropagation 的时候便会讲到。

那么这个 dispatchEvent 函数又是如何做到依次触发 #child 和 #parent 的事件的呢?我无力研究 React 这部分的源码,只好自己猜想了一下,其伪代码可能是这样子:

 function dispatchEvent(event) {
     let target = event.target;
     target.click && target.click();  // 触发点击元素的事件
     while (target.parentNode) {      // 沿 DOM 向上回溯,遍历父节点,触发其 click 事件
         target.parentNode.click && target.parentNode.click();
         target = target.parentNode;
     }
 }

这应该便是 React 模拟 DOM 事件冒泡的大致原理。

React 禁止事件冒泡

既然有“事件冒泡”,就得有相应的禁止它的方法,这一点 React 的官网中便有提到:通过 React 绑定的事件,其回调函数中的 event 对象,是经过 React 合成的 SyntheticEvent,与原生的 DOM 事件的 event 不是一回事。准确地说,在 React 中,e.nativeEvent 才是原生 DOM 事件的那个 event,虽然 React 的合成事件对象也同样实现了 stopPropagation 接口。

因此,在 React 中,想要阻止“事件冒泡”(再强调一次,React 只是模拟事件冒泡,并非真正的 DOM 事件冒泡),只需要在回调函数中调用 e.stopPropagation。请注意,这时候的 e.stopPropagation非原生事件对象的 stopPropagation

以上这些都是官网中已经有的,那本文又有什么新意呢?请看下面的例子:#child#parent 和 document 上都绑定了事件,如何做到只触发 #child 上的事件?

我们来尝试解释一下上图中的现象:

事件流首先进入到 #child ,然后触发直接绑定在 #child 上的事件;

事件流沿着 DOM 结构向上冒泡到 document,触发 React 绑定的 dispatchEvent 函数,从而调用了 #child 子元素上绑定的 clickChild 方法。

在 clickChild 方法的最后,我调用了 e.stopPropagation,成功地阻止了 React 模拟的事件冒泡,因此,成功地没有触发 #parent 上的事件。

然后,最后出现了问题,还是触发了 document 上直接绑定的事件。我想要的是:”点击 #child ,只触发 #child 上的事件,不要触发任何其他元素的事件,包括 document,我应该怎么做呢? → 答案是:调用e.nativeEvent.stopImmediatePropagation 上述过程用图解的方式来分析,我们能理解得清楚一些。

React 合成事件对象的e.stopPropagation,只能阻止 React 模拟的事件冒泡,并不能阻止真实的 DOM 事件冒泡,更加不能阻止已经触发元素的多个事件的依次执行。在这种情况下,只有原生事件对象的 stopImmediatePropagation能做到。

你可能会说:”既然 React 在合成事件对象中封装了 stopPropagation,为什么不把 stopImmediatePropagation 也一并封装了呢?“

我的猜测是:”因为在 React 中不允许给同一个组件绑定多个相同类型的事件,如果非要重复绑定,那么后绑定的会覆盖前绑定的,这是它的设计思路。在这种设计思路下,不会存在某个组件有多个同类型的事件会依次触发,自然便不需要 stopImmediatePropagation 了。

总结

对于 React 的合成事件对象 e 来说:

e.stopPropagation → 用来阻止 React 模拟的事件冒泡

e.stopImmediatePropagation → 没有这个函数

e.nativeEvent.stopPropagation → 原生事件对象的用于阻止 DOM 事件的进一步捕获或者冒泡

e.nativeEvent.stopImmediatePropagation → 原生事件对象的用于阻止 DOM 事件的进一步捕获或者冒泡,且该元素的后续绑定的相同事件类型的事件也被一并阻止。

12.this 的各种情况

call apply bind指的this是谁就是谁(bind不会调用,只会将当前的函数返回)

fun.call(obj,a,b) 

fun.apply(obj,[  ])

fun.bind(obj,a,b)()

this的情况:

1.以函数形式调用时,this永远都是window

2.以方法的形式调用时,this是调用方法的对象

3.以构造函数的形式调用时,this是新创建的那个对象

4.使用callapply调用时,this是指定的那个对象

5.箭头函数:箭头函数的this看外层是否有函数

1)如果有,外层函数的this就是内部箭头函数的this 2)如果没有,就是window

6.特殊情况:通常意义上this指针指向为最后调用它的对象。这里需要注意的一点就是如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例

13.介绍Promise,异常捕获,如何进行异常处理?

Promise是解决回调地狱的好工具,比起直接使用回调函数promise的语法结构更加清晰,代码的可读性大大增加。

但是想要在真是的项目中恰当的运用promise可不是随便写个Demo这个简单的,如果运用不当反而会增加代码的复杂性。

使用Promise经常遇到的问题

老旧浏览器没有Promise全局对象增么办?

可以使用es6-promise-polyfilles6-promise-polyfill可以使用页面标签直接引入,当然也可以通过es6import方法引入。

引入这个polyfill之后,它会在window对象中加入Promise对象。这样我们就可以全局使用Promise了。

如何进行异常处理?

参照promise的文档我们可以在reject回调和catch中处理异常。但是promise规定如果一个错误在reject函数中被处理,那么promise将从异常常态中恢复过来。这意味着接下来的then方法将接收到一个resolve回调。

大多数时候我们希望发生错误的时候,promise处理当前的异常并中断后续的then操作。 我们先来看一个使用reject处理异常的例子

var promiseStart = new Promise(function(resolve, reject){
    reject('promise is rejected');
});

promiseStart.then(function(response) {
    console.log('resolved');
    return new Promise(function(resolve, reject){
        resolve('promise is resolved');
    });
})
.then(function (response){
    console.log('resolved:', response);
})
.catch(function(error) {
    console.log('catched:', error);
})
 输出:
catched: promise is rejected

14.React怎么做数据的检查和变化

props:组件属性,专门用来连接父子组件间通信,父组件传输父类成员,子组件可以利用但不能编辑父类成员;

state:专门负责保存和改变组件内部的状态;

数据传递

React中,父组件给子组件传递数据时,通过给子组件设置props的方式,子组件取得props中的值,即可完成数据传递.被传递数据的格式可以是任何js可识别的数据结构

props一般只作为父组件给子组件传递数据用,不要试图去修改自己的props

数据改变

props不能被自身修改,如果组建内部的属性发生变化使用state

this.setState({ 
    ... 
})

React会实时监听每个组件的propsstate的值,一旦有变化,会立刻更新组件,将结果重新渲染到页面上,stateprops

15.介绍常见的react优化方式

React 渲染性能优化的三个方向,其实也适用于其他软件开发领域,这三个方向分别是:

1.减少计算的量。 -> 对应到 React 中就是减少渲染的节点 或者 降低组件渲染的复杂度

2.利用缓存。-> 对应到 React 中就是如何避免重新渲染,利用函数式编程的 memo 方式来避免组件重新渲染

3.精确重新计算的范围。 对应到 React 中就是绑定组件和状态关系, 精确判断更新的'时机'和'范围'. 只重新渲染'脏'的组件,或者说降低渲染范围

目录

减少渲染的节点/降低渲染计算量(复杂度)

0️⃣ 不要在渲染函数都进行不必要的计算

1️⃣ 减少不必要的嵌套

2️⃣ 虚拟列表

3️⃣ 惰性渲染

4️⃣ 选择合适的样式方案

避免重新渲染

0️⃣ 简化 props

1️⃣ 不变的事件处理器

2️⃣ 不可变数据

3️⃣ 简化 state

4️⃣ 使用 recompose 精细化比对

精细化渲染

0️⃣ 响应式数据的精细化渲染

1️⃣ 不要滥用 Context

扩展

减少渲染的节点/降低渲染计算量(复杂度) 首先从计算的量上下功夫,减少节点渲染的数量或者降低渲染的计算量可以显著的提高组件渲染性能。

0️⃣ 不要在渲染函数都进行不必要的计算

比如不要在渲染函数(render)中进行数组排序、数据转换、订阅事件、创建事件处理器等等. 渲染函数中不应该放置太多副作用

1️⃣ 减少不必要的嵌套

所以我们需要理性地选择一些工具,比如使用原生的 CSS,减少 React 运行时的负担.

一般不必要的节点嵌套都是滥用高阶组件/RenderProps 导致的。所以还是那句话‘只有在必要时才使用 xxx’。 有很多种方式来代替高阶组件/RenderProps,例如优先使用 props、React Hooks

2️⃣ 虚拟列表

虚拟列表是常见的‘长列表'和'复杂组件树'优化方式,它优化的本质就是减少渲染的节点。

虚拟列表常用于以下组件场景:

无限滚动列表,grid, 表格,下拉列表,spreadsheets 无限切换的日历或轮播图 大数据量或无限嵌套的树 聊天窗,数据流(feed), 时间轴 等等

3️⃣ 惰性渲染

惰性渲染的初衷本质上和虚表一样,也就是说我们只在必要时才去渲染对应的节点。

举个典型的例子,我们常用 Tab 组件,我们没有必要一开始就将所有 Tab 的 panel 都渲染出来,而是等到该 Tab 被激活时才去惰性渲染。

还有很多场景会用到惰性渲染,例如树形选择器,模态弹窗,下拉列表,折叠组件等等。

这里就不举具体的代码例子了,留给读者去思考.

4️⃣ 选择合适的样式方案

所以在样式运行时性能方面大概可以总结为:CSS > 大部分CSS-in-js > inline style

避免重新渲染 减少不必要的重新渲染也是 React 组件性能优化的重要方向. 为了避免不必要的组件重新渲染需要在做到两点:

保证组件纯粹性。即控制组件的副作用,如果组件有副作用则无法安全地缓存渲染结果 通过shouldComponentUpdate生命周期函数来比对 state 和 props, 确定是否要重新渲染。对于函数组件可以使用React.memo包装 另外这些措施也可以帮助你更容易地优化组件重新渲染:

0️⃣ 简化 props ① 如果一个组件的 props 太复杂一般意味着这个组件已经违背了‘单一职责’,首先应该尝试对组件进行拆解. ② 另外复杂的 props 也会变得难以维护, 比如会影响shallowCompare效率, 还会让组件的变动变得难以预测和调试.

下面是一个典型的例子, 为了判断列表项是否处于激活状态,这里传入了一个当前激活的 id:

这是一个非常糟糕的设计,一旦激活 id 变动,所有列表项都会重新刷新. 更好的解决办法是使用类似actived这样的布尔值 prop. actived 现在只有两种变动情况,也就是说激活 id 的变动,最多只有两个组件需要重新渲染.

简化的 props 更容易理解, 且可以提高组件缓存的命中率

1️⃣ 不变的事件处理器 ①避免使用箭头函数形式的事件处理器, 例如:

<ComplexComponent onClick={evt => onClick(evt.id)} otherProps={values} />

假设 ComplexComponent 是一个复杂的 PureComponent, 这里使用箭头函数,其实每次渲染时都会创建一个新的事件处理器,这会导致 ComplexComponent 始终会被重新渲染.

更好的方式是使用实例方法:

class MyComponent extends Component {
  render() {
    <ComplexComponent onClick={this.handleClick} otherProps={values} />;
  }
  handleClick = () => {
    /*...*/
  };
}

② 即使现在使用hooks,我依然会使用useCallback来包装事件处理器,尽量给下级组件暴露一个静态的函数:

const handleClick = useCallback(() => {
  /*...*/
}, []);

return <ComplexComponent onClick={handleClick} otherProps={values} />;

但是如果useCallback依赖于很多状态,你的useCallback可能会变成这样:

const handleClick = useCallback(() => {
  /*...*/
  //  
}, [foo, bar, baz, bazz, bazzzz]);

这种写法实在让人难以接受,这时候谁还管什么函数式非函数式的。我是这样处理的:

function useRefProps<T>(props: T) {
  const ref = useRef < T > props;
  // 每次渲染更新props
  useEffect(() => {
    ref.current = props;
  });
}

function MyComp(props) {
  const propsRef = useRefProps(props);

  // 现在handleClick是始终不变的
  const handleClick = useCallback(() => {
    const { foo, bar, baz, bazz, bazzzz } = propsRef.current;
    // do something
  }, []);
}

③设计更方便处理的 Event Props. 有时候我们会被逼的不得不使用箭头函数来作为事件处理器:

<List>
  {list.map(i => (
    <Item key={i.id} onClick={() => handleDelete(i.id)} value={i.value} />
  ))}
</List>

上面的 onClick 是一个糟糕的实现,它没有携带任何信息来标识事件来源,所以这里只能使用闭包形式,更好的设计可能是这样的:

// onClick传递事件来源信息
const handleDelete = useCallback((id: string) => {
  /*删除操作*/
}, []);

return (
  <List>
    {list.map(i => (
      <Item key={i.id} id={i.id} onClick={handleDelete} value={i.value} />
    ))}
  </List>
);

如果是第三方组件或者 DOM 组件呢? 实在不行,看能不能传递data-*属性:

const handleDelete = useCallback(event => {
  const id = event.dataset.id;
  /*删除操作*/
}, []);

return (
  <ul>
    {list.map(i => (
      <li key={i.id} data-id={i.id} onClick={handleDelete} value={i.value} />
    ))}
  </ul>
);

2️⃣ 不可变数据 不可变数据可以让状态变得可预测,也让 shouldComponentUpdate '浅比较'变得更可靠和高效.

相关的工具有Immutable.jsImmerimmutability-helper 以及 seamless-immutable

3️⃣ 简化 state

不是所有状态都应该放在组件的 state 中. 例如缓存数据。按照我的原则是:如果需要组件响应它的变动, 或者需要渲染到视图中的数据才应该放到 state 中。这样可以避免不必要的数据变动导致组件重新渲染.

4️⃣ 使用 recompose 精细化比对

尽管 hooks 出来后,recompose 宣称不再更新了,但还是不影响我们使用 recompose 来控制shouldComponentUpdate方法, 比如它提供了以下方法来精细控制应该比较哪些 props:

/* 相当于React.memo */
 pure()
 /* 自定义比较 */
 shouldUpdate(tes
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值