写点什么

前端面试指南之 React 篇(二)

作者:beifeng1996
  • 2022-11-02
    浙江
  • 本文字数:8999 字

    阅读完需:约 30 分钟

react 中这两个生命周期会触发死循环

componentWillUpdate生命周期在shouldComponentUpdate返回 true 后被触发。在这两个生命周期只要视图更新就会触发,因此不能再这两个生命周期中使用 setState。否则会导致死循环

react 性能优化是在哪个生命周期函数中

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

React 有哪些优化性能的手段

类组件中的优化手段


  • 使用纯组件 PureComponent 作为基类。

  • 使用 React.memo 高阶函数包装组件。

  • 使用 shouldComponentUpdate 生命周期函数来自定义渲染逻辑。


方法组件中的优化手段


  • 使用 useMemo

  • 使用 useCallBack


其他方式


  • 在列表需要频繁变动时,使用唯一 id 作为 key,而不是数组下标。

  • 必要时通过改变 CSS 样式隐藏显示组件,而不是通过条件判断显示隐藏组件。

  • 使用 Suspense 和 lazy 进行懒加载,例如:


import React, { lazy, Suspense } from "react";
export default class CallingLazyComponents extends React.Component { render() { var ComponentToLazyLoad = null;
if (this.props.name == "Mayank") { ComponentToLazyLoad = lazy(() => import("./mayankComponent")); } else if (this.props.name == "Anshul") { ComponentToLazyLoad = lazy(() => import("./anshulComponent")); }
return ( <div> <h1>This is the Base User: {this.state.name}</h1> <Suspense fallback={<div>Loading...</div>}> <ComponentToLazyLoad /> </Suspense> </div> ) }}
复制代码

React 的生命周期方法有哪些?

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

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

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

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

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

  • componentDidUpdate:它主要用于更新 DOM 以响应propsstate更改。

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

约束性组件( controlled component)与非约束性组件( uncontrolled  component)有什么区别?

在 React 中,组件负责控制和管理自己的状态。如果将 HTML 中的表单元素( input、 select、 textarea 等)添加到组件中,当用户与表单发生交互时,就涉及表单数据存储问题。根据表单数据的存储位置,将组件分成约東性组件和非约東性组件。约束性组件( controlled component)就是由 React 控制的组件,也就是说,表单元素的数据存储在组件内部的状态中,表单到底呈现什么由组件决定。如下所示, username 没有存储在 DOM 元素内,而是存储在组件的状态中。每次要更新 username 时,就要调用 setState 更新状态;每次要获取 username 的值,就要获取组件状态值。


class App extends Component {  //初始化状态  constructor(props) {    super(props);    this.state = {      username: "有课前端网",    };  }  //查看结果  showResult() {    //获取数据就是获取状态值    console.log(this.state.username);  }  changeUsername(e) {    //原生方法获取    var value = e.target.value;    //更新前,可以进行脏值检测    //更新状态    this.setState({      username: value,    });  }  //渲染组件  render() {    //返回虚拟DOM    return (      <div>        <p>          {/*输入框绑定va1ue*/}          <input type="text" onChange={this.changeUsername.bind(this)} value={this.state.username} />        </p>        <p>          <button onClick={this.showResult.bind(this)}>查看结果</button>        </p>      </div>    );  }}
复制代码


非约束性组件( uncontrolled component)就是指表单元素的数据交由元素自身存储并处理,而不是通过 React 组件。表单如何呈现由表单元素自身决定。如下所示,表单的值并没有存储在组件的状态中,而是存储在表单元素中,当要修改表单数据时,直接输入表单即可。有时也可以获取元素,再手动修改它的值。当要获取表单数据时,要首先获取表单元素,然后通过表单元素获取元素的值。注意:为了方便在组件中获取表单元素,通常为元素设置 ref 属性,在组件内部通过 refs 属性获取对应的 DOM 元素。


class App extends Component {  //查看结果  showResult() {    //获取值    console.log(this.refs.username.value);    //修改值,就是修改元素自身的值    this.refs.username.value = "专业前端学习平台";    //渲染组件    //返回虚拟DOM    return (      <div>        <p>          {/*非约束性组件中,表单元素通过 defaultvalue定义*/}          <input type="text" ref=" username" defaultvalue="有课前端网" />        </p>        <p>          <button onClick={this.showResult.bind(this)}>查看结果</button>        </p>      </div>    );  }}
复制代码


虽然非约東性组件通常更容易实现,可以通过 refs 直接获取 DOM 元素,并获取其值,但是 React 建议使用约束性组件。主要原因是,约東性组件支持即时字段验证,允许有条件地禁用/启用按钮,强制输入格式等。

React 父组件如何调用子组件中的方法?

  1. 如果是在方法组件中调用子组件(>= react@16.8),可以使用 useRef 和 useImperativeHandle:


const { forwardRef, useRef, useImperativeHandle } = React;
const Child = forwardRef((props, ref) => { useImperativeHandle(ref, () => ({ getAlert() { alert("getAlert from Child"); } })); return <h1>Hi</h1>;});
const Parent = () => { const childRef = useRef(); return ( <div> <Child ref={childRef} /> <button onClick={() => childRef.current.getAlert()}>Click</button> </div> );};
复制代码


  1. 如果是在类组件中调用子组件(>= react@16.4),可以使用 createRef:


const { Component } = React;
class Parent extends Component { constructor(props) { super(props); this.child = React.createRef(); }
onClick = () => { this.child.current.getAlert(); };
render() { return ( <div> <Child ref={this.child} /> <button onClick={this.onClick}>Click</button> </div> ); }}
class Child extends Component { getAlert() { alert('getAlert from Child'); }
render() { return <h1>Hello</h1>; }}
复制代码

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

该函数会在 setState 函数调用完成并且组件开始重渲染的时候被调用,我们可以用该函数来监听渲染是否完成:


this.setState(  { username: 'tylermcginnis33' },  () => console.log('setState has finished and the component has re-rendered.'))
复制代码


this.setState((prevState, props) => {  return {    streak: prevState.streak + props.count  }})
复制代码


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

类组件与函数组件有什么异同?

相同点: 组件是 React 可复用的最小代码片段,它们会返回要在页面中渲染的 React 元素。也正因为组件是 React 的最小编码单位,所以无论是函数组件还是类组件,在使用方式和最终呈现效果上都是完全一致的。


我们甚至可以将一个类组件改写成函数组件,或者把函数组件改写成一个类组件(虽然并不推荐这种重构行为)。从使用者的角度而言,很难从使用体验上区分两者,而且在现代浏览器中,闭包和类的性能只在极端场景下才会有明显的差别。所以,基本可认为两者作为组件是完全一致的。


不同点:


  • 它们在开发时的心智模型上却存在巨大的差异。类组件是基于面向对象编程的,它主打的是继承、生命周期等核心概念;而函数组件内核是函数式编程,主打的是 immutable、没有副作用、引用透明等特点。

  • 之前,在使用场景上,如果存在需要使用生命周期的组件,那么主推类组件;设计模式上,如果需要使用继承,那么主推类组件。但现在由于 React Hooks 的推出,生命周期概念的淡出,函数组件可以完全取代类组件。其次继承并不是组件最佳的设计模式,官方更推崇“组合优于继承”的设计概念,所以类组件在这方面的优势也在淡出。

  • 性能优化上,类组件主要依靠 shouldComponentUpdate 阻断渲染来提升性能,而函数组件依靠 React.memo 缓存渲染结果来提升性能。

  • 从上手程度而言,类组件更容易上手,从未来趋势上看,由于 React Hooks 的推出,函数组件成了社区未来主推的方案。

  • 类组件在未来时间切片与并发模式中,由于生命周期带来的复杂度,并不易于优化。而函数组件本身轻量简单,且在 Hooks 的基础上提供了比原先更细粒度的逻辑组织与复用,更能适应 React 的未来发展。

对于 store 的理解

Store 就是把它们联系到一起的对象。Store 有以下职责:


  • 维持应用的 state;

  • 提供 getState() 方法获取 state;

  • 提供 dispatch(action) 方法更新 state;

  • 通过 subscribe(listener)注册监听器;

  • 通过 subscribe(listener)返回的函数注销监听器

使用状态要注意哪些事情?

要注意以下几点。


  • 不要直接更新状态

  • 状态更新可能是异步的

  • 状态更新要合并。

  • 数据从上向下流动

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

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

react 组件的划分业务组件技术组件?

  • 根据组件的职责通常把组件分为 UI 组件和容器组件。

  • UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。

  • 两者通过React-Redux 提供connect方法联系起来

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

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


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

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

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

key 可以帮助 React 跟踪循环创建列表中的虚拟 DOM 元素,了解哪些元素已更改、添加或删除。每个绑定 key 的虚拟 DOM 元素,在兄弟元素之间都是独一无二的。在 React 的和解过程中,比较新的虛拟 DOM 树与上一个虛拟 DOM 树之间的差异,并映射到页面中。key 使 React 处理列表中虛拟 DOM 时更加高效,因为 React 可以使用虛拟 DOM 上的 key 属性,快速了解元素是新的、需要删除的,还是修改过的。如果没有 key,Rεat 就不知道列表中虚拟 DOM 元素与页面中的哪个元素相对应。所以在创建列表的时候,不要忽略 key。

hooks 和 class 比较的优势?

一、更容易复用代码


二、清爽的代码风格+代码量更少


缺点


状态不同步


不好用的 useEffect,

为什么要使用 React. Children. map( props. children,( )=>)而不是 props. children. map ( (  ) => )?

因为不能保证 props. children 将是一个数组。以下面的代码为例。


<Parent>    <h1>有课前端网</h1></Parent>
复制代码


在父组件内部,如果尝试使用 props.children. map 映射子对象,则会抛出错误,因为 props. children 是一个对象,而不是一个数组。如果有多个子元素, React 会使 props.children 成为一个数组,如下所示。


<Parent>  <h1>有课前端网</h1>  <h2>前端技术学习平台</h2></Parent>;//不建议使用如下方式,在这个案例中会抛出错误。
class Parent extends Component { render() { return <div> {this.props.children.map((obj) => obj)}</div>; }}
复制代码


建议使用如下方式,避免在上一个案例中抛出错误。


class Parent extends Component {  render() {    return <div> {React.Children.map(this.props.children, (obj) => obj)}</div>;  }}
复制代码

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 与 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 生命周期钩子被标记为 UNSAFE

componentWillMount

componentWillMount 生命周期发生在首次渲染前,一般使用的小伙伴大多在这里初始化数据或异步获取外部数据赋值。初始化数据,react 官方建议放在 constructor 里面。而异步获取外部数据,渲染并不会等待数据返回后再去渲染


class Example extends React.Component {       state = {        value: ''    };    componentWillMount() {            this.setState({                   value: this.props.source.value        });               this.props.source.subscribe(this.handleChange);    }       componentWillUnmount() {            this.props.source.unsubscribe(this.handleChange );     }       handleChange = source => {            this.setState({            value: source.value        });       }; }
复制代码


  • 试想一下,假如组件在第一次渲染的时候被中断,由于组件没有完成渲染,所以并不会执行 componentWillUnmount 生命周期(注:很多人经常认为 componentWillMount 和 componentWillUnmount 总是配对,但这并不是一定的。只有调用 componentDidMount 后,React 才能保证稍后调用 componentWillUnmount 进行清理)。因此 handleSubscriptionChange 还是会在数据返回成功后被执行,这时候 setState 由于组件已经被移除,就会导致内存泄漏。所以建议把异步获取外部数据写在 componentDidMount 生命周期里,这样就能保证 componentWillUnmount 生命周期会在组件移除的时候被执行,避免内存泄漏的风险。

  • 现在,小伙伴清楚为什么了要用UNSAFE_componentWillMount替换componentWillMount了吧

componentWillReceiveProps

componentWillReceiveProps 生命周期是在 props 更新时触发。一般用于 props 参数更新时同步更新 state 参数。但如果在 componentWillReceiveProps 生命周期直接调用父组件的某些有调用 setState 的函数,会导致程序死循环


// 如下是子组件componentWillReceiveProps里调用父组件改变state的函数示例
class Parent extends React.Component{ constructor(){ super(); this.state={ list: [], selectedData: {} }; }
changeSelectData = selectedData => { this.setState({ selectedData }); }
render(){ return ( <Clild list={this.state.list} changeSelectData={this.changeSelectData}/> ); }}
...class Child extends React.Component{ constructor(){ super(); this.state={ list: [] }; } componentWillReceiveProps(nextProps){ this.setState({ list: nextProps.list }) nextProps.changeSelectData(nextProps.list[0]); //默认选择第一个 } ...}
复制代码


  • 如上代码,在 Child 组件的 componentWillReceiveProps 里直接调用 Parent 组件的 changeSelectData 去更新 Parent 组件 state 的 selectedData 值。会触发 Parent 组件重新渲染,而 Parent 组件重新渲染会触发 Child 组件的 componentWillReceiveProps 生命周期函数执行。如此就会陷入死循环。导致程序崩溃。

  • 所以,React 官方把 componentWillReceiveProps 替换为 UNSAFE_componentWillReceiveProps,让小伙伴在使用这个生命周期的时候注意它会有缺陷,要注意避免,比如上面例子,Child 在 componentWillReceiveProps 调用 changeSelectData 时先判断 list 是否有更新再确定是否要调用,就可以避免死循环。

componentWillUpdate

componentWillUpdate 生命周期在视图更新前触发。一般用于视图更新前保存一些数据方便视图更新完成后赋值。 案例三:如下是列表加载更新后回到当前滚动条位置的案例


class ScrollingList extends React.Component {       listRef = null;       previousScrollOffset = null;       componentWillUpdate(nextProps, nextState) {            if (this.props.list.length < nextProps.list.length) {                  this.previousScrollOffset = this.listRef.scrollHeight - this.listRef.scrollTop;            }     }       componentDidUpdate(prevProps, prevState) {            if (this.previousScrollOffset !== null) {                  this.listRef.scrollTop = this.listRef.scrollHeight - this.previousScrollOffset;              this.previousScrollOffset = null;            }       }       render() {            return (                   `<div>` {/* ...contents... */}`</div>`             );       }       setListRef = ref => {    this.listRef = ref;   };
复制代码


  • 由于 componentWillUpdate 和 componentDidUpdate 这两个生命周期函数有一定的时间差(componentWillUpdate 后经过渲染、计算、再更新 DOM 元素,最后才调用 componentDidUpdate),如果这个时间段内用户刚好拉伸了浏览器高度,那 componentWillUpdate 计算的 previousScrollOffset 就不准确了。如果在 componentWillUpdate 进行 setState 操作,会出现多次调用只更新一次的问题,把 setState 放在 componentDidUpdate,能保证每次更新只调用一次。

  • 所以,react 官方建议把 componentWillUpdate 替换为 UNSAFE_componentWillUpdate。如果真的有以上案例的需求,可以使用 16.3 新加入的一个周期函数 getSnapshotBeforeUpdat

结论

  • React 意识到 componentWillMount、componentWillReceiveProps 和 componentWillUpdate 这三个生命周期函数有缺陷,比较容易导致崩溃。但是由于旧的项目已经在用以及有些老开发者习惯用这些生命周期函数,于是通过给它加 UNSAFE_来提醒用它的人要注意它们的缺陷

  • React 加入了两个新的生命周期函数 getSnapshotBeforeUpdate 和 getDerivedStateFromProps,目的为了即使不使用这三个生命周期函数,也能实现只有这三个生命周期能实现的功能

diff 算法是怎么运作

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


用户头像

beifeng1996

关注

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

还未添加个人简介

评论

发布
暂无评论
前端面试指南之React篇(二)_React_beifeng1996_InfoQ写作社区