前端面试,备考第 19 天—— React 基础一点通

Posted 前端修罗场

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端面试,备考第 19 天—— React 基础一点通相关的知识,希望对你有一定的参考价值。

  • ⭐️ 本文首发自 前端修罗场(点击加入社区,参与学习打卡,获取奖励)是一个由资深开发者独立运行的专业技术社区,我专注 Web 技术、答疑解惑、面试辅导以及职业发展。
  • 🏆 目前就职于全球前100强知名外企,曾就职于头部互联网企业 | 清华大学出版社签约作者 | CSDN 银牌讲师 | 蓝桥云课2021年度人气作者Top2 | CSDN 博客专家 | 阿里云专家博主 | 华为云享专家
    出品著作
    《ElementUI 详解与实战》| 《ThreeJS 在网页中创建动画》|《PWA 渐进式Web应用开发》
  • 🔥 本文已收录至前端面试题库专栏 《前端面试宝典》(点击订阅)
  • 💯 此专栏文章针对准备找工作的应届生、初中高级前端工程师设计,以及想要巩固前端基础知识的开发者文章中包含 90% 的面试考点和实际开发中需要掌握的知识,内容按点分类,环环相扣重要的是,形成了前端知识技能树,多数同学已经通过面试拿到 offer,并提升了自己的前端知识面。
    作者对重点考题做了详细解析和重点标注(建议在 PC 上阅读),并通过图解、思维导图的方式帮你降低了学习成本,节省备考时间,尽可能快地提升。可以说目前市面上没有像这样完善的面试备考指南!
  • ❤️ 现在订阅专栏,私聊博主,即可享受一次免费的模拟面试、简历修改、答疑服务拉你进前端答疑互助交流群,享受博主答疑服务和备考服务,优质文章分享。【私聊备注:前端修罗场】
  • 🚀 加入前端修罗场,从此快人一步,和一群人一起更进一步
  • 👉🏻 目前优惠中,即将恢复至原价 ~

1. React 事件机制

<div onClick=this.handleClick.bind(this)>点我</div>

React 并不是将 click 事件绑定到了 div 的真实 DOM 上,而是在 document 处监听了所有的事件,当事件发生并且冒泡到 document 处的时候,React 将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗,还能在组件挂在销毁时统一订阅和移除事件。

除此之外,冒泡到 document 上的事件也不是原生的浏览器事件,而是由react自己实现的合成事件(SyntheticEvent。因此如果不想要是事件冒泡的话应该调用 event.preventDefault() 方法,而不是调用 event.stopProppagation() 方法。

JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了 document。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。

另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault

实现合成事件的目的如下:

  • 合成事件首先抹平了浏览器之间的兼容问题,另外这是一个跨浏览器原生事件包装器,赋予了跨浏览器开发的能力;
  • 对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题。但是对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象。

2. React的事件和普通的html事件有什么不同?

区别:

  • 对于事件名称命名方式,原生事件为全小写react 事件采用小驼峰;
  • 对于事件函数处理语法,原生事件为字符串,react 事件为函数;
  • react 事件不能采用 return false 的方式来阻止浏览器的默认行为,而必须要地明确地调用preventDefault()来阻止默认行为。

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

  • 兼容所有浏览器,更好的跨平台;
  • 事件统一存放在一个数组,避免频繁的新增与删除(垃圾回收)。
  • 方便 react 统一管理和事务机制

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

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

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

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

  • 事件委派: React会把所有的事件绑定到结构的最外层,使用统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部事件监听和处理函数。
  • 自动绑定: React 组件中,每个方法的上下文都会指向该组件的实例,即自动绑定 this 为当前组件。

4. React 高阶组件、Render props、hooks 有什么区别,为什么要不断迭代?

这三者是目前 react 解决代码复用的主要方式:

  • 高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数。
  • render props 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术,更具体的说,render prop 是一个用于告知组件需要渲染什么内容的函数 prop。
  • 通常,render props 和高阶组件只渲染一个子节点。让 Hook 来服务这个使用场景更加简单。这两种模式仍有用武之地,(例如,一个虚拟滚动条组件或许会有一个 renderltem 属性,或是一个可见的容器组件或许会有它自己的 DOM 结构)。但在大部分场景下,Hook 足够了,并且能够帮助减少嵌套。

(1)HOC

官方解释∶

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

简言之,HOC 是一种组件的设计模式,HOC 接受一个组件和额外的参数(如果需要),返回一个新的组件。HOC 是纯函数,没有副作用。

// hoc的定义
function withSubscription(WrappedComponent, selectData) 
  return class extends React.Component 
    constructor(props) 
      super(props);
      this.state = 
        data: selectData(DataSource, props)
      ;
    
    // 一些通用的逻辑处理
    render() 
      // ... 并使用新数据渲染被包装的组件!
      return <WrappedComponent data=this.state.data ...this.props />;
    
  ;

// 使用
const BlogPostWithSubscription = withSubscription(BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id));

HOC的优缺点∶

  • 优点∶ 逻辑复用、不影响被包裹组件的内部逻辑。
  • 缺点∶ hoc 传递给被包裹组件的 props 容易和被包裹后的组件重名,进而被覆盖

(2)Render props

官方解释∶

“render prop” 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术

具有 render prop 的组件接受一个返回 React 元素的函数,将 render 的渲染逻辑注入到组件内部。在这里,“render” 的命名可以是任何其他有效的标识符。

// DataProvider组件内部的渲染逻辑如下
class DataProvider extends React.Components 
     state = 
    name: 'Tom'
  

    render() 
    return (
        <div>
          <p>共享数据组件自己内部的渲染逻辑</p>
           this.props.render(this.state) 
      </div>
    );
  


// 调用方式
<DataProvider render=data => (
  <h1>Hello data.name</h1>
)/>

由此可以看到,render props 的优缺点也很明显∶

  • 优点:数据共享、代码复用,将组件内的 state 作为 props 传递给调用者,将渲染逻辑交给调用者。
  • 缺点:无法在 return 语句外访问数据、嵌套写法不够优雅

(3)Hooks

官方解释∶

Hook是 React 16.8 的新增特性。 它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。通过自定义hook,可以复用代码逻辑。

// 自定义一个获取订阅数据的hook
function useSubscription() 
  const data = DataSource.getComments();
  return [data];

// 
function CommentList(props) 
  const data = props;
  const [subData] = useSubscription();
    ...

// 使用
<CommentList data='hello' />

以上可以看出,hook 解决了 hoc 的 prop 覆盖的问题,同时使用的方式解决了 render props 的嵌套地狱的问题。hook的优点如下∶

  • 使用直观;
  • 解决 hoc 的 prop 重名问题;
  • 解决 render props 因共享数据 而出现嵌套地狱的问题;
  • 能在 return 之外使用数据的问题。

需要注意的是:hook 只能在组件顶层使用,不可在分支 \\ 循环语句中使用。

总结∶

Hoc、render props 和 hook 都是为了解决代码复用的问题,但是 hoc 和 render props 都有特定的使用场景和明显的缺点。hook 是 react16.8 更新的新的 API,让组件逻辑复用更简洁明了,同时也解决了 hoc 和 render props 的一些缺点。

5. 对 React-Fiber 的理解,它解决了什么问题?

总结:
React-Fiber 的思想是基于协程的概念。首先协程式一种让出机制,它可以将当前正在执行的任务让出,让 CPU 处理其他任务。因此,React-Fiber 基于这个想法,为了在执行渲染时可以合理分配 CPU 资源,将对 DOM 的操作进行了分批延时处理。浏览器如果有高优先级的任务,可以优先处理,处理完再回来处理渲染任务。即可中断的概念。

中断机制 https://segmentfault.com/a/1190000039714631

React 16 开始,采用了 Fiber 机制替代了原有的同步渲染 VDOM 的方案,提高了页面渲染性能和用户体验。
在早期的
单任务系统
上,用户一次只能提交一个任务,当前运行的任务拥有全部硬件和软件资源,如果任务不主动释放 CPU 控制权,那么将一直占用所有资源,可能影响其他任务。

在没有中断的情况下,当 CPU 在执行一段代码时,如果程序不主动退出(如:一段无限循环代码),那么 CPU 将被一直占用,影响其他任务运行。

中断机制会强制中断当前 CPU 所执行的代码转而去执行先前注册好的中断服务程序。比较常见的如:时钟中断,它每隔一定时间将中断当前正在执行的任务,并立刻执行预先设置的中断服务程序,从而实现不同任务之间的交替执行,这也是在多任务系统的重要的基础机制。

浏览器中每一帧耗时大概在 16ms 左右,它会经过下面这几个过程:

  1. 输入事件处理
  2. requestAnimationFrame
  3. DOM 渲染
  4. RIC (RequestIdleCallback)

步骤 4 的 RIC,算是一种防止多余计算资源被浪费的机制,RIC 非常像前面提到的 “中断服务”,而浏览器的每一帧类似 “中断机制”。
React Fiber 是基于自定义一套机制来模拟实现,如:setTimeout、setImmediate、MessageChannel

  • 调度任务

中断服务后,不同任务就能实现间断执行的可能,如何实现多任务的合理调度,就需要一个调度任务来进行处理。在中断后,需要考虑现场保护和现场还原。

早期 React 是同步渲染机制,实际上是一个递归过程,递归可能会带来长的调用栈,这其实会给现场保护和还原变得复杂。
React Fiber 的做法
将递归过程拆分成一系列小任务(Fiber),转换成线性的链表结构
,此时现场保护只需要保存下一个任务结构信息即可,所以拆分的任务上需要扩展额外信息,该结构记录着任务执行时所需要的必备信息


    stateNode,//实例对象
    child, //子节点,指向自身下面的第一个fiber
    return,//父节点,指向上一个fiber
    sibling,//兄弟组件, 指向一个兄弟节点
    expirationTime
    ...

跟结构有关系的就三个属性:

  • return: 父节点,指向上一个 fiber
  • child : 子节点,指向自身下面的第一个 fiber
  • sibling : 兄弟组件, 指向一个兄弟节点

当 React 进行渲染时,会生成如下任务链,此时如果在执行任务 B 后时发现时间不足,主动释放后,只需要记录下一次任务 C 的信息,等再次调度时取得上次记录的信息即可。


React V15 在渲染时,会递归比对 VirtualDOM 树,找出需要变动的节点,然后同步更新它们, 一气呵成。这个过程期间, React 会占据浏览器资源,这会导致用户触发的事件得不到响应,并且会导致掉帧,导致用户感觉到卡顿

为了给用户制造一种应用很快的“假象”,不能让一个任务长期霸占着资源。 可以将浏览器的渲染、布局、绘制、资源加载(例如 HTML 解析)、事件响应、脚本执行视作操作系统的“进程”,需要通过某些调度策略合理地分配 CPU 资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。

所以从 React16开始, 通过 Fiber 架构,让这个执行过程变成可被中断。“适时”地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:

  • 分批延时对DOM进行操作,避免一次性操作大量 DOM 节点,可以得到更好的用户体验;
  • 给浏览器一点喘息的机会, 它会对代码进行编译优化(JIT)及进行热代码优化,或者对 reflow 进行修正。

核心思想Fiber 也称协程(类型 generator 或者纤程。它和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程),它只是一种控制流程的让出机制。让出 CPU 的执行权,让 CPU 能在这段时间执行其他的操作。渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。

6. React.Component 和 React.PureComponent 的区别

PureComponent 表示一个纯组件,可以用来优化 React 程序,减少 render 函数执行的次数,从而提高组件的性能。

在React中,当 prop 或者 state 发生变化时,可以通过在 shouldComponentUpdate 生命周期函数中执行return false 来阻止页面的更新,从而减少不必要的 rende r执行。React.PureComponent 会自动执行 shouldComponentUpdate

不过,pureComponent 中 的 shouldComponentUpdate() 进行的是浅比较,也就是说如果是引用数据类型的数据,只会比较不是同一个地址,而不会比较这个地址里面的数据是否一致。浅比较会忽略属性和或状态突变情况,其实也就是数据引用指针没有变化,而数据发生改变的时候 render 是不会执行的。 如果需要重新渲染那么就需要重新开辟空间引用数据。PureComponent一般会用在一些纯展示组件上。

使用 pureComponent 的好处:当组件更新时,如果组件的 props 或者 state 都没有改变,render函数就不会触发。省去虚拟 DOM 的生成和对比过程,达到提升性能的目的。这是因为 react 自动做了一层浅比较。

7. Component, Element, Instance 之间有什么区别和联系?

  • 元素:一个元素 element是一个普通对象(plain object),描述了对于一个 DOM 节点或者其他组件component你想让它在屏幕上呈现成什么样子。元素 element 可以在它的属性 props 中包含其他元素(译注:用于形成元素树)。创建一个 React 元素 element 成本很低。元素 element 创建之后是不可变的。
  • 组件: 一个组件 component 可以通过多种方式声明。**可以是带有一个 render() 方法的类,简单点也可以定义为一个函数。**这两种情况下,它都把属性 props 作为输入,把返回的一棵元素树作为输出。
  • 实例:一个实例 instance 是你在所写的组件类 component class 中使用关键字 this 所指向的东西(组件实例)。它用来存储本地状态和响应生命周期事件很有用。

函数式组件(Functional component)根本没有实例instance。类组件(Class component)有实例instance,但是永远也不需要直接创建一个组件的实例,因为React帮我们做了这些。

8. React.createClass 和 extends Component 的区别有哪些?

React.createClass 和 extends Component 的区别主要在于:

(1)语法区别

  • createClass 本质上是一个工厂函数extends 的方式更加接近最新的ES6 规范的 class 写法。两种方式在语法上的差别主要体现在方法的定义和静态属性的声明上。
  • createClass 方式的方法定义使用逗号,隔开,因为creatClass 本质上是一个函数,传递给它的是一个 Object;而 class 的方式定义方法时务必谨记不要使用逗号隔开,这是 ES6 class 的语法规范。

(2)propType 和 getDefaultProps

  • React.createClass:通过 propTypes 对象和 getDefaultProps() 方法来设置和获取 props.
  • React.Component:通过设置两个属性 propTypes 和 defaultProps

(3)状态的区别

  • React.createClass:通过 getInitialState() 方法返回一个包含初始值的对象
  • React.Component:通过 constructor 设置初始状态

(4)this区别

  • React.createClass:会正确绑定 this
  • React.Component:由于使用了 ES6,这里会有些微不同,属性并不会自动绑定到 React 类的实例上。

(5)Mixins

  • React.createClass:使用 React.createClass 的话,可以在创建组件时添加一个叫做 mixins 的属性,并将可供混合的类的集合以数组的形式赋给 mixins。
  • 如果使用 ES6 的方式来创建组件,那么 React mixins 的特性将不能被使用了。

9. React 高阶组件是什么,和普通组件有什么区别,适用什么场景

官方解释∶

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

高阶组件(HOC)就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件,它只是一种组件的设计模式,这种设计模式是由 react 自身的组合性质必然产生的。我们将它们称为纯组件,因为它们可以接受任何动态提供的子组件,但它们不会修改或复制其输入组件中的任何行为。

// hoc的定义
function withSubscription(WrappedComponent, selectData) 
  return class extends React.Component 
    constructor(props) 
      super(props);
      this.state = 
        data: selectData(DataSource, props)
      ;
    
    // 一些通用的逻辑处理
    render() 
      // ... 并使用新数据渲染被包装的组件!
      return <WrappedComponent data=this.state.data ...this.props />;
    
  ;

// 使用
const BlogPostWithSubscription = withSubscription(BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id));

1)HOC的优缺点

  • 优点∶ 逻辑服用、不影响被包裹组件的内部逻辑。
  • 缺点∶ hoc传递给被包裹组件的props容易和被包裹后的组件重名,进而被覆盖

2)适用场景

  • 代码复用,逻辑抽象
  • 渲染劫持
  • State 抽象和更改
  • Props 更改

3)具体应用例子

  • 权限控制:利用高阶组件的 条件渲染 特性可以对页面进行权限控制,权限控制一般分为两个维度:页面级别和 页面元素级别
// HOC.js
function withAdminAuth(WrappedComponent) 
    return class extends React.Component 
        state = 
            isAdmin: false,
        
        async UNSAFE_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...
    
    UNSAFE_componentWillMount() 
        // fetching data
    
    render() 
        // render page with data
    

export default withAdminAuth(PageA);


// pages/page-b.js
class PageB extends React.Component 
    constructor(props) 
        super(props);
    // something here...
        
    UNSAFE_componentWillMount() 
    // fetching data
    
    render() 
    // render page with data
    

export default withAdminAuth(PageB);
  • 组件渲染性能追踪:借助父组件子组件生命周期规则捕获子组件的生命周期,可以方便的对某个组件的渲染时间进行记录∶
class Home extends React.Component 
        render() 
            return (<h1>Hello World.</h1>);
        
    
    function withTiming(WrappedComponent) 
        return class extends WrappedComponent 
            constructor(props) 
                super(props);
                this.start = 0;
                this.end = 0;
            
            UNSAFE_componentWillMount() 
                super.componentWillMount && super.componentWillMount();
                this.start = Date.now();
            
            componentDidMount() 
                super.componentDidMount && super.componentDidMount();
                this.end = Date.now();
                console.log(`$WrappedComponent.name 组件渲染时间为 $this.end - this.start ms`);
            
            render() 
                return super.render();
            
        ;
    

    export default withTiming(Home);

注意:withTiming 是利用 反向继承 实现的一个高阶组件,功能是计算被包裹组件(这里是 Home 组件)的渲染时间。

  • 页面复用
const withFetching = fetching => WrappedComponent => 
    return class extends React.Component 
        state = 
            data: [],
        
        async UNSAFE_componentWillMount() 
            const data = await fetching();
            this.setState(
                data,
            );
        
        render() 
            return <WrappedComponent data=this.state.data ...this.props />;
        
    


// pages/page-a.js
export default withFetching(fetching('science-fiction'))(MovieList);
// pages/page-b.js
export default withFetching(fetching('action'))(MovieList);
// pages/page-other.js
export default withFetching(fetching('some-other-type'))(MovieList);

10. 对componentWillReceiveProps 的理解

该方法当 props 发生变化时执行,初始化 render 时不执行,在这个回调函数里面,你可以根据属性的变化,通过调用 this.setState() 来更新你的组件状态,旧的属性还是可以通过 this.props 来获取,这里调用更新状态是安全的,并不会触发额外的 render 调用。

使用好处:在这个生命周期中,可以在子组件的 render 函数执行前获取新的 props,从而更新子组件自己的 state。 可以将数据请求放在这里进行执行,需要传的参数则从componentWillReceiveProps(nextProps) 中获取。而不必将所有的请求都放在父组件中。于是该请求只会在该组件渲染时才会发出,从而减轻请求负担。componentWillReceiveProps 在初始化 render 的时候不会执行,它会在 Component 接受到新的状态(Props)时被触发,一般用于父组件状态更新时子组件的重新渲染

11. 哪些方法会触发 React 重新渲染?重新渲染 render 会做些什么?

(1)哪些方法会触发 react 重新渲染?

  • setState()方法被调用

setState 是 React 中最常用的命令,通常情况下,执行 setState 会触发 render。但是这里有个点值得关注,执行 setState 的时候不一定会重新渲染。当 setState 传入 null 时,并不会触发 render。

class App extends React.Component 
  state = 
    a: 1
  ;

  render() 
    console.log("render");
    return (
      <React.Fragement>
        <p>this.state.a</p>
        <button
          onClick=() => 
            this.setState( a: 1 ); // 这里并没有改变 a 的值
          
        >
          Click me
        </button>
        <button onClick=() => this.setState(null)>setState null</button>
        <Child />
      </React.Fragement>
    );
  

  • 父组件重新渲染

只要父组件重新渲染了,即使传入子组件的 props 未发生变化

以上是关于前端面试,备考第 19 天—— React 基础一点通的主要内容,如果未能解决你的问题,请参考以下文章

前端面试,备考第 21 天—— React 怎么做数据管理,原理是什么

前端面试,备考第 12 天 - 原型与原型链

前端面试,备考第 13 天 - 执行上下文 | 作用域链 | 闭包

前端面试,备考第 15 天 - 异步编程:Promise | Async/Await | 定时器 | 扩展

前端面试,备考第 14 天 - 指针指向问题:this/call/apply/bind

前端面试,备考第 17 天—— 怎么对项目做性能优化:图片优化 | CDN | 懒加载 | 回流重绘 | 动画