Skip to content
什么是JSX?

JSX 是 JavaScript 语法扩展,可以让你在 JavaScript 文件中书写类似 HTML 的标签。

  • 只能返回一个根元素;
  • 标签必须闭合;
  • 使用驼峰式命名法给大部分属性命名;
useCallback

在多次渲染中缓存函数

const cachedFn = useCallback(fn, dependencies)

  • fn: 想要缓存的函数
  • dependencies:有关是否更新 fn 的所有响应式值的一个列表;React 使用 Object.is 比较每一个依赖和它的之前的值
    • 没有依赖项数组,每次都会返回一个新函数
jsx
/** 跳过组件的重新渲染   **/
/** 防止频繁触发 Effect **/
import { useCallback } from 'react';

export default function ProductPage({ productId, referrer, theme }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);
useEffect

执行副作用(componentDidMount、componentDidUpdate、componentWillUnmount)

useEffect(setup, dependencies?)

jsx
import { useState, useEffect } from 'react';

export default function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {

    const timer = setInterval(() => {
      console.log('console once per second.');
    }, 1000);

    function handleMove(e) {
      setPosition({ x: e.clientX, y: e.clientY });
    }

    window.addEventListener('pointermove', handleMove);

    return () => {
      // 清理(cleanup)函数
      // 清除监听事件
      window.removeEventListener('pointermove', handleMove);

      clearInterval(timer);
    };
  }, []);


  useEffect(() => {
    // 这里的代码会在每次渲染后执行
  });

  useEffect(() => {
    // 这里的代码只会在组件挂载后执行
  }, []);

  useEffect(() => {
    // 这里的代码只会在每次渲染后,并且 a 或 b 的值与上次渲染不一致时执行
  }, [a, b]);

  return null;
}
useMemo

每次重新渲染的时候能够缓存计算的结果

const cachedValue = useMemo(calculateValue, dependencies)

  • calculateValue:要缓存计算值的函数
  • dependencies: 所有在 calculateValue 函数中使用的响应式变量组成的数组;React 使用 Object.is 将每个依赖项与其之前的值进行比较。
jsx
import { useMemo } from 'react';

function TodoList({ todos, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
}
useContext

读取和订阅组件中的 context

const value = useContext(SomeContext)

jsx
/** 向组件树深层传递数据 **/
/** 通过 context 更新传递的数据 **/
/** 指定后备方案默认值 **/
/** 覆盖组件树一部分的 context **/
/** 在传递对象和函数时优化重新渲染 **/

import { createContext, useContext } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
  return (
    <ThemeContext.Provider value="dark">
      <Buton />
    </ThemeContext.Provider>
  )
}

function Button({ children }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button className={className}>
      {children}
    </button>
  );
}
useId

可以生成传递给无障碍属性的唯一 ID

jsx
/** useId 不应该被用来生成列表中的 key */
const id = useId();
useLayoutEffect

在浏览器重新绘制屏幕之前触发

jsx
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);

useLayoutEffect(() => {
  const { height } = ref.current.getBoundingClientRect();
  setTooltipHeight(height);
}, []);
useRef

它能帮助引用一个不需要渲染的值

jsx
const intervalRef = useRef(null);

function handleStart() {
  setStartTime(Date.now());
  setNow(Date.now());

  clearInterval(intervalRef.current);
  intervalRef.current = setInterval(() => {
    setNow(Date.now());
  }, 10);
}


import { forwardRef } from 'react';

// 父级组件就可以得到它的 ref
const MyInput = forwardRef(({ value, onChange }, ref) => {
  return (
    <input
      value={value}
      onChange={onChange}
      ref={ref}
    />
  );
});
useSate

它允许你向组件添加一个 状态变量

const [state, setState] = useState(initialState)

  • 如果传递函数作为 initialState,则它将被视为 初始化函数。它应该是纯函数,不应该接受任何参数,并且应该返回一个任何类型的值。当初始化组件时,React 将调用你的初始化函数,并将其返回值存储为初始状态。
jsx
const [state, setState] = useState(initialState)

/**  函数不会更新已经运行代码中的 state 状态 */
function handleClick() {
  setAge(age + 1); // setAge(42 + 1)
  setAge(age + 1); // setAge(42 + 1)
  setAge(age + 1); // setAge(42 + 1)

  // 更新函数, 它获取待定状态, 并从中计算下一个状态
  setAge(a => a + 1)
}


import { useState } from 'react';

function createInitialTodos() {
  const initialTodos = [];
  for (let i = 0; i < 50; i++) {
    initialTodos.push({
      id: i,
      text: 'Item ' + (i + 1)
    });
  }
  return initialTodos;
}

export default function TodoList() {
  // 不应该写成 createInitialTodos()
  const [todos, setTodos] = useState(createInitialTodos);
  const [text, setText] = useState('');

  return (
    <>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button onClick={() => {
        setText('');
        setTodos([{
          id: todos.length,
          text: text
        }, ...todos]);
      }}>Add</button>
      <ul>
        {todos.map(item => (
          <li key={item.id}>
            {item.text}
          </li>
        ))}
      </ul>
    </>
  );
}
useReducer

它允许你向组件里面添加一个 reducer

const [state, dispatch] = useReducer(reducer, initialArg, init?)

jsx
import { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        name: state.name,
        age: state.age + 1
      };
    }
    case 'changed_name': {
      return {
        name: action.nextName,
        age: state.age
      };
    }
  }

  throw Error('Unknown action: ' + action.type);
}

const initialState = { name: 'Taylor', age: 42 };

export default function Form() {
  const [state, dispatch] = useReducer(reducer, initialState);

  function handleButtonClick() {
    dispatch({ type: 'incremented_age' });
  }

  function handleInputChange(e) {
    dispatch({
      type: 'changed_name',
      nextName: e.target.value
    }); 
  }

  return (
    <>
      <input
        value={state.name}
        onChange={handleInputChange}
      />
      <button onClick={handleButtonClick}>
        Increment age
      </button>
      <p>Hello, {state.name}. You are {state.age}.</p>
    </>
  );
}
createRef, useRef, ref, forwordRef, useImperativeHandle
  • ref是 React 提供的用来操纵 React 组件实例或者 DOM 元素的接口

  • Class 组件中使用 ref, createRef; 每次渲染都会返回一个新的引用

  • 函数式组件中使用 ref, useRef; useRef 每次都会返回相同的引用

  • forwardRef 可以直接包裹一个函数式组件,被包裹的函数式组件会获得被分配给自己的 ref(作为第二个参数)

  • useImperativeHandle 与 forwardRef 一起使用, 控制要将哪些东西暴露给父组件。

jsx
// 接收一个ref
// 接收一个函数,这个函数返回的对象即是要暴露出的ref
// 类似useEffect,接收一个依赖数组
const FancyInput=(props, ref) =>{
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} />;
}

export default forwardRef(FancyInput);
什么是高阶组件?

高阶组件(HOC)是React中用于复用组件逻辑的一种高级技巧。它本质上是一个函数,这个函数接受一个组件作为参数,并返回一个新的组件。这个新的组件会使用传入的组件作为子组件,但不会修改或复制其输入组件的任何行为,因此被称为“纯”组件。

它们提供了一种将组件逻辑与 UI 分离的方式,使得组件更加模块化和可重用。它们可以控制 props、通过 refs 使用引用、抽象 state 以及使用其他元素包裹传入的组件。新的组件可以根据需要进行各种操作,如增强功能、添加新的 props 或修改行为等。然后,可以使用这个高阶组件来包裹任何需要复用逻辑的组件,从而轻松实现组件的复用和扩展。

jsx
const EnhancedComponent = higherOrderComponent(WrappedComponent);
  • 高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧
  • 高阶组件的参数为一个组件返回一个新的组件
  • 组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件
React render 方法的原理?在什么时候会被触发?

React render 方法的原理:

  • 描述性编程:React 的 render 方法使用 JSX(JavaScript XML)来描述组件的 UI 结构。这是一种声明式的方式,你只需要告诉 React 你想要什么,而不是如何去做。React 会负责计算最小的 DOM 操作来更新 UI。

  • 纯函数:在 React 中,render 方法应该是一个纯函数。这意味着给定相同的输入(即组件的 props 和 state),它应该总是返回相同的输出。同时,它不应该修改组件的状态或触发任何副作用

  • 虚拟 DOM:React 使用虚拟 DOM 来提高性能。当组件的 propsstate 发生变化时,React 会重新调用 render 方法并生成一个新的虚拟 DOM 树。然后,React 会将这个新的虚拟 DOM 树与旧的虚拟 DOM 树进行比较,找出差异,并只将这些差异应用到实际的 DOM 上

React render 方法的触发时机:

  • 初始化渲染:当组件首次被创建并挂载到 DOM 上时,React 会调用组件的 render 方法来生成初始的 UI。

  • 状态更新:当组件的 state 发生变化时,React 会重新调用组件的 render 方法。这通常是因为你调用了 this.setState()(在类组件中)或使用了 React 的 state hook(在函数组件中)。

  • 属性更新:当父组件传递给子组件的 props 发生变化时,子组件的 render 方法也会被重新调用。这是因为父组件的重新渲染会导致其子组件也重新渲染。

  • forceUpdate:虽然不常用,但在某些情况下,你可以使用 forceUpdate() 方法强制组件重新渲染。

总的来说,React 的 render 方法是组件生命周期中的一个重要部分,它负责根据组件的 props 和 state 生成 UI。当组件的状态或属性发生变化时,React 会重新调用 render 方法来更新 UI。

受控组件和非受控组件的理解?
  • 受控组件 - 值由 React 组件的 state 来控制
jsx
import React, { useState } from 'react';  
  
function ControlledInput() {  
  const [inputVal, setInputVal] = useState(null);  

  const handleChange = (event) => { 
    setInputVal(event.target.value);  
  };  

  return (  
    <input type="text" value={inputVal} onChange={handleChange} />  
  );  
}
  • 非受控组件 - 由 dom 自生管理,通常使用 ref 来访问 DOM 元素并获取或设置其值。
jsx
import React, { useRef } from 'react';
  
function UncontrolledInput() {  
  const inputRef = useRef(null);  

  const handleSubmit = (event) => {  
    event.preventDefault();  
    alert('你输入的值是: ' + inputRef.current.value);  
  };  
  
  return (  
    <form onSubmit={handleSubmit}>  
      <input type="text" ref={inputRef} />  
      <button type="submit">提交</button>  
    </form>  
  );  
}
React 事件和原生事件的执行顺序?

React的所有事件都通过 document 进行统一分发。当真实 DOM 触发事件后,这些事件会冒泡到document,然后 React 才会对这些事件进行处理。因此,原生事件的执行会先于React的合成事件。

  • 旧版的React中:原生事件的捕获阶段、原生事件的冒泡阶段、原生事件冒泡到 document 开始 React 事件执行。
  • 新版的React中:原生事件的捕获阶段、原生事件的冒泡阶段至 React App 根元素、React 合成事件开始从根节点往下进行事件模拟捕获和冒泡,合成事模拟完毕后,继续原生事件的冒泡到 document。
说说你对 immutable 的理解?如何应用在 react 项目中?

immutable,不可改变的,在计算机中,即指一旦创建,就不能再被更改的数据

对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象

Immutable 实现的原理是 Persistent Data Structure(持久化数据结构):

  • 当数据被修改时,会返回一个对象,但是新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费
  • 也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变,同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享
React JSX 转换成真实 DOM 过程?

JSX -> React.createElement -> VDOM -> diff VDOM -> commit update -> print

  • JSX 转换为 JavaScript 对象:

    • JSX 本质上是 JavaScript 的扩展语法,它允许我们在 JavaScript 中编写类似 HTML 的结构。
    • 当 JSX 被编译(例如,通过 Babel 这样的工具)时,它会被转换为 React.createElement 函数的调用。React.createElement 函数接收三个参数:元素的类型(通常是字符串,如 'div' 或 'span',或者是一个 React 组件类),一个包含该元素所有属性的对象,以及一个子元素数组
    • 因此,JSX 最终会被转换为一系列的 React.createElement 调用,这些调用返回一系列的 JavaScript 对象,这些对象描述了 UI 的结构。
  • React 元素转换为虚拟 DOM:

    • 这些通过 React.createElement 创建的 JavaScript 对象被称为 React 元素。它们不是真正的 DOM 元素,而是轻量级的、描述性的对象,表示屏幕上应该看到的内容。
    • React 使用这些 React 元素来构建一个虚拟 DOM 树。虚拟 DOM 是一个编程概念,是一个轻量级的 JavaScript 数据结构,它是对真实 DOM 的一个内存中的抽象表示。
  • 比较虚拟 DOM 与旧的虚拟 DOM:

    • 当组件的状态或属性发生变化时,React 会创建一个新的虚拟 DOM 树。
    • 然后,React 会使用一种高效的算法(如 React 的协调算法)来比较新的虚拟 DOM 树与旧的虚拟 DOM 树,找出两者之间的差异。
  • 将差异应用到真实的 DOM:

    • 一旦 React 知道了哪些部分发生了变化,它就会计算出必要的最小操作来更新真实的 DOM。这通常涉及添加、更新或删除一些 DOM 节点。
    • React 会尽可能地重用和重新排序现有的 DOM 节点,以提高性能。
  • 浏览器渲染:

    • 最后,浏览器接收到这些 DOM 更改并重新渲染页面。

在整个过程中,React 的目标是尽可能减少直接与真实 DOM 交互的次数,因为直接操作 DOM 是昂贵的。通过维护一个虚拟 DOM 并只更新真正发生变化的部分,React 能够实现高效的 UI 更新。

如何捕获错误?

错误边界是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生崩溃的子组件树。

形成错误边界组件的两个条件:

  • 使用了 static getDerivedStateFromError()
  • 使用了 componentDidCatch()

抛出错误后,请使用 static getDerivedStateFromError()渲染备用 UI ,使用 componentDidCatch() 打印错误信息,如下:

jsx
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 可以将错误日志上报给服务器
    logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <h1>Something is wrong.</h1>;
    }

    return this.props.children;
  }
}

jsx
<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

对于错误边界无法捕获的异常,如事件处理过程中发生问题并不会捕获到,是因为其不会在渲染期间触发,并不会导致渲染时候问题。

React Fiber 是如何实现更新过程可控?

更新过程的可控主要体现在下面几个方面:

  • 任务拆分
  • 任务挂起、恢复、终止
  • 任务具备优先级

在 React Fiber 机制中,它采用"化整为零"的思想,将调和阶段(Reconciler)递归遍历 VDOM 这个大任务分成若干小任务,每个任务只负责一个节点的处理。

workInProgress 代表当前正在执行更新的 Fiber 树。在 render 或者 setState 后,会构建一颗 Fiber 树,也就是 workInProgress tree,这棵树在构建每一个节点的时候会收集当前节点的副作用,整棵树构建完成后,会形成一条完整的副作用链

currentFiber 表示上次渲染构建的 Filber 树。在每一次更新完成后 workInProgress 会赋值给 currentFiber。在新一轮更新时 workInProgress tree 再重新构建,新 workInProgress 的节点通过 alternate 属性和 currentFiber 的节点建立联系。

在新 workInProgress tree 的创建过程中,会同 currentFiber 的对应节点进行 Diff 比较,收集副作用。同时也会复用和 currentFiber 对应的节点对象,减少新创建对象带来的开销。也就是说无论是创建还是更新、挂起、恢复以及终止操作都是发生在 workInProgress tree 创建过程中的。workInProgress tree 构建过程其实就是循环的执行任务和创建下一个任务。

当第一个小任务完成后,先判断这一帧是否还有空闲时间,没有就挂起下一个任务的执行,记住当前挂起的节点,让出控制权给浏览器执行更高优先级的任务

在浏览器渲染完一帧后,判断当前帧是否有剩余时间,如果有就恢复执行之前挂起的任务。如果没有任务需要处理,代表调和阶段完成,可以开始进入渲染阶段。

如何判断一帧是否有空闲时间的呢?使用 RequestIdleCallback 浏览器原生 API,React 源码中为了兼容低版本的浏览器,对该方法进行了 Polyfill。

恢复执行的时候又是如何知道下一个任务是什么呢?答案是在前面提到的链表。在 React Fiber 中每个任务其实就是在处理一个 FiberNode 对象,然后又生成下一个任务需要处理的 FiberNode。

其实并不是每次更新都会走到提交阶段。当在调和过程中触发了新的更新,在执行下一个任务的时候,判断是否有优先级更高的执行任务,如果有就终止原来将要执行的任务,开始新的 workInProgressFiber 树构建过程,开始新的更新流程。这样可以避免重复更新操作。这也是在 React 16 以后生命周期函数 componentWillMount 有可能会执行多次的原因。

React Fiber 除了通过挂起,恢复和终止来控制更新外,还给每个任务分配了优先级。具体点就是在创建或者更新 FiberNode 的时候,通过算法给每个任务分配一个到期时间(expirationTime)。在每个任务执行的时候除了判断剩余时间,如果当前处理节点已经过期,那么无论现在是否有空闲时间都必须执行该任务。过期时间的大小还代表着任务的优先级

任务在执行过程中顺便收集每个 FiberNode 的副作用,将有副作用的节点通过 firstEffect、lastEffect、nextEffect 形成一条副作用单链表

其实最终都是为了收集到这条副作用链表,有了它,在接下来的渲染阶段就通过遍历副作用链完成 DOM 更新。这里需要注意,更新真实 DOM 的这个动作是一气呵成的,不能中断,不然会造成视觉上的不连贯(commit)。

Fiber 为什么是 React 性能的一个飞跃?

在 React 中,Fiber 就是 React 16 实现的一套新的更新机制,让 React 的更新过程变得可控,避免了之前采用递归需要一气呵成影响性能的做法。把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。

React Fiber 把更新过程碎片化,每执行完一段更新过程,就把控制权交还给 React 负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。

基于的 Reconciler,浏览器引擎会从执行栈的顶端开始执行,执行完毕就弹出当前执行上下文,开始执行下一个函数,直到执行栈被清空才会停止。然后将执行权交还给浏览器。由于 React 将页面视图视作一个个函数执行的结果。每一个页面往往由多个视图组成,这就意味着多个函数的调用。如果一个页面足够复杂,形成的函数调用栈就会很深。每一次更新,执行栈需要一次性执行完成。结合前面提到的浏览器刷新率,JS 一直执行,浏览器得不到控制权,就不能及时开始下一帧的绘制。如果这个时间超过 16ms,当页面有动画效果需求时,动画因为浏览器不能及时绘制下一帧,这时动画就会出现卡顿。不仅如此,因为事件响应代码是在每一帧开始的时候执行,如果不能及时绘制下一帧,事件响应也会延迟。

在 React Fiber 中用链表遍历的方式替代了 React 16 之前的栈递归方案。在 React 16 中使用了大量的链表。链表相比顺序结构数据格式的好处就是:

  • 操作更高效,比如顺序调整、删除,只需要改变节点的指针指向就好了。
  • 不仅可以根据当前节点找到下一个节点,在多向链表中,还可以找到他的父节点或者兄弟节点。

但链表也不是完美的,缺点就是:

  • 比顺序结构数据更占用空间,因为每个节点对象还保存有指向下一个对象的指针。
  • 不能自由读取,必须找到他的上一个节点。

React 用空间换时间,更高效的操作可以方便根据优先级进行操作。同时可以根据当前节点找到其他节点,在挂起和恢复过程中起到了关键作用。

简述下 React 的事件代理机制?

React 并不会把所有的处理函数直接绑定在真实的节点上。而是把所有的事件绑定到结构的最外层,使用一个统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部的事件监听和处理函数。当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象。

当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。这样做的优点是解决了兼容性问题,并且简化了事件处理和回收机制(不需要手动的解绑事件,React 已经在内部处理了)。但是有些事件 React 并没有实现,比如 window 的 resize 事件。

为什么不能在循环、条件或嵌套函数中调用 Hooks?

react 用链表来严格保证 hooks 的顺序。

js
function mountWorkInProgressHook() {
  var hook = {
    memoizedState: null,

    baseState: null,

    baseQueue: null,

    queue: null,

    next: null,
  };

  if (workInProgressHook === null) {
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }

  return workInProgressHook;
}
如何让 useEffect 支持 async/await?
js
function useAsyncEffect(
  effect: (isCanceled: () => boolean) => Promise<void>,
  dependencies?: any[]
) {
  return useEffect(() => {
    let canceled = false;
    effect(() => canceled);
    return () => {
      canceled = true;
    };
  }, dependencies);
}

由于 useEffect 是在函数式组件中承担执行副作用操作的职责,它的返回值的执行操作应该是可以预期的,而不能是一个异步函数,所以不支持回调函数 async...await 的写法。

说说你对 React Hook 的闭包陷阱的理解,有哪些解决方案?
jsx
function App() {
  const [count, setCount] = useState(1);
  useEffect(() => {
    setInterval(() => {
      // 始终是 1
      console.log(count);
    }, 1000);
  }, []);
}

useRef() 所返回的都是同一个对象

怎么在代码中判断一个 React 组件是 class component 还是 function component?
js
function isClassComponent(component) {
  return (
    typeof component === "function" && !!component.prototype.isReactComponent
  );
}
React 中,怎么实现父组件调用子组件中的方法?

如果是类组件,可以在子组件类中定义一个方法,并将其挂载到实例上。例如:

jsx
class ChildComponent extends React.Component {
  childMethod() {
    // 子组件中需要执行的操作
  }

  render() {
    // 子组件的渲染逻辑
  }
}

jsx
class ParentComponent extends React.Component {
  constructor(props) {
    super(props);
    this.childRef = React.createRef();
  }

  handleClick() {
    // 调用子组件的方法
    this.childRef.current.childMethod();
  }

  render() {
    return (
      <div>
        <ChildComponent ref={this.childRef} />
        <button onClick={() => this.handleClick()}>调用子组件方法</button>
      </div>
    );
  }
}

如果是函数式组件,可以使用 useImperativeHandle Hook 将指定的方法暴露给父组件。例如:

jsx
import { forwardRef, useImperativeHandle } from "react";

function ChildComponent(props, ref) {
  useImperativeHandle(ref, () => ({
    childMethod() {
      // 子组件中需要执行的操作
    },
  }));

  // 子组件的渲染逻辑
}

export default forwardRef(ChildComponent);

jsx
function ParentComponent() {
  const childRef = useRef(null);

  const handleClick = () => {
    // 调用子组件的方法
    childRef.current.childMethod();
  };

  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={handleClick}>调用子组件方法</button>
    </div>
  );
}
怎么判断一个对象是否是 React 元素?

React.isValidElement

jsx
import React from "react";

const App = () => {
  return <div>Hello, world!</div>;
};

const ele = <App />;

console.log(React.isValidElement(ele)); // true
console.log(React.isValidElement({})); // false
React 和 Vue 在技术层面有哪些区别?

react:

  • 单向数据流
  • 函数式编程, UI = Function(data)
  • jsx 语法

vue:

  • MVVM
  • 数据劫持,双向绑定
  • template 语法
React 中 Element、Component、Node、Instance 四个概念的理解 ?
  • Element:Element 是 React 应用中最基本的构建块,它是一个普通的 JavaScript 对象,用来描述 UI 的一部分。Element 可以是原生的 DOM 元素,也可以是自定义的组件。它的作用是用来向 React 描述开发者想在页面上 render 什么内容。Element 是不可变的,一旦创建就不能被修改。

  • Component:Component 是 React 中的一个概念,它是由 Element 构成的,可以是函数组件或者类组件。Component 可以接收输入的数据(props),并返回一个描述 UI 的 Element。Component 可以被复用,可以在应用中多次使用。分为 Class Component 以及 Function Component。

  • Node:Node 是指 React 应用中的一个虚拟节点,它是 Element 的实例。Node 包含了 Element 的所有信息,包括类型、属性、子节点等。Node 是 React 内部用来描述 UI 的一种数据结构,它可以被渲染成真实的 DOM 元素。

  • Instance:Instance 是指 React 应用中的一个组件实例,它是 Component 的实例。每个 Component 在应用中都会有一个对应的 Instance,它包含了 Component 的所有状态和方法。Instance 可以被用来操作组件的状态,以及处理用户的交互事件等。

react-router ?

react-router 等前端路由的原理大致相同,可以实现无刷新的条件下切换显示不同的页面。本质就是页面的 URL 发生改变时,页面的显示结果可以根据 URL 的变化而变化,但是页面不会刷新。因此,可以通过前端路由可以实现单页(SPA)应用。

React Router 对应的 hash 模式和 history 模式对应的组件为:

  • HashRouter:在 url 后面加上#,如http://127.0.0.1:5173/vm/#/host, 通过window.addEventListener('hashChange',callback)监听 hash 值的变化,并传递给其嵌套的组件

  • BrowserRouter: 允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录

react-router 主要分成了几个不同的包:

  • react-router: 实现了路由的核心功能 - react-router-dom: 基于 react-router,加入了在浏览器运行环境下的一些功能
  • react-router-native:基于 react-router,加入了 react-native 运行环境下的一些功能
  • react-router-config: 用于配置静态路由的工具库

这里主要讲述的是 react-router-dom 的常用 API,主要是提供了一些组件:

  • BrowserRouter、HashRouter
  • Route
  • Link、NavLink
  • switch
  • redirect

Router 中包含了对路径改变的监听,并且会将相应的路径传递给子组件。

jsx
import { BrowserRouter as Router } from "react-router-dom";

export default function App() {
  return (
    <Router>
      <main>
        <nav>
          <ul>
            <li>
              <a href=" ">Home</a>
            </li>
            <li>
              <a href="/about">About</a>
            </li>
            <li>
              <a href="/contact">Contact</a>
            </li>
          </ul>
        </nav>
      </main>
    </Router>
  );
}

Route 用于路径的匹配,然后进行组件的渲染,对应的属性如下:

  • path 属性:用于设置匹配到的路径
  • component 属性:设置匹配到路径后,渲染的组件
  • render 属性:设置匹配到路径后,渲染的内容
  • exact 属性:开启精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件

通常路径的跳转是使用Link组件,最终会被渲染成 a 元素,其中属性 to 代替 a 标题的 href 属性

NavLink 是在 Link 基础之上增加了一些样式属性,例如组件被选中时,发生样式变化,则可以设置 NavLink 的一下属性:

  • activeStyle:活跃时(匹配时)的样式
  • activeClassName:活跃时添加的 class
jsx
<NavLink to="/" exact activeStyle={{ color: "black" }}>首页</NavLink>
<NavLink to="/about" activeStyle={{ color: "black" }}>关于</NavLink>

如果需要实现 js 实现页面的跳转,那么可以通过下面的形式:

  • 通过 Route 作为顶层组件包裹其他组件后,页面组件就可以接收到一些路由相关的东西,props 中接收到的 history 对象具有一些方便的方法,如 goBackgoForward, push
jsx
const Contact = ({ history }) => (
  <>
    <h1>xxx</h1>
    <button onClick={() => history.push("/")}>Go</button>
  </>
);
  • redirect 用于路由的重定向,当这个组件出现时,就会执行跳转到对应的 to 路径中,如下例子:
jsx
const About = ({
  match: {
    params: { name },
  },
}) => (
  // props.match.params.name
  <>
    {name !== "qzjiang" ? <Redirect to="/" /> : null}
    <h1>About {name}</h1>
  </>
);
  • swich 组件的作用适用于当匹配到第一个组件的时候,后面的组件就不应该继续匹配。如下例子:
jsx
<Switch>
  <Route exact path="/" component={Index} />
  <Route path="/:vmId" component={Vm} />
  <Route component={NoMatch} />
</Switch>

除了一些路由相关的组件之外,react-router 还提供一些 hooks,如下:

  • useHistory
  • useParams
  • useLocation
jsx
import { useHistory, useLocation, useParams } from "react-router-dom";

const Contact = () => {
  const history = useHistory();
  return (
    <>
      <h1>xxx</h1>
      <button onClick={() => history.push("/")}>Go</button>
    </>
  );
};

const About = () => {
  const { name } = useParams();
  return (
    <>{name !== "John Doe" ? <Redirect to="/" /> : <h1>About {name}</h1>}</>
  );
};

const Contact = () => {
  const { pathname } = useLocation();
  return (
    <>
      <h1>Contact</h1>
      <p>Current URL: {pathname}</p>
    </>
  );
};

这些路由传递参数主要分成了三种形式:

  • 动态路由的方式
  • search 传递参数
  • to 传入对象
jsx
// to传参
// console.log(props.match.params.xxx)
<NavLink to="/vm/123">详情</NavLink>

<Switch>
  <Route path="/vm/:id" component={vmDetail}/>
  <Route component={NoMatch} />
</Switch>


// search, 在跳转的路径中添加了一些query参数;
// console.log(props.location.search)
<NavLink to="/vm?vmId=xxx&hostId=xxxx">vm</NavLink>

<NavLink to={{
    pathname: "/vm",
    query: {
      name: "vm-1",
    },
    state: {
      status: "running",
    },
    search: "?hostId=xxx"
  }}>
  vm
</NavLink>
// console.log(props.location)
react-router 里的 Link 标签和 a 标签有什么区别?

Link 避免了不必要的重新渲染。react-router 接管了其默认的链接跳转行为,与传统的页面跳转有区别的是,Link 的跳转行为只会触发相匹配的对应的页面内容更新,而不会刷新整个页面。

Link 跳转做了三件事情:

  • 有 onclick 那就执行 onclick
  • click 的时候阻止 a 标签默认事件
  • 根据跳转 href,用 history 跳转,此时只是链接变了,并没有刷新页面

而 a 标签就是普通的超链接了,用于从当前页面跳转到 href 指向的另一个页面(非锚点情况)。

使用 useState 有时候连续调用会出现值的丢失?
jsx
setState((prevState) => [...prevState, newValue]);
实现一个 useTimeout ?
jsx
function useTimeout(callback, delay) {
  const memoCb = useRef();

  useEffect(() => {
    memoCb.current = callback;
  }, [callback]);

  useEffect(() => {
    if (delay !== null) {
      const timer = setTimeout(() => {
        memoCb.current();
      }, delay);
      return () => {
        clearTimeout(timer);
      };
    }
  }, [delay]);
}
React hooks 和生命周期?
  • useState 只在初始化时执行一次,后面不再执行;

  • useEffect 相当于是 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合,可以通过传参及其他逻辑,分别模拟这三个生命周期函数;

  • useEffect 第二个参数是一个数组,如果数组为空时,则只执行一次(相当于 componentDidMount);如果数组中有值时,则该值更新时,useEffect 中的函数才会执行;如果没有第二个参数,则每次 render 时,useEffect 中的函数都会执行;

  • React 保证了每次运行 effect 的同时,DOM 都已经更新完毕,也就是说 effect 中的获取的 state 是最新的,但是需要注意的是,effect 中返回的函数(其清除函数)中,获取到的 state 是更新前的。

  • 传递给 useEffect 的函数在每次渲染中都会有所不同,这是刻意为之的。事实上这正是我们可以在 effect 中获取最新的 count 的值,而不用担心其过期的原因。每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。

  • effect 的清除阶段(返回函数)在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次。它会在调用一个新的 effect 之前对前一个 effect 进行清理,从而避免了我们手动去处理一些逻辑 。

React.memo() 和 useMemo() 的用法是什么,有哪些区别?
  • React.memo() 是一个高阶组件,我们可以使用它来包装我们不想重新渲染的组件,除非其中的 props 发生变化。(PureComponent, shouldComponentUpdate)
  • useMemo() 是一个 React Hook,我们可以使用它在组件中包装函数。 我们可以使用它来确保该函数中的值仅在其依赖项之一发生变化时才重新计算。
React 中可以做哪些性能优化?
  • 使用 shouldComponentUpdate 避免不需要的渲染,但是如果对 props 和 state 做深比较,代价很大,所以需要根据业务进行些取舍;在有子组件的情况下,为了避免子组件的重复渲染,可以通过父组件来判断子组件是否需要 PureRender。

  • 将 props 设置为数组或对象:每次调用 React 组件都会创建新组件,就算传入的数组或对象的值没有改变,他们的引用地址也会发生改变,比如,如果按照如下的写法,那么每次渲染时 style 都是一个新对象

  • 将函数的绑定移动到构造函数内:可以避免每次都绑定事件。

  • 使用 不可变数据

  • 给子组件设置一个唯一的 key,因为在 diff 算法中,会用 key 作为唯一标识优化渲染

useEffect 与 useLayoutEffect 有什么区别?
  • 使用场景:

    • useEffect 在 React 的渲染过程中是被异步调用的,用于绝大多数场景;
    • 而 useLayoutEffect 会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。也正因为是同步处理,所以需要避免在 useLayoutEffect 做计算量较大的耗时任务从而造成阻塞。
  • 使用效果:

    • useEffect 是按照顺序执行代码的,改变屏幕像素之后执行(先渲染,后改变 DOM),当改变屏幕内容时可能会产生闪烁;
    • useLayoutEffect 是改变屏幕像素之前就执行了(会推迟页面显示的事件,先改变 DOM 后渲染),不会产生闪烁。useLayoutEffect 总是比 useEffect 先执行。
React Hooks 在使用上有哪些限制?
  • 不要在循环、条件或嵌套函数中调用 Hook
  • 在 React 的函数组件中调用 Hook
说说 react 中引入 css 的方式有哪几种?区别?
  • 在组件内直接定义 style 对象
  • 引入 css 文件(样式是全局生效,样式之间会互相影响)
  • 组件中引入 .module.css 文件(webpack 配置文件中 modules:true
  • CSS in JS(styled-components,emotion)
react 中懒加载的实现原理是什么?

React.lazy 函数,它能让你像渲染常规组件一样处理动态引入的组件,配合 webpack 的 Code Splitting ,只有当组件被加载,对应的资源才会导入 ,从而达到懒加载的效果。

webpack 检测到import()语法会自动代码分割。使用这种动态导入语法代替以前的静态引入,可以让组件在渲染的时候,再去加载组件对应的资源。

jsx
import React, { Suspense } from "react";

const Host = React.lazy(() => import("./host"));

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <Host />
      </Suspense>
    </div>
  );
}


function import(url) {
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    const tempGlobal = "__tempModuleLoadingVariable" + Math.random().toString(32).substring(2);
    script.type = "module";
    script.textContent = `import * as m from "${url}"; window.${tempGlobal} = m;`;

    script.onload = () => {
      resolve(window[tempGlobal]);
      delete window[tempGlobal];
      script.remove();
    };

    script.onerror = () => {
      reject(new Error("Failed to load module script with URL " + url));
      delete window[tempGlobal];
      script.remove();
    };

    document.documentElement.appendChild(script);
  });
}
React Fiber 是什么?

首先要知道的是,JavaScript 引擎和页面渲染引擎两个线程是互斥的,当其中一个线程执行时,另一个线程只能挂起等待。

在这样的机制下,如果 JavaScript 线程长时间地占用了主线程,那么渲染层面的更新就不得不长时间地等待,界面长时间不更新,会导致页面响应度变差,用户可能会感觉到卡顿。

而这正是 React 15 的 Stack Reconciler 所面临的问题,即是 JavaScript 对主线程的超时占用问题。Stack Reconciler 是一个同步的递归过程,使用的是 JavaScript 引擎自身的函数调用栈,它会一直执行到栈空为止,所以当 React 在渲染组件时,从开始到渲染完成整个过程是一气呵成的。如果渲染的组件比较庞大,js 执行会占据主线程较长时间,会导致页面响应度变差。

而且所有的任务都是按照先后顺序,没有区分优先级,这样就会导致优先级比较高的任务无法被优先执行。

从编码角度来看,Fiber 是 React 内部所定义的一种数据结构,它是 Fiber 树结构的节点单位,也就是 React 16 新架构下的虚拟 DOM。一个 fiber 就是一个 JavaScript 对象,Fiber 的数据结构如下:

ts
type Fiber = {
  // 用于标记fiber的WorkTag类型,主要表示当前fiber代表的组件类型如FunctionComponent、ClassComponent等
  tag: WorkTag;
  // ReactElement里面的key
  key: null | string;
  // ReactElement.type,调用`createElement`的第一个参数
  elementType: any;
  // The resolved function/class/ associated with this fiber.
  // 表示当前代表的节点类型
  type: any;
  // 表示当前FiberNode对应的element组件实例
  stateNode: any;

  // 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回
  return: Fiber | null;
  // 指向自己的第一个子节点
  child: Fiber | null;
  // 指向自己的兄弟结构,兄弟节点的return指向同一个父节点
  sibling: Fiber | null;

  index: number;

  ref: null | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject;

  // 当前处理过程中的组件props对象
  pendingProps: any;
  // 上一次渲染完成之后的props
  memoizedProps: any;

  // 该Fiber对应的组件产生的Update会存放在这个队列里面
  updateQueue: UpdateQueue<any> | null;

  // 上一次渲染的时候的state
  memoizedState: any;

  // 一个列表,存放这个Fiber依赖的context
  firstContextDependency: ContextDependency<mixed> | null;

  mode: TypeOfMode;

  // Effect
  // 用来记录Side Effect
  effectTag: SideEffectTag;

  // 单链表用来快速查找下一个side effect
  nextEffect: Fiber | null;

  // 子树中第一个side effect
  firstEffect: Fiber | null;
  // 子树中最后一个side effect
  lastEffect: Fiber | null;

  // 代表任务在未来的哪个时间点应该被完成,之后版本改名为 lanes
  expirationTime: ExpirationTime;

  // 快速确定子树中是否有不在等待的变化
  childExpirationTime: ExpirationTime;

  // fiber的版本池,即记录fiber更新过程,便于恢复
  alternate: Fiber | null;
};

Fiber 实现原理是 requestIdleCallback(浏览器空闲时执行一些任务) 这一API,但 React 团队 polyfill 了这个 API,使其对比原生的浏览器兼容性更好且拓展了特性。

每个 Fiber 节点有个对应的 React element,多个 Fiber 节点是如何连接形成树呢?靠如下三个属性:

js
// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;
介绍下 React 中的 diff 算法

diff 算法主要基于三个规律:

  • DOM 节点的跨层级移动的操作特别少,可以忽略不计
  • 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构
  • 对于同一层级的一组子节点,可以通过唯一的 id 进行区分
  • tree diff: 因为上面的三个策略中的第一点, DOM 节点的跨级操作比较少,那么 diff 算法只会对相同层级的 DOM 节点进行比较。如果发现节点不存在 那么会将该节点以及其子节点完全删除,不会再继续比较。如果出现了 DOM 节点的跨层级的移动操作,那么会删除改节点以及其所有的子节点,然后再移动后的位置重新创建。

  • component diff: 如果是同一类型的组件,那么会继续对比 VM 数;如果不是同一类型的组件,那么会将其和其子节点完全替换,不会再进行比对;同一类型的组件,有可能 VM 没有任何的变化,如果可以确定的知道这点,那么就可以节省大量的 diff 时间,所以用户可以设置 shouldComponentUpdate() 来判断是否需要进行 diff 算法。

  • element diff:当节点处于同一层级的时候时,有三种操作:INSERT_MAKEUP 插入、 MOVE_EXISTING 移动、 REMOVE_NODE 删除。这里 React 有一个优化策略,对于同一层级的同组子节点,添加唯一的 key 进行区分。这样的话,就可以判断出来是否是移动节点。通过 key 发现新旧集合中的节点都是相同的节点,就只需要进行移动操作就可以。

useEffect 是如何区分生命周期钩子的 ?

useEffect 可以看成是 componentDidMountcomponentDidUpdatecomponentWillUnmount 三者的结合。

useEffect(callback, [source]) 接收两个参数,调用方式如下:

jsx
useEffect(() => {
  console.log("mounted");

  return () => {
    console.log("willUnmount");
  };
}, [source]);

生命周期函数的调用主要是通过第二个参数[source]来进行控制,有如下几种情况:

  • [source]参数不传时,则每次都会优先调用上次保存的函数中返回的那个函数,然后再调用外部那个函数;
  • [source]参数传[]时,则外部的函数只会在初始化时调用一次,返回的那个函数也只会最终在组件卸载时调用一次;
  • [source]参数有值时,则只会监听到数组中的值发生变化后才优先调用返回的那个函数,再调用外部的函数。
React 中的 VM 一定会提高性能吗?

不一定,因为 VM 只是通过 diff 算法避免了一些不需要变更的 DOM 操作,最终还是要操作 DOM 的,并且 diff 的过程也是有成本的。

对于某些场景,比如都是需要变更 DOM 的操作,因为 VM 还会有额外的 diff 算法的成本在里面,所以 VM 的方式并不会提高性能,甚至比原生 DOM 要慢。

没有任何框架可以比纯手动的优化 DOM 操作更快。

react 的虚拟 dom 是怎么实现的?

React 是把真实的 DOM 树转换为 JS 对象树,也就是 Virtual DOM。每次数据更新后,重新计算 VM,并和上一次生成的 VM 树进行对比,对发生变化的部分进行批量更新。除了性能之外,VM 的实现最大的好处在于和其他平台的集成。

比如我们一个真是的 DOM 是这样的:

html
<button class="hero">
  <span>fly</span>
</button>

那么在转化为 VM 之后就是这样的:

js
{
  type: 'button',
  props: {
  	className: 'hero',
    children: [{
      type: 'span',
      props: {
        type: 'text'
        children: 'fly'
      }
    }]
  }
}
React 的事件代理机制和原生事件绑定有什么区别?
  • 事件传播与阻止事件的传播: React 的合成事件并没有实现事件捕获只支持了事件冒泡。阻止事件传播 React 做了兼容性处理,只需要 e.preventDefault() 即可,原生存在兼容性问题。

  • 事件类型:React 是原生事件类型的一个子集(React 只是实现了 DOM level3 的事件接口,有些事件 React 并没有实现,比如 window 的 resize 事件。)阻止 React 事件冒泡的行为只能用于 React 合成事件系统,但是在原生事件中的阻止冒泡行为,却可以阻止 React 合成事件的传播。

  • 事件的绑定方式:原生事件系统中支持多种不同的绑定事件的方式,React 中只有一种

  • 事件对象:原生中存在 IE 的兼容性问题,React 做了兼容处理。

在 shouldComponentUpdate 或 componentWillUpdate 中使用 setState 会发生什么?

当调用 setState 的时候,实际上会将新的 state 合并到状态更新队列中,并对 partialState 以及 pendingStateQueue 更新队列进行合并操作。最终通过 enqueueUpdate 执行 state 更新。

如果在 shouldComponentUpdate 或 componentWillUpdate 中使用 setState,会使得 state 队列(pendingStateQueue)不为 null,从而调用 updateComponent 方法,updateComponent 中会继续调用 shouldComponentUpdatecomponentWillUpdate,因此造成死循环

React 中为什么要给组件设置 key?

在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。在 React Diff 算法中 React 会借助元素的 Key 值来判断该元素是新创建的还是被移动而来的元素,从而减少不必要的元素重新渲染。

setState 之后发生了什么?

React 利用状态队列机制实现了 setState 的异步更新,避免频繁的重复更新state。首先将新的 state 合并到状态更新队列中,然后根据更新队列和 shouldComponentUpdate 的状态来判断是否需要更新组件。

详细过程:

  • enqueueSetState 将 state 放入队列中,并调用 enqueueUpdate 处理要更新的 Component
  • 如果组件当前正处于 update 事务中,则先将 Component 存入 dirtyComponent 中。否则调用 batchedUpdates 处理。
  • batchedUpdates 发起一次 transaction.perform() 事务 开始执行事务初始化,运行,结束三个阶段
    • 初始化:事务初始化阶段没有注册方法,故无方法要执行
    • 运行:执行 setSate 时传入的 callback 方法
    • 结束:更新 isBatchingUpdates 为 false,并执行 FLUSH_BATCHED_UPDATES 这个 wrapper 中的 close 方法,FLUSH_BATCHED_UPDATES 在 close 阶段,会循环遍历所有的 dirtyComponents,调用 updateComponent 刷新组件,并执行它的 pendingCallbacks, 也就是 setState 中设置的 callback。
React.PureComponent 和 React.Component 有什么区别?

PureComponent 和 Component 的区别是:

  • Component 需要手动实现 shouldComponentUpdate
  • PureComponent 通过浅对比默认实现了 shouldComponentUpdate 方法。

浅比较(shallowEqual),即 react 源码中的一个函数,然后根据下面的方法进行是不是 PureComponent 的判断,帮我们做了本来应该我们在 shouldComponentUpdate 中做的事情。

js
if (this._compositeType === CompositeTypes.PureClass) {
  shouldUpdate =
    !shallowEqual(prevProps, nextProps) || !shallowEqual(inst.state, nextState);
}
constructor 中 super 与 props 参数一起使用的目的是什么?

在调用方法之前,子类构造函数无法使用 this 引用 super()。

在 ES6 中,在子类的 constructor 中必须先调用 super 才能引用 this

在 constructor 中可以使用 this.props

  • 使用props:
jsx
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    console.log(this.props); // Prints { name: 'sudheer',age: 30 }
  }
}
  • 不使用props:
jsx
class MyComponent extends React.Component {
  constructor(props) {
    super();
    console.log(this.props); // Prints undefined
    // But Props parameter is still available
    console.log(props); // Prints { name: 'sudheer',age: 30 }
  }

  render() {
    // No difference outside constructor
    console.log(this.props); // Prints { name: 'sudheer',age: 30 }
  }
}

Powered by VitePress.