React 知识点(面试题)

目录

1.什么是JSX?

2.讲一下虚拟Dom?

3.类组件和函数组件之间的区别是什么?

4.hooks出现的意义?

5.了解React Fiber吗?

6.requestIdleCallback了解多少?

7.setState 同步还是异步?(比较常问)

8.React-router路由模式?

9.React的生命周期?

10.你知道那些hook?

11.你对Time Silce(时间分片)的理解?

12.redux的工作流程?

13.生命周期

14.setState

15.性能优化

16.通信

父子通信

兄弟组件通信

跨多层次组件通信

任意组件

17.HOC 是什么?相比 mixins 有什么优点?

18.事件机制



1.什么是JSX?

JSX是一种Javascript的语法扩展,可以很好的描述UI的架构。是React.createElement的语法糖。浏览器不能直接解析JSX文件,需要通过Bable进行转译成js。

2.讲一下虚拟Dom?

虚拟Dom:虚拟Dom是描述真实Dom的js对象。
特点:

(1)处理了浏览器兼容性问题,避免用户操作真实DOM,不容易出错。

(2)内容经过了XSS处理,可以防范XSS攻击。

(3)可以实现跨平台开发。

(4)在更新的时候,比较两棵虚拟DOM树的差异,差异化更新。

延伸题:什么是diff算法?

diff算法,就是用来找出两段文本之间的差异的一种算法。

vdom为什么用diff算法?

由于DOM操作是非常昂贵的,就可以通过diff算法来减少DOM操作。

vdom比真实dom快?

回答:不一定。

在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不同的场合。

无效、无意义的diff是需要浪费性能的,因此有些场景不如直接操作原生DOM性能好。

3.类组件和函数组件之间的区别是什么?

相同点:它们都可以接收属性并且返回React元素。

不同点:

(1)类组件需要创建实例,是基于面向对象的方式编程,函数组件不需要创建实例,接收输入,返回输出,是基于函数编程的思想。

(2)类组件需要创建并且保持实例,会占用一定的内存,函数组件不需要创建实例,可以节约内存占用。

(3)类组件有完整的生命周期,函数组件没有生命周期(现在通过useEffect实现类似生命周期的功能)

(4)类组件通过shouldComponent和pureComponent跳过更新,而函数组件可以通过React.memo跳过更新。

(5)类组件服用逻辑一般用HOC,函数组件可以自定义Hook。

延伸题:
源码如何区分函数组件与类组件?
通过Component.prototype.isReactComponent属性来判断。

4.hooks出现的意义?

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
Hooks 优势:

(1)函数组件无 this 问题

(2)自定义 Hook 方便复用状态逻辑

(3)副作用的关注点分离

5.了解React Fiber吗?

React Fiber初衷是改变js在浏览器的主线程上长时间执行,会阻塞其他操作,影响用户体验。

Fiber的关键特性:

(1)增量渲染(把渲染任务拆分成块,匀到多帧)

(2)更新时能够暂停,终止,复用渲染任务

(3)给不同类型的更新赋予优先级

(4)并发方面新的基础能力

协调器reconciler :
Fiber前的 Reconciler 被命名为Stack Reconciler 运作的过程无法中断(持续占用主线程),这样主线程上的布局、动画等周期性任务以及交互响应就无法立即得到处理,影响体验。

而新的Reconciler 命名为Fiber Reconciler 每执行一段时间,都会将控制权交回给浏览器,可以分段执行。

Fiber Reconciler 在执行过程中,会分为 2 个阶段 :

(1)render阶段,生成 Fiber 树,得出需要更新的节点信息。这一步是一个渐进的过程,可以被打断。

(2)commit阶段,将需要更新的节点一次过批量更新,这个过程不能被打断。

6.requestIdleCallback了解多少?

requestIdleCallback方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。

具体用法如下:

window.requestIdleCallback(callback[, options])
// 示例
let handle = window.requestIdleCallback((idleDeadline) => {
    const {didTimeout, timeRemaining} = idleDeadline;
    console.log(`超时了吗?${didTimeout}`);
    console.log(`可用时间剩余${timeRemaining.call(idleDeadline)}ms`);
    // do some stuff
    const now = +new Date, timespent = 10;
    while (+new Date < now + timespent);
    console.log(`花了${timespent}ms搞事情`);
    console.log(`可用时间剩余${timeRemaining.call(idleDeadline)}ms`);
}, {timeout: 1000});

7.setState 同步还是异步?(比较常问)

(1)React生命周期中以及事件处理中,为异步。

(2)原生方法(setTimeout,setInnerval,addEventListener )中是同步 。

原理:setState本身并不是异步,只是因为react的性能优化机制体现为异步。在react的生命周期函数或者作用域下为异步,在原生的环境下为同步。 因为每次调用setState都会触发更新,异步操作是为了提高性能,将多个状态合并一起更新,减少re-render调用。

性能优化机制:在 React 的 setState 函数实现中,会根据一个变量isBatchingUpdates判断是直接更新 this.state 还是放到队列中。isBatchingUpdates 默认是 false,React 在调用事件处理函数之前会将isBatchingUpdates改为true,造成的后果就是由 React 控制的事件处理过程 setState 不会同步更新 this.state。而原生方法不会被React控制。

8.React-router路由模式?

hash模式(HashRouter):通过监听 hashchange 事件, 在回调里拿到 window.location.hash 的值。 hash 就是指 url 尾巴后的 # 号以及后面的字符。

hash模式原理:

使用window.location.hash属性及窗口的onhashchange事件,可以实现监听浏览器地址hash值变化,执行相应的js切换网页。 hash指的是地址中#号以及后面的字符,也称为散列值。

history模式(BrowserRouter): 利用history API实现url地址改变,网页内容改变。

history模式原理:

window.history 属性指向 History 对象,它表示当前窗口的浏览历史。 History 对象保存了当前窗口访问过的所有页面网址。

9.React的生命周期?

目前React 16.8+的生命周期分为三个阶段,分别是挂载阶段、更新阶段、卸载阶段。

react16之后又三个生命周期被废除(但并未删除),保留了前缀UNSAVE_的三个函数,目的是为了向下兼容。

componentWillMount
componentWillReceiveProps
componentWillUpdate

延伸问题:为什么要删除这三个函数?
答:废弃的三个函数都是在render之前,因为fiber的出现,很可能因为高优先级任务的出现而打断现有任务导致它们会被执行多次。

挂载阶段:

constructor:构造函数,最新被执行,我们通常在构造函数里初始化state对象或者给自定义方法绑定this。
getDerivedStateFromProps:static getDerivedStateFromProps(nextProps,prevState),这个是静态方法,当我们接收到新的属性想去修改我们state,可以使用getDerivedStateFromProps(新增生命周期)
render:render函数是纯函数,只返回需要渲染的东西,不应该包含其它业务逻辑,可以返回原生的DOM、React组件、Fragment、Portals、字符串和数字、Boolean和null等内容
componentDidMount:组件装载之后调用,此时我们可以获取到DOM节点并操作,比如对canvas,svg的操作,服务器请求,订阅都可以写在这个里面,但是记得在componentWillUnmount中取消订阅
更新阶段:

getDerivedStateFromProps:此方法在更新挂载阶段都可能会调用
shouldComponentUpdate:shouldComponentUpdate(nextProps,nextState),有两个参数nectProps和nectState,表示新的属性和变化之后的state,返回一个布尔值,true表示会触发重新渲染,false表示不会触发重新渲染,默认返回true,我们通常利用此声明周期来优化React程序性能
render:更新阶段也会触发此生命周期
getSnapShotBeforeUpdate:getSnapshotBeforeUpdate(prevProps,prevState),这个在render方法之后,componentDidUpdate之前调用,有两个参数prevProps,prevState,表示之前的属性和之前的state,这个函数有一个返回值,会作为第三个参数传给componentDidUpdate,如果你不想要返回值,可以返回null,此生命周期必须与componentDidUpdate搭配使用(新增生命周期)
componentDidUpdate:componentDidUpdate(prevProps,prevState,snapshot),该方法在getSnapshotBeforeUpdate方法之后被调用,有三个参数prevPropsprevState,snapshot,表示之前的props,之前的state,和snapshot。第三个参数是getSnapshotBeforeUpdate返回的,如果触发某些回调函数时需要用到DOM元素的状态,则将对比或计算过程迁移至getSnapshotBeforeUpdate,然后再componentDidUpdate中统一触发回调或更新状态。

卸载阶段:

  • componentWillUnmount:当我们的组件被卸载或者销毁了就回调用,我们可以在这个函数里去清楚一些定时器,取消网络请求,清理无效的DOM元素等垃圾清理工作

10.你知道那些hook?

useState()`,状态管理钩子。通过在函数组件中调用useState,就会创建一个单独的状态。

useEffect(),副作用钩子。它接收两个参数, 第一个是进行的异步操作, 第二个是数组,用来给出Effect的依赖项

useContext(),共享钩子。该钩子的作用是,在组件之间共享状态。

useReducer(),Action 钩子。useReducer() 提供了状态管理,其基本原理是通过用户在页面中发起action,
从而通过reducer方法来改变state, 从而实现页面和状态的通信。

useRef(),获取组件的实例;渲染周期之间共享数据的存储(state不能存储跨渲染周期的数据,因为state的保存会触发组件重渲染)

useMemo和useCallback:可缓存函数的引用或值,useMemo用在计算值的缓存,注意不用滥用。经常用在下面两种场景(要保持引用相等;对于组件内部用到的
object、array、函数等,如果用在了其他 Hook 的依赖数组中,或者作为 props 传递给了下游组件,应该使用
useMemo/useCallback)

11.你对Time Silce(时间分片)的理解?

React在渲染(render)的时候,不会阻塞现在的线程
如果你的设备足够快,你会感觉渲染是同步的
如果你得设备非常慢,你会感觉还算是灵敏的
虽然异步渲染,但是你将会看到完整的渲染,而不是一个组件一行行的渲染出来
同样书写组件的方式
也就是说,这事React背后在做的事情,对于我们开发者来说,是透明的,具体是什么样的效果呢?

例如,由三个图表,有一个输入框以及上面的三种模式

这个组件非常的巨大,而且在输入框每次输入东西的时候,就回进去一直在渲染。

同步模式:

在同步模式下,我们都知道,我们没输入一个字符,React就开始渲染,当React渲染一颗巨大的树的时候,是非常卡的。所以才会有shouldUpdate的出现。

Debounced模式:

Debounced模式简单的来说,就是延迟渲染,比如,当你输入完以后,在开始渲染所有的变化。这么做的坏处就是,至少不会阻塞用户的输入了,但是依然由非常严重的卡顿。

异步模式:

一部渲染模式就是不阻塞当前线程,继续跑。时间分片正是基于可随时打断、重启Fiber架构,可打断当前任务,优先处理紧急且重要的任务,保证页面的流程运行。

12.redux的工作流程?

核心概念:

Store:保存数据的地方,你可以把它看成一个容器,整个应用只能由一个Store>
State:Store对象包含所有数据,如果想得到某个时点的数据,就要对Store生成快照,这种时点的数据集合,就叫做State。
Action:State的变化,会导致View的变化。但是,用户接触不到State,只能接触到view。所以,State的变化必须由View导致的。Action就是View发出的通知,表示State应该要发生变化了。
Action Creator:View要发送多少种消息,就会有多少种Action。如果都手写,会很麻烦,所以我们定义一个函数来生成Action,这个函数就叫Action Creator。
Reducer:Store收到Action以后,必须给出一个新的State,这样View才会发生变化。这种State的计算过程就叫做Reducer。Reducer是一个函数,他接受Action和当前State作为参数,返回一个新的State。
dispatch:是Vie发出Action的唯一方法。
工作流程:

(1)首先,用户(通过View)发出Action,发出方式就用到了dispatch方法。

(2)然后,Store自动调用Reducer,并且传入两个参数:当前State和收到的Action,Reducer会返回新的State。

(3)State一旦有变化,Store就回调用监听函数,来更新View。

13.生命周期

在 V16 版本中引入了 Fiber 机制。这个机制一定程度上的影响了部分生命周期的调用,并且也引入了新的 2 个 API 来解决问题,关于 Fiber 的内容将会在下一章节中讲到。

在之前的版本中,如果你拥有一个很复杂的复合组件,然后改动了最上层组件的 state,那么调用栈可能会很长

调用栈过长,再加上中间进行了复杂的操作,就可能导致长时间阻塞主线程,带来不好的用户体验。Fiber 就是为了解决该问题而生。

Fiber 本质上是一个虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧,从而将之前的同步渲染改成了异步渲染,在不影响体验的情况下去分段计算更新。

对于如何区别优先级,React 有自己的一套逻辑。对于动画这种实时性很高的东西,也就是 16 ms 必须渲染一次保证不卡顿的情况下,React 会每 16 ms(以内) 暂停一下更新,返回来继续渲染动画。

对于异步渲染,现在渲染有两个阶段:reconciliation 和 commit 。前者过程是可以打断的,后者不能暂停,会一直更新界面直到完成。

Reconciliation 阶段

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

Commit 阶段

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

因为 Reconciliation 阶段是可以被打断的,所以 Reconciliation 阶段会执行的生命周期函数就可能会出现调用多次的情况,从而引起 Bug。由此对于 Reconciliation 阶段调用的几个函数,除了 shouldComponentUpdate 以外,其他都应该避免去使用,并且 V16 中也引入了新的 API 来解决这个问题。

getDerivedStateFromProps 用于替换 componentWillReceiveProps ,该函数会在初始化和 update 时被调用

class ExampleComponent extends React.Component {
  // Initialize state in constructor,
  // Or with a property initializer.
  state = {};

  static getDerivedStateFromProps(nextProps, prevState) {
    if (prevState.someMirroredValue !== nextProps.someValue) {
      return {
        derivedData: computeDerivedState(nextProps),
        someMirroredValue: nextProps.someValue
      };
    }

    // Return null to indicate no change to state.
    return null;
  }
}

getSnapshotBeforeUpdate 用于替换 componentWillUpdate ,该函数会在 update 后 DOM 更新前被调用,用于读取最新的 DOM 数据。

14.setState

setState 在 React 中是经常使用的一个 API,但是它存在一些的问题经常会导致初学者出错,核心原因就是因为这个 API 是异步的。

首先 setState 的调用并不会马上引起 state 的改变,并且如果你一次调用了多个 setState ,那么结果可能并不如你期待的一样。

handle() {
  // 初始化 `count` 为 0
  console.log(this.state.count) // -> 0
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log(this.state.count) // -> 0
}

第一,两次的打印都为 0,因为 setState 是个异步 API,只有同步代码运行完毕才会执行。setState 异步的原因我认为在于,setState 可能会导致 DOM 的重绘,如果调用一次就马上去进行重绘,那么调用多次就会造成不必要的性能损失。设计成异步的话,就可以将多次调用放入一个队列中,在恰当的时候统一进行更新过程。

第二,虽然调用了三次 setState ,但是 count 的值还是为 1。因为多次调用会合并为一次,只有当更新结束后 state 才会改变,三次调用等同于如下代码

Object.assign(  
  {},
  { count: this.state.count + 1 },
  { count: this.state.count + 1 },
  { count: this.state.count + 1 },
)w

当然你也可以通过以下方式来实现调用三次 setState 使得 count 为 3

handle() {
  this.setState((prevState) => ({ count: prevState.count + 1 }))
  this.setState((prevState) => ({ count: prevState.count + 1 }))
  this.setState((prevState) => ({ count: prevState.count + 1 }))
}

如果你想在每次调用 setState 后获得正确的 state ,可以通过如下代码实现

handle() {
    this.setState((prevState) => ({ count: prevState.count + 1 }), () => {
        console.log(this.state)
    })
}

15.性能优化

这小节内容集中在组件的性能优化上,这一方面的性能优化也基本集中在 shouldComponentUpdate 这个钩子函数上做文章。

PS:下文中的 state 指代了 state 及 props

在 shouldComponentUpdate 函数中我们可以通过返回布尔值来决定当前组件是否需要更新。这层代码逻辑可以是简单地浅比较一下当前 state 和之前的 state 是否相同,也可以是判断某个值更新了才触发组件更新。一般来说不推荐完整地对比当前 state 和之前的 state 是否相同,因为组件更新触发可能会很频繁,这样的完整对比性能开销会有点大,可能会造成得不偿失的情况。

当然如果真的想完整对比当前 state 和之前的 state 是否相同,并且不影响性能也是行得通的,可以通过 immutable 或者 immer 这些库来生成不可变对象。这类库对于操作大规模的数据来说会提升不错的性能,并且一旦改变数据就会生成一个新的对象,对比前后 state 是否一致也就方便多了,同时也很推荐阅读下 immer 的源码实现。

另外如果只是单纯的浅比较一下,可以直接使用 PureComponent,底层就是实现了浅比较 state

class Test extends React.PureComponent {
  render() {
    return (
      <div>
        PureComponent
      </div>
    )
  }
}

这时候你可能会考虑到函数组件就不能使用这种方式了,如果你使用 16.6.0 之后的版本的话,可以使用 React.memo 来实现相同的功能。

const Test = React.memo(() => (
    <div>
        PureComponent
    </div>
))

通过这种方式我们就可以既实现了 shouldComponentUpdate 的浅比较,又能够使用函数组件。

16.通信

其实 React 中的组件通信基本和 Vue 中的一致。同样也分为以下三种情况:

  • 父子组件通信
  • 兄弟组件通信
  • 跨多层级组件通信
  • 任意组件

父子通信

父组件通过 props 传递数据给子组件,子组件通过调用父组件传来的函数传递数据给父组件,这两种方式是最常用的父子通信实现办法。

这种父子通信方式也就是典型的单向数据流,父组件通过 props 传递数据,子组件不能直接修改 props, 而是必须通过调用父组件函数的方式告知父组件修改数据。

兄弟组件通信

对于这种情况可以通过共同的父组件来管理状态和事件函数。比如说其中一个兄弟组件调用父组件传递过来的事件函数修改父组件中的状态,然后父组件将状态传递给另一个兄弟组件。

跨多层次组件通信

如果你使用 16.3 以上版本的话,对于这种情况可以使用 Context API。

// 创建 Context,可以在开始就传入值
const StateContext = React.createContext()
class Parent extends React.Component {
  render () {
    return (
      // value 就是传入 Context 中的值
      <StateContext.Provider value='yck'>
        <Child />
      </StateContext.Provider>
    )
  }
}
class Child extends React.Component {
  render () {
    return (
      <ThemeContext.Consumer>
        // 取出值
        {context => (
          name is { context }
        )}
      </ThemeContext.Consumer>
    );
  }
}

任意组件

这种方式可以通过 Redux 或者 Event Bus 解决,另外如果你不怕麻烦的话,可以使用这种方式解决上述所有的通信情况

17.HOC 是什么?相比 mixins 有什么优点?

很多人看到高阶组件(HOC)这个概念就被吓到了,认为这东西很难,其实这东西概念真的很简单,我们先来看一个例子。

function add(a, b) {
    return a + b
}

现在如果我想给这个 add 函数添加一个输出结果的功能,那么你可能会考虑我直接使用 console.log 不就实现了么。说的没错,但是如果我们想做的更加优雅并且容易复用和扩展,我们可以这样去做:

function withLog (fn) {
    function wrapper(a, b) {
        const result = fn(a, b)
        console.log(result)
        return result
    }
    return wrapper
}
const withLogAdd = withLog(add)
withLogAdd(1, 2)

其实这个做法在函数式编程里称之为高阶函数,大家都知道 React 的思想中是存在函数式编程的,高阶组件和高阶函数就是同一个东西。我们实现一个函数,传入一个组件,然后在函数内部再实现一个函数去扩展传入的组件,最后返回一个新的组件,这就是高阶组件的概念,作用就是为了更好的复用代码。

其实 HOC 和 Vue 中的 mixins 作用是一致的,并且在早期 React 也是使用 mixins 的方式。但是在使用 class 的方式创建组件以后,mixins 的方式就不能使用了,并且其实 mixins 也是存在一些问题的,比如:

  • 隐含了一些依赖,比如我在组件中写了某个 state 并且在 mixin 中使用了,就这存在了一个依赖关系。万一下次别人要移除它,就得去 mixin 中查找依赖
  • 多个 mixin 中可能存在相同命名的函数,同时代码组件中也不能出现相同命名的函数,否则就是重写了,其实我一直觉得命名真的是一件麻烦事。。
  • 雪球效应,虽然我一个组件还是使用着同一个 mixin,但是一个 mixin 会被多个组件使用,可能会存在需求使得 mixin 修改原本的函数或者新增更多的函数,这样可能就会产生一个维护成本

HOC 解决了这些问题,并且它们达成的效果也是一致的,同时也更加的政治正确(毕竟更加函数式了)。

18.事件机制

React 其实自己实现了一套事件机制,首先我们考虑一下以下代码:

const Test = ({ list, handleClick }) => ({
    list.map((item, index) => (
        <span onClick={handleClick} key={index}>{index}</span>
    ))
})

以上类似代码想必大家经常会写到,但是你是否考虑过点击事件是否绑定在了每一个标签上?事实当然不是,JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了 document 上。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。

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

那么实现合成事件的目的是什么呢?总的来说在我看来好处有两点,分别是:

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

引用文章:react高频面试题(react篇) 

猜你喜欢

转载自blog.csdn.net/weixin_57844432/article/details/128867015