前端面试-React

285 阅读18分钟

1.React18有哪些更新?

  • 自动批处理

    在React18之前,状态更新是同步的,只有在React时间处理函数更新状态才会自动批处理。React18扩展了自动批处理,在promise、setTimeout或者原生时间处理中,多次状态更新也会自动批处理。例如,以往如果在一个异步函数中有多个setState操作,会导致多次重新渲染,现在可以合并为一次重新渲染,提高性能。

    如果想退出批量更新,可以使用flushSync

import React,{useState} from "react"
import {flushSync} from "react-dom"

const App=()=>{
  const [count,setCount]=useState(0)
  const [count2,setCount2]=useState(0)

  return (
    <div className="App">
      <button onClick=(()=>{
        // 第一次更新
        flushSync(()=>{
          setCount(count=>count+1)
        })
        // 第二次更新
        flushSync(()=>{
          setCount2(count2=>count2+1)
        })
      })>点击</button>
      <span>count:{count}</span>
      <span>count2:{count2}</span>	
    </div>	
  )
}
export default App
  • 全新的API useTransition

    用于区分紧急更新和过度更新。紧急更新(比如打字、点击、按下按钮等)会立即被处理,过度更新(比如从一个页面过渡到另一个页面时的数据获取和渲染)可以被标记,不会阻塞紧急更新。比如在一个大型列表渲染场景中,用户输入筛选条件(紧急更新),同时列表根据新条件重新渲染(过度更新),startTransition可以让筛选条件的响应更及时,列表的重新渲染也不会影响用户交互。

  • 并发渲染

    React18能够同时处理多个渲染任务。它可以根据设备性能和用户交互等因素,暂停、终端或者继续渲染。就像在加载多个组件时,如果其中一个组件比较复杂,React可以暂停它的渲染先去处理其他更紧急的组件渲染任务。

//React 17
import React from "react"
import ReactDOM from "react-dom"
import App from "./App"

const root = document.getElementById("root")
ReactDOM.render(<App/>,root)

// 卸载组件
ReactDOM.unmountComponentAtNode(root)  

// React 18
import React from "react"
import ReactDOM from "react-dom/client"
import App from "./App"
const root = document.getElementById("root")
ReactDOM.createRoot(root).render(<App/>)

// 卸载组件
root.unmount()  

  • 服务端渲染

    React18提供了新的renderToPipeableStream和renderToReadableStream方法用于服务端渲染,使得服务端渲染更高效、灵活。这有助于在服务端更好的生成HTML,提升应用的首屏加载速度和SEO优化。

2.React事件机制和原生DOM事件流有什么区别

react中的事件是绑定到document上面的(React17以后事件绑定在container),但它包含了原生DOM事件的引用,可以通过e.nativeEvent访问

而原生的事件是绑定到dom上面的,

因此相对绑定的地方来说,dom上的事件要优先于document上的事件执行

动机

  • 在底层磨平不同浏览器的差异,React实现了统一的事件机制,我们不再需要处理浏览器事件机制方面的兼容问题,在上层面向开发者暴露稳定、统一的、与原生事件相同的事件接口

  • React把握了事件机制的主动权,实现了对所有事件的中心化管控

  • React引入事件池避免垃圾回收,在事件池中获取或释放事件对象,避免频繁的创建和销毁

image.png React怎么阻止事件冒泡

  • 阻止合成事件的冒泡用e.stopPropagation()
  • 阻止合成事件和最外层document事件冒泡,使用e.nativeEvent.stopImmediatePropogation()
  • 阻止合成事件和除了最外层document事件冒泡,通过判断e.target避免
document.body.addEventListener('click',e=>{
  if(e.target && e.target.matches('div.stop')){
    return
  }
  this.setState({active:false})
})

3. react-router原理

自己来监听URL的变化,改变url,渲染不同组件(页面),但是页面不进行强制刷新。

  • hash模式,localhost:3000/#/abc
    • 优势就是兼容性好,在老版IE中都可以运行
    • 缺点是有一个#,显得不像一个真实的路径
  • 通过HTML5的history修改URL
  1. URL的hash
  • 原理:通过监听hashchange变化

image.png

  1. HTML5的history
  • HTML5的history API是一组用于管理浏览器历史记录的接口, 它允许你通过JavaScript动态的操作浏览器历史记录,以便在不刷新页面的情况下实现页面内容的变化和导航。
  • 前端路由不管是hash还是history模式,本质都是改变URL不能刷新页面
  • History API提供了以下几个重要方法和属性:
    • pushState():向浏览器历史堆栈中添加新的状态
    • replaceState():换当前状态而不向历史堆栈中添加新的状态
    • state:表示当前历史状态的对象
    • popState事件:当前用户点击浏览器的前进后退按钮时触发。
  • 以下是一个简单的例子,演示如何使用History API在页面中切换内容而不刷新页面

image.png 通过使用History API,你可以在不刷新页面的情况下改变URL和页面内容,使得单页面应用程序和其他现代Web应用程序能更好的管理用户导航和状态。 在history.pushState()方法中,有三个参数,它们分别表示:

  • state:这是一个表示新状态的对象,可以包含任意数据。当任务导航到新状态时,可以通过event.state来访问这个对象。在您的示例中:{page:1}表示新状态页面为第一页。
  • title:这是一个现在已经被忽略的参数,通常传入一个空字符串即可。之前这个参数用于指定新状态的标题,但现在大多数浏览器都忽略了这个值。
  • URL:这是新状态的URL地址,表示用户导航到新的页面。在示例中,“/page1” 表示用户导航到页面为/page1。

image.png

4.React性能优化

  1. 使用[React.memo]缓存组件‌:React.memo可以缓存组件,只有在传入组件的状态值发生变化时才会重新渲染。这样可以减少不必要的重新渲染,提升性能‌1。
  2. 使用[useMemo]缓存大量的计算‌:useMemo钩子可以记忆计算结果,只有当传入参数发生变化时才会重新计算,从而减少重复计算带来的性能损耗‌1。
  3. 避免使用内联对象和匿名函数‌:在render方法中定义内联函数或使用内联对象会导致每次渲染时创建新的函数或对象,影响性能。可以在constructor中或组件外部定义函数和对象,然后在render方法中引用‌。
  4. 延迟加载非立即需要的组件‌:对于不是立即需要的组件,可以延迟加载,减少初始加载时间,提升应用启动速度‌1。
  5. 调整CSS而不是强制组件加载和卸载‌:通过调整CSS来改变样式,而不是通过条件渲染来加载和卸载组件,这样可以减少DOM操作,提升性能‌1。
  6. 使用React.Fragment避免添加额外的DOM节点‌:React.Fragment可以用来避免向DOM添加额外的节点,减少DOM的操作和渲染时间‌3。
  7. 使用React.PureComponent和shouldComponentUpdate‌:React.PureComponent内部实现了shouldComponentUpdate方法,可以帮助避免不必要的更新。通过浅层比较props和state,可以减少不必要的渲染‌。
  8. 使用Key优化列表渲染‌:在渲染列表时,为每个元素添加唯一的Key属性,可以帮助React识别哪些元素发生了变化,避免不必要的重新渲染‌。
  9. 避免在render方法中调用setState‌:在render方法中调用setState会导致不必要的渲染,因为每次调用都会触发重新渲染‌。

具体实施这些优化方法的示例代码和最佳实践‌:

-   **React.memo示例**
   const Child = React.memo((props) => {
     console.log('子组件');
     return <div>子组件</div>;
   });

-   ‌**useMemo示例**‌:
   const expensiveCalculation = useMemo(() => {
     // 复杂的计算逻辑
   }, [dependency]); // 依赖项发生变化时才会重新计算

-   ‌**PureComponent示例**‌:
   class PureComponent extends React.PureComponent {
     render() {
       return <div>{this.props.children}</div>;
     }
   }

5.为什么使用Hooks

优点:

  1. 告别难以理解的class组件
  2. 解决业务逻辑难以拆分的问题
  3. 使状态逻辑复用变的简单可行
  4. 函数组件从设计理念来看,更适合react

局限性:

  1. hooks还不能完整的为函数组件提供类组件的能力
  2. 函数组件给了我们一定程度的自由,却也对开发者的水平提出了更高的要求
  3. Hooks 在使用层面有着严格的规则约束

6.Redux工作原理

Redux是一个状态管理库,使用场景:

  • 跨层级组件数据共享与通信
  • 一些需要持久化的全局数据,比如用户登录信息

Redux工作原理

使用单例模式实现

Store 一个全局状态管理对象

Reducer 一个纯函数,根据旧state和props更新新state

Action 改变状态的唯一方式是dispatch action

7.什么是fiber,fiber解决了什么问题

在React16以前,React更新是通过树的深度优先遍历完成的,遍历是不能中断的,当树的层级深就会产生栈的层级过深,页面渲染速度变慢的问题,为了解决这个问题引入了fiber,React fiber就是虚拟DOM,它是一个链表结构,返回了return、children、siblings,分别代表父fiber,子fiber和兄弟fiber,随时可中断

Fiber是纤程,比线程更精细,表示对渲染线程实现更精细的控制

应用目的
实现增量渲染,增量渲染指的是把一个渲染任务分解为多个渲染任务,而后将其分散到多个帧里。增量渲染是为了实现任务的可中断、可恢复,并按优先级处理任务,从而达到更顺滑的用户体验

Fiber的可中断、可恢复怎么实现的

fiber是协程,是比线程更小的单元,可以被人为中断和恢复,当react更新时间超过1帧时,会产生视觉卡顿的效果,因此我们可以通过fiber把浏览器渲染过程分段执行,每执行一会就让出主线程控制权,执行优先级更高的任务

fiber是一个链表结构,它有三个指针,分别记录了当前节点的下一个兄弟节点,子节点,父节点。当遍历中断时,它是可以恢复的,只需要保留当前节点的索引,就能根据索引找到对应的节点

Fiber更新机制

初始化

  1. 创建fiberRoot(React根元素)和rootFiber(通过ReactDOM.render或者ReactDOM.createRoot创建出来的)
  2. 进入beginWork

workInProgress:正在内存中构建的fiber树叫workInProgress fiber,在第一次更新时,所有的更新都发生在workInProgress树,在第一次更新后,workInProgress树上的状态是最新状态,它会替换current树

current:正在视图层渲染的树叫current fiber树

ini
 代码解读
复制代码
currentFiber.alternate = workInProgressFiber
workInProgressFiber.alternate = currentFiber

3. 深度调和子节点,渲染视图

在新建的alternate树上,完成整个子节点的遍历,包括fiber的创建,最后会以workInProgress树最为最新的渲染树,fiberRoot的current指针指向workInProgress使其变成current fiber,完成初始化流程

更新

  1. 重新创建workInProgress树,复用当前current树上的alternate,作为新的workInProgress

渲染完成后,workInProgress树又变成current树

双缓冲模式

话剧演出中,演员需要切换不同的场景,以一个一小时话剧来说,在舞台中切换场景,时间来不及。一般是准备两个舞台,切换场景从左边舞台到右边舞台演出

在计算机图形领域,通过让图形硬件交替读取两套缓冲数据,可以实现画面的无缝切换,减少视觉的抖动甚至卡顿。

react的current树和workInProgress树使用双缓冲模式,可以减少fiber节点的开销,减少性能损耗

React渲染流程

如图,React用JSX描述页面,JSX经过babel编译为render function,执行后产生VDOM,VDOM不是直接渲染的,会先转换为fiber,再进行渲染。vdom转换为fiber的过程叫reconcile,转换过程会创建DOM,全部转换完成后会一次性commit到DOM,这个过程不是一次性的,而是可打断的,这就是fiber架构的渲染流程

vdom(React Element对象)中只记录了子节点,没有记录兄弟节点,因此渲染不可打断

fiber(fiberNode对象)是一个链表,它记录了父节点、兄弟节点、子节点,因此是可以打断的

8.React高级使用

Hocks使用

useSyncExternalStore

你的多数组件只会从它们的 propsstate,以及 context 读取数据。然而,有时一个组件需要从一些 React 之外的 store 读取一些随时间变化的数据。这包括:

  • 在 React 之外持有状态的第三方状态管理库
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';

export default function TodosApp() {
  const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
  return (
    <>
      <button onClick={() => todosStore.addTodo()}>Add todo</button>
      <hr />
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </>
  );
}
// 这是一个第三方 store 的例子,
// 你可能需要把它与 React 集成。

// 如果你的应用完全由 React 构建,
// 我们推荐使用 React state 替代。

let nextId = 0;
let todos = [{ id: nextId++, text: 'Todo #1' }];
let listeners = [];

export const todosStore = {
  addTodo() {
    todos = [...todos, { id: nextId++, text: 'Todo #' + nextId }]
    emitChange();
  },
  subscribe(listener) {
    listeners = [...listeners, listener];
    return () => {
      listeners = listeners.filter(l => l !== listener);
    };
  },
  getSnapshot() {
    return todos;
  }
};

function emitChange() {
  for (let listener of listeners) {
    listener();
  }
}
  • 暴露出一个可变值及订阅其改变事件的浏览器 API
import { useSyncExternalStore } from 'react'

export function useLocalStorage(key, initialValue){
    // 订阅浏览器事件,并返回取消订阅函数
    const subscribe = (listener) => {
        window.addEventListener('storage', listener)
        return () => window.removeEventListener('storage', listener)
    }
    // getSnapshot 获取当前值快照
    const getSnapshot = () => localStorage.getItem(key)
    // 使用useSyncExternalStore, 返回store
    const store = useSyncExternalStore(subscribe, getSnapshot)
    // 业务逻辑通过setState去改变Store并派发事件
    const setState = (v) => {
        const preState = JSON.parse(store)
        const nextState = typeof v === 'function' ? v(preState) : v
        window.localStorage.setItem(key, JSON.stringif(nextStore))
        window.dispatchEvent(new StorageEvent('storage', {
            key,
            newValue: JSON.stringify(nextState)
        }))
    }
        return [store?JSON.parse(store) : initialValue, setState]
}

useImperativeHandle

  • 向父组件暴露一个自定义的 ref 句柄
import { forwardRef } from 'react';  

const MyInput = forwardRef(function MyInput(props, ref) {  

return <input {...props} ref={ref} />;  
});

在上方的代码中,MyInput 的 ref 会接收到 <input> DOM 节点。然而,你可以选择暴露一个自定义的值。为了修改被暴露的句柄,在你的顶层组件调用 useImperativeHandle

import { forwardRef, useRef, useImperativeHandle } from 'react';  
const MyInput = forwardRef(function MyInput(props, ref) {  
    const inputRef = useRef(null);  
    useImperativeHandle(ref, () => {  
        return {  
            focus() {  
                inputRef.current.focus();  
            },  
            scrollIntoView() {  
                inputRef.current.scrollIntoView();  
            },  
        };  
    }, []);  
    return <input {...props} ref={inputRef} />;  
});
  • 暴露你自己的命令式方法

你通过命令式句柄暴露出来的方法不一定需要完全匹配 DOM 节点的方法。例如,这个 Post 组件暴露了一个 scrollAndFocusAddComment 方法。它可以让你在点击按钮后,使父组件 Page 滚动到评论列表的底部 聚焦到输入框

//Appjs
import { useRef } from 'react';
import Post from './Post.js';

export default function Page() {
  const postRef = useRef(null);

  function handleClick() {
    postRef.current.scrollAndFocusAddComment();
  }

  return (
    <>
      <button onClick={handleClick}>
        Write a comment
      </button>
      <Post ref={postRef} />
    </>
  );
}

//Post.js
import { forwardRef, useRef, useImperativeHandle } from 'react';
import CommentList from './CommentList.js';
import AddComment from './AddComment.js';

const Post = forwardRef((props, ref) => {
  const commentsRef = useRef(null);
  const addCommentRef = useRef(null);

  useImperativeHandle(ref, () => {
    return {
      scrollAndFocusAddComment() {
        commentsRef.current.scrollToBottom();
        addCommentRef.current.focus();
      }
    };
  }, []);

  return (
    <>
      <article>
        <p>Welcome to my blog!</p>
      </article>
      <CommentList ref={commentsRef} />
      <AddComment ref={addCommentRef} />
    </>
  );
});

export default Post;

//CommentList.js
import { forwardRef, useRef, useImperativeHandle } from 'react';

const CommentList = forwardRef(function CommentList(props, ref) {
  const divRef = useRef(null);

  useImperativeHandle(ref, () => {
    return {
      scrollToBottom() {
        const node = divRef.current;
        node.scrollTop = node.scrollHeight;
      }
    };
  }, []);

  let comments = [];
  for (let i = 0; i < 50; i++) {
    comments.push(<p key={i}>Comment #{i}</p>);
  }

  return (
    <div className="CommentList" ref={divRef}>
      {comments}
    </div>
  );
});

export default CommentList;

//addComment.js
import { forwardRef, useRef, useImperativeHandle } from 'react';

const AddComment = forwardRef(function AddComment(props, ref) {
  return <input placeholder="Add comment..." ref={ref} />;
});

export default AddComment;

  • useEffect和useLayoutEffect这两个到底有什么区别?

一句话总结就是这两者的执行时机是不一样的,什么意思呢?就是useEffect的大致流程就是触发函数渲染执行,然后React调用组件的渲染函数,然后在屏幕中渲染完毕,最后再执行useEffect。useLayoutEffect呢?它是先触发渲染函数的执行,然后React调用组件的渲染函数,然后这时候执行useLayoutEffect。并且React等它执行完成之后,屏幕才会绘制完成。

useEffect其实是异步的,而useLayoutEffect它是同步执行的(换句话说useLayoutEffect阻塞了浏览器的绘制),useEffect的执行时机是浏览器渲染完成之后,而useLayoutEffect它的执行时机是浏览器把内容真正渲染到页面之前去执行

  • useTransition和useDeferredValue的区别和用法

这里涉及到React18的重要概念concurrency并发性,其实就是如何处理同时更新多个状态,提升UI渲染效率的机制,这里对渲染的优先级做了区分,就跟排队的快速通道和低速通道。

React提供了两个接口给开发者设置渲染优先级,提升用户体验。但凡是包裹在useTransition的变更,或useDeferredValue的数据,都表示渲染优先级比较低(相当于低速通道),渲染的时候会有一定的滞后性,从而用更多的CPU资源来渲染优先级更高的更新。

什么时候用useDeferredValue?假如你使用第三方的Hooks,它只给你暴露了更新结果,没有给你暴露更新函数,那就没法用useTransition,只能用useDeferredValue。什么情况用useTransition,当你需要处理更新方法,useTransition的回调函数是可以处理多个更新方法调用的,React会批量更新,性能会好一些。

useDeferredValue用法:

import { useState, useDeferredValue } from 'react';
import SlowList from './SlowList.js';

export default function App() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text);
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <SlowList text={deferredText} />
    </>
  );
}

// SlowList.js
import { memo } from 'react';

const SlowList = memo(function SlowList({ text }) {
  // 仅打印一次。实际的减速是在 SlowItem 组件内部。
  console.log('[ARTIFICIALLY SLOW] Rendering 250 <SlowItem />');

  let items = [];
  for (let i = 0; i < 250; i++) {
    items.push(<SlowItem key={i} text={text} />);
  }
  return (
    <ul className="items">
      {items}
    </ul>
  );
});

function SlowItem({ text }) {
  let startTime = performance.now();
  while (performance.now() - startTime < 1) {
    // 每个 item 暂停 1ms,模拟极其缓慢的代码
  }

  return (
    <li className="item">
      Text: {text}
    </li>
  )
}

export default SlowList;

useTransition用法

import { useTransition } from 'react';

export default function TabButton({ children, isActive, onClick }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        onClick();
      });
    }}>
      {children}
    </button>
  )
}

  • useCallback 与 useMemo的区别

useMemo 经常与 useCallback 一同出现。当尝试优化子组件时,它们都很有用。他们会 记住(或者说,缓存)正在传递的东西:

import { useMemo, useCallback } from 'react';  
function ProductPage({ productId, referrer }) {  
    const product = useData('/product/' + productId);  
    const requirements = useMemo(() => { //调用函数并缓存结果  
        return computeRequirements(product);  
    }, [product]);  
    const handleSubmit = useCallback((orderDetails) => { // 缓存函数本身  
        post('/product/' + productId + '/buy', {  
            referrer,  
            orderDetails,  
        });  
    }, [productId, referrer]);  
    return (  
        <div className={theme}>  
        <ShippingForm requirements={requirements} onSubmit={handleSubmit} />  
        </div>  
    );  
}

useMemo 缓存函数调用的结果。在这里,它缓存了调用 computeRequirements(product) 的结果。除非 product 发生改变,否则它将不会发生变化。这让你向下传递 requirements 时而无需不必要地重新渲染 ShippingForm。必要时,React 将会调用传入的函数重新计算结果。 useCallback 缓存函数本身。不像 useMemo,它不会调用你传入的函数。相反,它缓存此函数。从而除非 productId 或 referrer 发生改变,handleSubmit 自己将不会发生改变。这让你向下传递 handleSubmit 函数而无需不必要地重新渲染 ShippingForm。直至用户提交表单,你的代码都将不会运行。

什么情况下使用这两个hooks?

将其作为 props 传递给包装在 [memo] 中的组件。如果 props 未更改,则希望跳过重新渲染。缓存允许组件仅在依赖项更改时重新渲染。 传递的函数可能作为某些 Hook 的依赖。比如,另一个包裹在 useCallbackuseMemo 中的函数依赖于它,或者依赖于 useEffect 中的函数。

  • 对比 useState 和 useReducer

    • 代码体积:  通常,在使用 useState 时,一开始只需要编写少量代码。而 useReducer 必须提前编写 reducer 函数和需要调度的 actions。但是,当多个事件处理程序以相似的方式修改 state 时,useReducer 可以减少代码量。
    • 可读性:  当状态更新逻辑足够简单时,useState 的可读性还行。但是,一旦逻辑变得复杂起来,它们会使组件变得臃肿且难以阅读。在这种情况下,useReducer 允许你将状态更新逻辑与事件处理程序分离开来。
    • 可调试性:  当使用 useState 出现问题时, 你很难发现具体原因以及为什么。 而使用 useReducer 时, 你可以在 reducer 函数中通过打印日志的方式来观察每个状态的更新,以及为什么要更新(来自哪个 action)。 如果所有 action 都没问题,你就知道问题出在了 reducer 本身的逻辑中。 然而,与使用 useState 相比,你必须单步执行更多的代码。
    • 可测试性:  reducer 是一个不依赖于组件的纯函数。这就意味着你可以单独对它进行测试。一般来说,我们最好是在真实环境中测试组件,但对于复杂的状态更新逻辑,针对特定的初始状态和 action,断言 reducer 返回的特定状态会很有帮助。
    • 个人偏好:  并不是所有人都喜欢用 reducer,没关系,这是个人偏好问题。你可以随时在 useState 和 useReducer 之间切换,它们能做的事情是一样的!

如果你在修改某些组件状态时经常出现问题或者想给组件添加更多逻辑时,我们建议你还是使用 reducer。当然,你也不必整个项目都用 reducer,这是可以自由搭配的。你甚至可以在一个组件中同时使用 useStateuseReducer

useRecucer使用:

import { useReducer } from 'react';

function reducer(state, action) {
  if (action.type === 'incremented_age') {
    return {
      age: state.age + 1
    };
  }
  throw Error('Unknown action.');
}

export default function Counter() {
  const [state, dispatch] = useReducer(reducer, { age: 42 });

  return (
    <>
      <button onClick={() => {
        dispatch({ type: 'incremented_age' })
      }}>
        Increment age
      </button>
      <p>Hello! You are {state.age}.</p>
    </>
  );
}