React面试题

337 阅读12分钟

一、React hooks 不能在哪些地方使用?

它可以让我们不编写类组建的情况下 使用 状态和生命周期等功能

1 hooks规则的限制

他们必须在函数组件的最顶层使用,不能放在条件语句中、循环或嵌套函数中,

  • 这是因为React依赖于Hooks的顺序调用
  • 单链表的每个hook节点没有名字或者key,因为除了它们的顺序,我们无法记录它们的唯一性。
  • 因此为了确保某个Hook是它本身,我们不能破坏这个链表的稳定性。

二、React18 那些新特性

1. concurrent 新特性实现的基础

1.1 是什么

Concurrent React(并发模式的React) 是React框架的一项功能,旨在提高应用程序的性能和用户体验。它是自React 16开始引入的一组特性,通过使用协调器(coordinator)和调度器(scheduler)来实现

1.2 为什么要用concurrent

  1. 传统的React渲染是基于递归的,意味着在处理组件更新时,React会一直执行下去,直到完成整个组件树的渲染。这种方式在大型组件树或复杂的更新情况下可能会导致阻塞主线程,影响应用程序的响应性和性能。ReactDom.render('root' as element)
  2. Concurrent React引入了一种新的渲染模式,即可中断的渲染。它允许React在渲染过程中执行中断和恢复操作,使得浏览器能够在渲染期间执行其他高优先级的任务,例如用户交互或动画。 createRoot('root' as element)

1.3. concurrent 的特点

 1. 异步渲染: Concurrent React能够将渲染工作分解为多个优先级较低的任务,并根据任务的优先级以适当的方式调度它们。这样可以避免长时间的阻塞,提高应用程序的响应性。

2. 优先级调度: Concurrent React引入了任务调度器(scheduler),它根据任务的优先级来决定何时执行任务。通过定义不同任务的优先级,可以确保高优先级任务优先执行,从而更好地响应用户操作。

3. 中断和恢复: 在渲染过程中,Concurrent React允许React在执行任务时中断并恢复。这使得浏览器能够在必要时执行其他任务,提高了应用程序的流畅性和性能。

 4. 延迟加载: Concurrent React还提供了延迟加载组件的能力。可以将某些组件标记为“懒加载”,只有在需要时才会开始加载和渲染,从而减少初始加载时间和资源消耗。

2. setState

2.1 改变的地方

在React18以前,可以把setState放在promises、setTimeout或者原生事件中实现同步。

但是以前的React的批量更新是依赖于合成事件的,到了React18之后,state的批量更新不再与合成事件有直接关系,而是自动批量处理。

2.1.1 自动批量处理 Automatic Batching / setState

// 以前: 这里的两次setState并没有批量处理,React会render两次
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
}, 1000);
​
// React18: 自动批量处理,这里只会render一次
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
}, 1000);

2.1.2 但是如果你有一些其它理由或者需要应急,想要同步setState,这个时候可以使用flushSync

// import { flushSync } from "react-dom";
​
changeCount = () => {
  const { count } = this.state;
  
  flushSync(() => {
    this.setState({
      count: count + 1,
    });
  });
  
  console.log("改变count", this.state.count); //sy-log
};
​
// <button onClick={this.changeCount}>change count 合成事件</button>

3. Suspense

子元素在异步取数时会阻塞父组件渲染,并一直冒泡到最外层第一个 Suspense,此时 Suspense 不会渲染子组件,而是渲染 fallback,当所有子组件异步阻塞取消后才会正常渲染。

<Suspense fallback={<Spinner />}>
  <Comments />
</ Suspense>
​
​
// 子元素在异步取数时会阻塞父组件渲染,并一直冒泡到最外层第一个 Suspense,此时 Suspense 不会渲染子组件,而是渲染 fallback,当所有子组件异步阻塞取消后才会正常渲染。
class Suspense extends React.Component {
  state = {
    promise: null
  }
​
  componentDidCatch(e) {
    if (e instanceof Promise) {
      this.setState({
        promise: e
      }, () => {
        e.then(() => {
          this.setState({
            promise: null
          })
        })
      })
    }
  }
​
  render() {
    const { fallback, children } = this.props
    const { promise } = this.state
    return <>
      { promise ? fallback : children }
    </>
  }
}
​

4. transition

  • Urgent updates 紧急更新,指直接交互,通常指的用户交互。如点击、输入等。这种更新一旦不及时,用户就会觉得哪里不对。
  • Transition updates 过渡更新,如UI从一个视图向另一个视图的更新。通常这种更新用户并不着急看到。

1.1 startTransition

startTransition中触发的更新会让更高优先级(如外面的click)的更新先进行

startTransition中的延迟更新,不会触发Suspens组件的fallback,便于用户在更新期间的交互

// 被startTransiton标记后为过渡更新
startTransition(()=> {
    // 非紧急更新,会被降低优先级,延迟执行
    setQueryValue(inputValue)
})
​
// 未被标记则马上执行
setInputValue(inputValue)
​
​
​
const  [ isPending , startTransition ] = useTransition()
  isPending:过度任务状态,true代表过渡中,false过渡结束
  startTransition:执行的过渡任务
    startTransition(()=>{
      console.log(过度任务)
    })
​

例子

export default function App(){
    const [ value ,setInputValue ] = React.useState('')
    const [ query ,setSearchQuery  ] = React.useState('')
    const [ isPending , startTransition ] = React.useTransition()
    const handleChange = (e) => {
        setInputValue(e.target.value)
        startTransition(()=>{
            setSearchQuery(e.target.value)
        })
    }
    return  <div>
    {isPending && <span>isTransiton</span>}
    <input onChange={handleChange}
        placeholder="输入搜索内容"
        value={value}
    />
   <NewList  query={query} />
</div>
}

5. useDeferredValue

本质上和内部实现与 useTransition 一样都是把任务标记成了过渡更新任务。

只是接受的是值

import { useState, useMemo, useDeferredValue } from "react";
​
const numbers = [...new Array(200000).keys()];
​
export default function App() {
    const [query, setQuery] = useState("");
​
    const handleChange = (e) => {
        setQuery(e.target.value);
    };
​
    return (
        <div>
            <input type="number" onChange={handleChange} value={query} />
            <List query={query} />
        </div>
    );
}
​
function List(props) {
    const { query } = props;
    const defQuery = useDeferredValue(query);
​
    const list = useMemo(() => (
        numbers.map((i, index) => (
            defQuery
                ? i.toString().startsWith(defQuery)
                && <p key={index}>{i}</p>
                : <p key={index}>{i}</p>
        ))
    ), [defQuery]);
​
    return (
        <div>
            {list}
        </div>
    );
}

6. useSyncExternalStore

创建

createStore.js

export default function createStore (reducer) {
  let currentState;
  let listeners = [];
  
  function getState() {
    return currentState;
  }
  
  function dispatch(action) {
    currentState = reducer(action, currentState);
    listeners.forEach(listener => listener())
  }
  
  function subscribe(listener) {
    listeners.push(listener);
  }
  
  return {
    getState,
    dispatch,
    subscribe
  }
}

index.js

import createStore from "./createStore";
​
const store = createStore(countReducer)
​
export default store;
​
function countReducer(action, state = 0){
  switch (action.type) {
    case "ADD":
      return state + 1;
    case "MINUS":
      return state - 1;
    default:
      return state;
  }
}

使用

import store from './index'
// 获取
const state = useSyncExternalStore(store.subScribe,store.getState)
​
​
// 修改
store.dispatch(type:'ADD')

7. useEffect

在这里插入图片描述

三、state 和 props

state是变量,一般情况下,state改变之后,组件会更新。

props是属性,用于父子通信,且props不可修改。

1、组件不更新的情况

状态改变,组件不会更新的情况:指更新被拦截,比如使用PureComponent组件、Component组件的shouldComponentUpdate、memo、useMemo。具体可参考下面的组件优化小节

四、Redux、MobX、Recoil、解决什么问题

这三者都是状态管理库,当组件内部状态无法满足需求的时候,比如需要实现组件间的状态共享,此时就可以定义一些外部状态,同时还要保证外部状态更新了,组件也随之更新

Redux

基于函数式编程思想实现,集中式管理状态仓库,即一个项目中通常只定义一个store。

image

MobX

是个响应式状态管理库,实现之初参考了Vue的设计思想。与Redux不同,MobX奉行分散式管理状态,即你可以定义多个store。其主要实现思路是拦截状态值的get与set函数,get时候的把状态值标记可观察变量,set的时候让组件更新。

image1

Recoil

Recoil本身就是React的状态管理库,不再需要与React的绑定库,属于一体机。在Recoil中,状态的定义是渐进式和分布式的。

总结一下,Redux集中管理一个大状态,优点是比较专一,缺点是对于某些场景,比如不需要大量共享状态的时候,就不是特别灵活。而MobX和Recoil是可以分散式管理状态,因此相对Redux来说,状态比较单一来源。Recoil由于又多了一层selector,因此又可以渐进式定义状态。因此,就学习成本来说,一般是这样:Redux<MobX<Recoil。

五、fiber是什么

iber是VDOM的一种表现形式。

  • 传统的VDOM中,如React15 VDOM 和 Vue3 VDOM,当父节点有多个子节点的时候,父节点标记子节点的属性children是个数组,在更新VDOM的过程中,我们会按照深度优先遍历的方式,自上而下,自左而右,遍历子节点
  • 但是随着React的演进,传统VDOM被淘汰,Fiber取而代之,Fiber与传统VDOM的不同之处主要体现在它的结构上,如child、return、sibling属性的添加。关系如下图所示:

image2

为什么要用fiber

在传统的stack reconciler中,一旦任务开始,就无法停下,不管这个任务有多庞大,而这个时候如果来了更高优先级的任务,那么高优先级的任务无法得到立即处理,从而会出现卡顿现象。

如何解决无法停止

做任务分解、给任务添加优先级,即实现增量渲染,把把渲染任务拆分成块,匀到多帧。这也就意味着一个任务执行完毕后,下个任务可能是它的下一个兄弟节点或者叔叔节点,这个时候Fiber的链表结构就派上用场了。

六、函数组件和类组件的内部状态不同点

类组件

export defalut class SetStatePage extends Component {
  changeCount = () => {
    const { count } = this.state
    flushSync(()=>{
      this.setState({
        count:count + 1
      })
    })
    
    console.log("改变count",this.state.count); // sy-log 1
  }
}

函数组件

function FCSetStatePage(props) {
  const [count, setCount] = useState(0)
  
  const changeCount = () => {
    flushSync(()=>{
      setCount(count + 1);
    });
    
    console.log('count',count); // sy-log 0
  }
}

相同点

定义组件状态,并且状态更新,组件也要更新

不同点

存储方式

  • 类组件的state存储在类组件实例与fiber上
  • 函数组件的state存储在fiber的hook上

更新不同

  • setState时候,类组件的新的state与旧的state合并对象
  • 而函数组件是新的state覆盖老的state。并且,在useState的setState中,新旧state相同,则函数组件拒绝更新。

取值不同

  • 函组件中使用状态,直接使用this.state,它的直接来源是类组件实例。
  • 函数组件中使用状态,直接使用useState或者useReducer函数返回值数组的第0个元素,这个值来着fiber上的hook对象。

换句话说,如果想要获取类组件的新的状态值,可以直接访问this.state。而如果想要获取函数组件中的一个新的状态值,必须重新执行useState或者useReducer函数,即必须执行函数组件。

常用组件

Fragment

function ULElment (){
  return (<ul>
      <LiElement />
    </ul>)
}
​
function LiElement () {
  return (<>
          <li></li>
          <li></li>
          </>)
}
​
或者
​
function LiElement () {
  return (<Fragment key={random}>
          <li></li>
          <li></li>
          </Fragment>)
}
​
​

Suspense

请看上面的

Portal

相当于vue3的tTeleport

八、常见组件性能优化

组件复用

协调阶段,组件复用的前提是必须同时满足三个条件:同一层级下、同一类型、同一个key值。所以我们要尽量保证这三者的稳定性。

  • 如果不复用会被销毁

减少组件的重新render

组件重新render会导致组件进入协调,协调的核心就是我们常说的vdom diff,所以协调本身就是比较耗时的算法,因此如果能够减少协调,复用旧的fiber节点,那么肯定会加快渲染完成的速度。组件如果没有进入协调阶段,我们称为进入bailout阶段,意思就是这层组件退出更新。

shouldComponentUpdate

Component类组件的一个生命周期,当用户定义的这个函数并且返回false,则进入bailout阶段。

shouldComponentUpdate(nextProps,nextState) {
    if (nextProps.m1 === this.props.m1 && nextState.m2 === this.state.m2) {
        return false;
    } else {
        return true;
    }
}

PureComponent

更新前会自行浅比较新旧props与state是否改变,如果两者都没变,则进入bailout阶段

class ChildPureComponent extends PureComponent {
  render() {
    console.log("ChildPureComponent"); //sy-log
    return (
      <div className="border">
        <p>{this.props.item}</p>
      </div>
    );
  }
}

memo

用户可以自定义,如果没有定义,默认使用浅比较,比较组件更新前后的props是否相同,如果相同,则进入bailout阶段。

const ChildMemo = memo(
  function Child({item}) {
    console.log("ChildMemo"); //sy-log
    return <div className="border">{item}</div>;
  }
  // (prev, next) => {
  //   return prev.item === next.item;
  // }
);

useMemo

可以缓存参数,可以对比useCallback使用,useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

就是吧这些缓存到fiber节点上

function ChildUseMemo({item}) {
  console.log("ChildUseMemo"); //sy-log
  return useMemo(
    () => (
      <div className="border">
        {item}
        <Child2 />
      </div>
    ),
    []
  );
}
​
function Child2() {
  console.log("Child2"); //sy-log
  return (
    <div className="border">
      <h1>Child2</h1>
      <Child3 />
    </div>
  );
}
​
function Child3() {
  console.log("Child3"); //sy-log
  return (
    <div className="border">
      <h1>Child3</h1>
    </div>
  );
}
​
const [count, setCount] = useState(0)
const expensive = useMemo(()=>{
  console.log("compute")\
  let sum = 0;
  for (let i = 0; i < count; i++) {
    sum += i;
  }
  return sums
},[count])

九、组件通信的几种方式(juejin.cn/post/717504…)

父传子

父组件可以向子组件通过传 props 的方式,向子组件进行通讯

子传父

props+回调的方式,父组件向子组件传递props进行通讯,此props为作用域为父组件自身的函数,子组件调用该函数,将子组件想要传递的信息,作为参数,传递到父组件的作用域中

兄弟组件通信

找到这两个兄弟节点共同的父节点,结合上面两种方式由父节点转发信息进行通信

跨层级通信

Context设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言,对于跨越多层的全局数据通过Context通信再适合不过

发布订阅模式

发布者发布事件,订阅者监听事件并做出反应,我们可以通过引入event模块进行通信

全局状态管理工具

借助Redux或者Mobx等全局状态管理工具进行通信,这种工具会维护一个全局状态中心Store,并根据不同的事件产生新的状态

十、useEffect/useLayoutEffect 的用法和区别

useEffect

使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后延迟执行。

useLayoutEffect

它会在所有的 DOM 变更之后同步调用 effect

解释同步 延迟

这里所谓的延迟、同步,指的是React任务调度中的任务调度,所谓延迟就是useEffect的effect不与组件渲染使用同一个任务调度函数,而是再单独调用一次任务调度函数

十一、useState和useReducer 的原理

都是用于定义函数组件内部状态、状态更新、组件更新

状态值存储在函数组件的fiber的hook.memoizedState上

组件初次渲染

  • 把state初始值存储hook.memoizedState
  • 初始化更新队列,存储到hook.queue上
  • 义dispatch事件,并存储到hook.queue上。

组件更新阶段(批量更新)

  • 检查是否有上次未处理的更新,如果有,则添加到更新队列上(更新队列是个环形链表)
  • 循环遍历更新队列,得到newState
  • 把最终得到的newState赋值到hook.memoizedState上