2024React高频面试题

2,711 阅读1小时+

一、React组件通信的方式有哪些?

  • ⽗组件向⼦组件通讯: ⽗组件可以向⼦组件通过传 props 的⽅式,向⼦组件进⾏通讯

  • ⼦组件向⽗组件通讯: props+回调的⽅式,⽗组件向⼦组件传递props进⾏通讯,此props为作⽤域为⽗组件⾃身的函 数,⼦组件调⽤该函数,将⼦组件想要传递的信息,作为参数,传递到⽗组件的作⽤域中。

  • 兄弟组件通信: 找到这两个兄弟节点共同的⽗节点,进行状态提升,结合上⾯两种⽅式由⽗节点转发信息进⾏通信

  • 跨层级通信:

    • props层层传递回调函数。
    • Context 设计⽬的是为了共享那些对于⼀个组件树⽽⾔是“全局”的数据,例如当前认证的⽤户、主题或⾸选语⾔,对于跨越多层的全局数据通过 Context 通信再适合不过。
  • 非嵌套关系的组件通信:

    • (发布订阅模式): 发布者发布事件,订阅者监听事件并做出反应,我们可以通过引⼊event模块进⾏通信
    • (全局状态管理⼯具): 借助Redux或者Mobx等全局状态管理⼯具进⾏通信,这种⼯具会维护⼀个全局状态中⼼Store,并根据不同的事件产⽣新的状态

二、哪些方法会触发 React 重新渲染以及如何避免不必要的重新渲染?

  • 触发 React 重新渲染:

    • setState() 、useState被调用
    • props发生变化
    • 组件依赖的上下文 context 发生变化
    • 自定义hook发生变化
    • forceUpdate强制组件重新渲染
  • 如何避免不必要的重新渲染:

    • React.memo:包裹函数式组件,react渲染之前会先做属性值的对比,如果没有变化,则不重新渲染。
    • useMemo:缓存计算结果,只有在依赖项变化时才重现计算结果,而不是每次都渲染,避免因为计算开销大会导致渲染时间长。
    • useCallback:缓存函数,只有在依赖项变化时才重现创建函数,而不是每次都创建。
    • React.PureComponent/shouldComponentUpdate:控制是否重新渲染类组件。
    • key:增加key,提升就进复用率。

三、受控组件和非受控组件的区别?

  • 受控组件: 组件的展示完全由传入的属性决定。比如说,如果一个输入框中的值完全由传入的 value 属性决定,而不是由用户输入决定,那么就是受控组件,写法是:

  • 非受控组件: 表单组件可以有自己的内部状态,而且它的展示值是不受控的。比如 input 在非受控状态下的写法是:< input onChange={handleChange}/>。

    • 使用场景:1.必须手动操作DOM的 2.文件上传 3.富文本编辑,需要传入DOM元素

四、高阶组件是什么,和普通组件有什么区别?适用什么场景?

  • 概念:高阶组件(HOC)就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件,它只是一种组件的设计模式,这种设计模式是由react自身的组合性质必然产生的。我们将它们称为纯组件,因为它们可以接受任何动态提供的子组件,但它们不会修改或复制其输入组件中的任何行为。
  • 适用场景:
  • 代码复用,逻辑抽象:当多个组件有相似的逻辑时,可以使用高阶组件来共享这些逻辑。
  • 渲染劫持: 高阶组件可以读取、修改、甚至替换被包裹组件的元素树,React中权限封装,无权限返回替换。
  • State和Props的抽象、更改:连接 Redux 或 MobX,高阶组件可以用来包裹组件并连接到应用的状态管理库。

五、为什么React自定义组件首字母要大写?

  • JSX 解析规则:在 JSX 中,React 使用首字母大小写来区分自定义组件和原生 HTML 标签。首字母大写被解释为自定义组件,而首字母小写则被视为普通的 HTML 标签。
  • Babel 转换过程:当使用 Babel 转换 JSX 代码时,它根据元素的首字母大小写来决定如何转换。首字母大写的组件会被保留其名称并传递给 React.createElement,而小写字母开头的则被视为普通 HTML 标签并转换为对应的字符串。
  • 组件渲染识别: 这种首字母大小写的约定让 React 可以正确识别和渲染自定义组件和原生 DOM 元素,保证了 JSX 的灵活性和表现力。
  • 代码清晰和一致性: 遵循这个约定有助于维持代码的清晰性和一致性,使得代码的阅读和维护变得更加容易。

React.createElement 的基本语法如下:

React.createElement(type, [props], [...children])
  • type: 这个参数是一个字符串(对于 HTML 或 SVG 标签)或一个 React 组件(可以是类组件或函数组件)。这决定了将要创建的元素类型。
  • props: 这是一个对象,包含了传递给 React 元素的属性(或“props”)。对于 HTML 标签,这些属性就是常见的 HTML 属性;对于 React 组件,这些属性将会成为组件的 props。
  • children: 这些参数代表元素的子元素。它们可以是字符串、数字、React 元素,或者这些类型的数组。这里也可以传递 null 或 undefined,表示没有子元素。
const element = React.createElement('div', { className: 'container' }, 'Hello, world!');

六、Redux模块

1.Redux的工作流程是什么?

    1. 通过createStore,生成Store
    1. 用户定义action行为,并使用dispatch发起action
    1. 当前Store中对应的reducer会处理Action返回新的State
    1. State—旦有变化,Store就会调用监听(subscribe)函数,来更新View

2.说一说你理解的Redux中间件机制?

在 Redux 中,中间件的作用类似于服务器框架(如 Express 或 Koa)中的中间件。它们提供了一种方式来增强 Redux 的 dispatch 函数。通过使用中间件,我们可以在 action 被发送到 reducer 之前执行额外的操作,例如:

  • 日志记录:记录每个 action 的信息和状态变化。
  • 异步操作:处理异步逻辑,例如 API 请求。
  • 错误处理:捕获和上报异常信息。
中间件的实现

中间件的基本结构是一个嵌套的三层高阶函数,这种结构允许中间件访问到 Redux 的 dispatch 和 getState 方法,以及下一个中间件。以下是一个简单的日志记录中间件的例子:

function logger(store) {
  return function(next) {
    return function(action) {
      console.log('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      return result;
    }
  }
}

在这个例子中,logger 函数接收 store 对象,并返回一个函数。这个返回的函数接收下一个中间件作为参数 next,然后再返回一个新的函数,这个新函数接收 action 并进行处理。

应用中间件

为了在 Redux 应用中使用中间件,我们需要使用 applyMiddleware 函数。这个函数允许我们将多个中间件绑定到 Redux store。下面是如何在 Redux store 中应用中间件的例子:

import { createStore, applyMiddleware } from 'redux';
import rootReducer from './reducers';
​
const store = createStore(
  rootReducer,
  applyMiddleware(logger, /* 其他中间件 */)
);

3.Redux 中异步的请求怎么处理?

redux-thunk

优点:

  • 体积⼩: redux-thunk的实现⽅式很简单,只有不到20⾏代码

  • 使⽤简单: redux-thunk没有引⼊像redux-saga或者redux-observable额外的范式,上⼿简单

    缺点:

  • 样板代码过多: 与redux本身⼀样,通常⼀个请求需要⼤量的代码,⽽且很多都是重复性质的。

  • 耦合严重: 异步操作与redux的action偶合在⼀起,不⽅便管理。

  • 功能孱弱: 有⼀些实际开发中常⽤的功能需要⾃⼰进⾏封装

例子:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
​
const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);
const fetchSomeData = () => {
  return (dispatch) => {
    dispatch({ type: 'FETCH_DATA_REQUEST' });
    fetch('https://some-api.com/data')
      .then(response => response.json())
      .then(data => dispatch({ type: 'FETCH_DATA_SUCCESS', payload: data }))
      .catch(error => dispatch({ type: 'FETCH_DATA_FAILURE', error }));
  };
};
redux-saga

优点:

  • 异步解耦:把异步操作单独分离出来放在saga文件中。当我们提交普通action的时候,如果匹配到了saga文件中的监听器就会被拦截下来,然后调用saga里配置的方法进行异步操作。如果没匹配上就走提交普通action的逻辑。总体来说逻辑较为清晰,但是使用成本增加。
  • 异常处理: 受益于 generator function 的 saga 实现,代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理
  • 功能强⼤: redux-saga提供了⼤量的Saga 辅助函数和Effect 创建器供开发者使⽤,开发者⽆须封装或者简单封装即可使⽤。
  • 灵活: redux-saga可以将多个Saga可以串⾏/并⾏组合起来,形成⼀个⾮常实⽤的异步flow

缺点:

  • 额外的学习成本: redux-saga不仅在使⽤难以理解的 generator function,⽽且有数⼗个API,学习成本远超redux-thunk
  • 体积庞⼤: 体积略⼤,代码近2000⾏,min版25KB左右
  • 功能过剩: 实际上并发控制等功能很难⽤到,但是我们依然需要引⼊这些代码
介绍一下redux-toolkit

Redux Toolkit 是 Redux 官方推荐的工具集,旨在简化 Redux 应用的编写和维护。它提供了一系列工具和函数,帮助开发者以更简洁和模块化的方式编写 Redux 代码。Redux Toolkit 主要解决了 Redux 在使用过程中的一些常见问题,比如繁琐的设置、过多的样板代码以及复杂的配置。 主要特性

  1. 配置 Store 简化:提供 configureStore() 方法来自动设置 store。这个方法默认集成了常见的中间件,简化了 store 的配置。
  2. 创建 Reducer 和 Action 简化:通过 createSlice() 方法,可以在一个函数中同时定义 reducer 和相关的 actions。这减少了样板代码,并使得逻辑更加集中。
  3. 不可变性管理:内置了 Immer 库,允许在 reducer 中以更直观的方式修改 state,而无需手动保证 state 的不可变性。
  4. 异步逻辑处理:提供 createAsyncThunk 函数用于处理异步逻辑,使得在 Redux 应用中处理异步操作变得更简单。
  5. 扩展性和可重用性:鼓励将 Redux 逻辑切割成更小的片段,通过提供的 API,这些片段可以轻松重用和测试。

创建Redux-Store

// store.js
import { configureStore } from '@reduxjs/toolkit';
import userReducer from '../features/userSlice';
​
export const store = configureStore({
  reducer: {
    user: userReducer,
  },
});

创建 Slice 使用 createSlice 来同时定义 reducer 和 actions:

// features/userSlice.js
import { createSlice } from '@reduxjs/toolkit';
​
export const userSlice = createSlice({
  name: 'user',
  initialState: {
    value: 0,
  },
  reducers:
      {
        increment: state => {
          // Immer 让我们直接 "修改" state
          state.value += 1;
        },
        decrement: state => {
          state.value -= 1;
        },
        incrementByAmount: (state, action) => {
          state.value += action.payload;
        },
      },
  });
// 自动生成 action creators
export const { increment, decrement, incrementByAmount } = userSlice.actions;
​
export default userSlice.reducer;
​
在这个示例中,`createSlice` 自动为每个 reducer 函数生成对应的 action creator 和 action type。这样可以减少需要编写和维护的代码量。
#### 在组件中使用 Redux Toolkit
然后,可以在 React 组件中使用这些 slices 和生成的 actions:
// App.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount } from './features/userSlice';
​
function App() {
  const count = useSelector(state => state.user.value);
  const dispatch = useDispatch();
​
  return (
    <div>
      <div>{count}</div>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
      <button onClick={() => dispatch(incrementByAmount(2))}>Increment by 2</button>
    </div>
  );
}
​
export default App;
​

4.Redux 请求中间件如何处理并发?

使用redux-Saga处理并发 Redux-Saga 使用 ES6 Generator 函数来管理副作用(如数据获取、访问浏览器缓存等)。对于并发请求,可以使用 all 效果指令(Effect)来同时触发多个任务。

import { call, put, all } from 'redux-saga/effects';
​
// 模拟两个异步请求的函数
function fetchUser() {
  return fetch('/api/user').then(response => response.json());
}
​
function fetchPosts() {
  return fetch('/api/posts').then(response => response.json());
}
​
// Saga 处理两个并发请求
function* fetchUserAndPosts() {
  try {
    // 使用 all 同时发起请求
    const [user, posts] = yield all([
      call(fetchUser),
      call(fetchPosts)
    ]);
​
    // 当两个请求都完成时,dispatch actions
    yield put({ type: 'FETCH_USER_SUCCESS', payload: user });
    yield put({ type: 'FETCH_POSTS_SUCCESS', payload: posts });
  } catch (error) {
    // 如果任何一个请求失败,dispatch 一个失败的 action
    yield put({ type: 'FETCH_FAILED', error });
  }
}

5.Redux 和 Vuex 有什么区别,它们的共同思想?

Redux Redux 是一个用于 JavaScript 应用的状态管理库,广泛用于 React(也可以用于其他框架或库):

  • 单一状态树(Store):Redux 使用一个单一的、不可变的状态树来存储整个应用的状态。
  • Action:状态更改是通过发送(dispatch)action 来表示的。Action 是描述“发生了什么”的普通 JavaScript 对象。
  • Reducer:Reducer 函数决定如何根据 action 更改状态。Reducer 必须是纯函数,它接收前一个状态和一个 action,返回新状态。
  • 中间件:Redux 支持中间件,用于处理副作用(如异步操作或日志记录)。

Vuex Vuex 是专为 Vue.js 设计的状态管理模式和库:

  • 状态管理(State):Vuex 也使用单一状态树,存储整个应用的状态。
  • Mutation:更改 Vuex 的状态的唯一方法是通过提交(commit)mutation。Mutation 必须是同步函数。
  • Action:Action 类似于 Redux 中的 action,用于提交 mutation。不同的是,它们可以包含异步操作。
  • Module:Vuex 允许将 store 分割成模块,每个模块可以拥有自己的状态、mutation、action 和 getter。

共同思想 尽管 Redux 和 Vuex 的实现细节有所不同,但它们在状态管理方面有着共同的理念:

  • 集中式状态管理:它们都提倡将应用的状态集中存储在单一的状态树中,这使得状态更容易追踪和调试。
  • 可预测性:通过明确的规则和模式(如 Redux 的 reducer 或 Vuex 的 mutation)来管理状态更改,使得应用的状态变得可预测和一致。
  • 维护性和组织性:通过提供一种清晰的方式组织代码和管理数据流,帮助开发者构建大型、复杂的应用程序。

主要区别

  • 框架依赖:Redux 是一个独立的库,可以与任何 UI 层一起使用,但通常与 React 一起使用;Vuex 专为 Vue.js 设计。
  • 异步操作:Redux 本身不支持异步操作,需要中间件(如 redux-thunk 或 redux-saga);Vuex 的 action 支持异步操作。
  • API 和设计哲学:Redux 倾向于函数式编程风格,Vuex 更紧密地集成了 Vue.js 的响应式系统。

6.Redux的缺点:

ca8b851ae1ff83ec761de86d5b5c7b1.png 1)繁重的代码模板:修改一个state可能要动四五个文件,可谓牵一发而动全身。 2)store 里状态残留:多组件共用 store 里某个状态时要注意初始化清空问题。 3)无脑的发布订阅:每次 dispatch 一个 action 都会遍历所有的reducer,重新计算 connect,这无疑是一种损耗; 4)交互频繁时会有卡顿:如果 store 较大时,且频繁地修改 store,会明显看到页面卡顿。 5)不支持Typescript。

七、虚拟 DOM

1.VDom是什么?

虚拟 DOM 的概念:

  1. 轻量级的 JavaScript 对象:虚拟 DOM 是真实 DOM 的一种抽象,它是由普通的 JavaScript 对象组成的树结构。每个 JavaScript 对象都对应着真实 DOM 树中的一个节点。
  2. 同步与真实 DOM 的变化:在 React 应用中,每当组件的状态变化时,React 会先在虚拟 DOM 上进行相应的更新。这意味着每次状态变化并不会直接引起真实 DOM 的更新。

2.为什么要用虚拟dom?

  1. 性能提升: 保证性能下限,在不进行手动优化的情况下,提供过得去的性能。操作真实 DOM 是昂贵的(性能开销较大),因为它会导致浏览器的重绘和重排。虚拟 DOM 允许 React 在内存中进行所有的计算,减少了直接操作真实 DOM 的次数,从而提升性能。
  2. 跨平台: 虚拟 DOM 是平台无关的,这意味着相同的组件可以在不同的环境中渲染,如浏览器、服务器(SSR)或原生应用(React Native)。

3.虚拟 DOM 的引入与直接操作原生 DOM 相比,哪一个效率更高,为什么?

在整个 DOM 操作的演化过程中,其实主要矛盾并不在于性能,而在于开发体验和开发效率。虚拟 DOM 并不一定会带来更好的性能,React 官方也从来没有把虚拟 DOM 作为性能层面的卖点对外输出过。虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式的同时,仍然保持一个还不错的性能。 总结:

  1. 开发体验与效率:虚拟 DOM 主要提升了开发体验和效率。它允许开发者通过更声明式的编程方式来管理 UI,专注于数据和状态,而不是复杂的 DOM 操作。这种方法简化了代码,提高了可维护性和开发效率,特别是在构建大型和复杂的应用时。
  2. 性能的平衡和适用性:虚拟 DOM 在性能方面提供了一种平衡。它通过减少不必要的 DOM 操作和优化批量更新,避免了一些性能瓶颈。虽然在某些简单操作中,直接操作原生 DOM 可能更快,但在处理大量数据和复杂的 UI 更新时,虚拟 DOM 可以提供更稳定和预测性更强的性能。虚拟 DOM 的性能优势并非绝对,而是依赖于特定的应用场景和需求。
  3. React 官方的立场:React 官方从未声称虚拟 DOM 主要是为了性能优化。相反,React卖点强调的是组件化开发和声明式 UI,这提高了开发的灵活性和效率。

八、Dom Diff 算法

1.Dom Diff是什么?

React 在执行 render 过程中会产生新的虚拟 DOM, 在浏览器平台下, 为了尽量减少 DOM 的创建, React 会对新旧虚拟 DOM 进行 diff 算法找到它们之间的差异, 尽量复用 DOM 从而提高性能; 所以 diff 算法主要就是用于查找新旧虚拟 DOM 之间的差异。

2.大致流程:

对新旧两棵树做深度优先遍历,避免对两棵树做完全比较,因此算法复杂度可以达到 O(n)。然后给每个节点生成一个唯一的标志。 在遍历的过程中,每遍历到一个节点,就将新旧两棵树作比较,并且只对同一级别的元素进行比较:

  • 只进行同一层级的比较,如果跨层级的移动则视为创建和删除操作。
  • 如果是不同类型的元素,则认为是创建了新的元素,而不会递归比较他们的孩子。
  • 如果是列表元素等比较相似的内容,可以通过key来唯一确定是移动还是创建或删除操作。

3.React diff 算法具体策略:

1.tree diff:同级元素比较:

react 会对 fiber 树进行分层比较,只比较同级元素,当出现节点跨层级移动时,并不会出现想象中的移动操作,而是将旧节点删除,然后重新创建新节点。 1705392936152.jpg

2.component diff:组件之间的比较
  • 对同种类型组件对比,按照层级比较继续比较虚拟DOM树即可,但有种特殊的情况,当组件A如果变化为组件B的时候,有可能虚拟DOM并没有任何变化,所以用户可以通过shouldComponentUpdate() 来判断是否需要更新,判断是否计算。
  • 对于不同组件来说,React会直接判定该组件为Dirty component(脏组件),无论结构是否相似,只要判断为脏组件就会直接替换整个组件的所有节点。
  • 更新过程: 先在新节点下面创建新的元素,创建完成后,删除老节点下面的变动元素。

1705393846638.jpg

3.element diff:节点比较:

element 同一层级的节点的比较规则, 根据每个节点在对应层级的唯一 key 作为标识, 并且对于同一层级的节点操作只有 3 种, 分别为 INSERT_MARKUP(插入)MOVE_EXISTING(移动)REMOVE_NODE(删除)

  • 插入(INSERT_MARKUP): 如果是全新的节点,需要对新节点执行插入操作。
  • 移动(MOVE_EXISTING): 新节点某个类型组件或元素节点存在旧节点里,通过key来进行直接移动复用。
  • 删除(REMOVE_NODE): 旧节点中某个组件或节点类型在新节点中也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者旧组件或节点不在新节点里的,也需要执行删除操作。

1705394372990.jpg 注意事项:

  1. key 的值必须保证 唯一 且 稳定, 有了 key 属性后, 就可以与组件建立了一种对应关系, react 根据 key 来决定是销毁还是重新创建组件, 是更新还是移动组件。
  2. index 的使用存在的问题: 大部分情况下可能没有啥问题, 但是如果涉及到数据变更(更新、新增、删除), 这时 index 作为 key 会导致展示错误的数据, 其实归根结底, 使用 index 的问题在于两次渲染的index 是相同的, 所以组件并不会重新销毁创建, 而是直接进行更新。

九、JSX本质是什么?

1.概念:

JSX 是** JavaScript 的扩展 ,浏览器没办法解析原始的JSX代码,需要通过Babel转译**,Babel会把JSX 中标签都转换为 React.createElement 函数调用,JSX 就是 React.createElement 函数调用的语法糖。

2.处理流程:

  1. 调用 createElement,对用户传入数据进行处理,并调用ReactElement函数。
  2. 调用ReactElement函数,返回ReactElement对象,即虚拟 DOM。
  3. 调用ReactDOM.render方法,将虚拟DOM转换为真实DOM。

3.fiber架构下,jsx怎么转换为真实dom?

  1. JSX 到 render function 的转换:React 使用 JSX 来描述页面。在构建过程中,Babel 编译器将 JSX 转换为 render 函数。
  2. 生成虚拟 DOM(VDOM) :当 render 函数执行时,它产生虚拟 DOM(VDOM)。虚拟 DOM 是对真实 DOM 的轻量级表示,用于描述页面的结构和内容。
  3. 虚拟 DOM 转换为 Fiber 节点:在 Fiber 架构中,虚拟 DOM 不会直接用于渲染。相反,它首先被转换为 Fiber 节点。这些 Fiber 节点包含了额外的信息和结构,使得 React 能够更有效地管理更新过程。
  4. 协调(Reconciliation)过程:将虚拟 DOM 转换为 Fiber 节点的过程称为协调(Reconciliation)。在协调过程中,React 会比较新旧虚拟 DOM,确定实际需要在真实 DOM 上执行的更新。
  5. Commit 阶段:一旦所有必要的变更被确定,React 会在 Commit 阶段一次性将这些变更应用到真实的 DOM 上。这包括创建、更新或删除 DOM 节点。
  6. 中断和恢复:与早期 React 版本相比,Fiber 架构的一个关键特性是其渲染过程可以被中断和恢复。这允许 React 根据需要暂停渲染工作,以确保高优先级的任务(如用户输入)能够及时处理。

十、Hooks相关专栏

1.Hooks解决了什么问题?

  • 更好的逻辑组织和代码分离:Hooks 允许开发者按照逻辑关系而非生命周期方法来组织代码,这使得相关代码更加集中和一致,提高了代码的可维护性。
  • Hoc高阶组件,render props使得使组件结构复杂化。复杂组件变得难以理解。
  • 在函数组件中使用状态和其他 React 特性: 扩展了函数组件的功能。

2.useMemo 和 useCallback 有什么区别?

useMemo
  • useMemo 用于对复杂的计算结果进行记忆化。当你有一段计算逻辑,但只希望在特定的依赖项改变时才重新计算时,可以使用 useMemo。
  • 它接受一个“创建”函数和一个依赖项数组。只有当依赖项发生变化时,这个“创建”函数才会被执行。
  • useMemo 返回的是“创建”函数的返回值。
const computedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useCallback
  • useCallback 用于对函数本身进行记忆化。当你将函数作为 props 传递给子组件,或者在依赖于函数的 useEffect 中使用函数时,使用 useCallback 可以防止组件不必要的重新渲染。
  • 它接受一个内联函数和一个依赖项数组。只有当依赖项发生变化时,这个函数才会被重新创建。
  • useCallback 返回的是记忆化的函数本身。
const memoizedCallback = useCallback(() => {
 return doSomething(a, b);
}, [a, b]);

3.useEffect 和 useLayoutEffect 有什么区别?

  • useEffect 的执行时机是组件渲染到屏幕(render)之后延迟执行,而 useLayoutEffect 的执行时机是在所有的DOM变更之后同步调用effect。
  • useEffect中的延迟执行,指的是effect会在组件渲染任务调度函数结束后,再单独调用一次任务调度函数,好处是,effect的调用可以单独进行,不会加长组件渲染的任务时间,也就不会阻碍组件的渲染了。
  • useLayoutEffect的同步,但它在 DOM 更新完成后立即同步执行,也就是在浏览器进行任何绘制之前。这意味着你可以在浏览器绘制前读取布局并同步重新渲染。由于 useLayoutEffect 会阻塞浏览器的绘制,不当使用可能会导致性能问题。

总结来说,两者的主要区别在于执行时机:useEffect 在所有的 DOM 更改之后异步执行不会阻塞页面渲染,而 useLayoutEffect 则在 DOM 更新之后立即同步执行,适用于对 DOM 布局和样式有影响的操作。在大多数情况下,useEffect 已足够使用,但在需要同步更改 DOM 或者避免闪烁时,应该使用 useLayoutEffect。

4.useEffect:执行副作用:

使用方法:

  • 副作用函数:在组件渲染到屏幕之后执行。
  • 依赖项数组:指定了 useEffect 执行的依赖。如果依赖项没有改变,副作用函数在重新渲染时不会再次执行。
useEffect(() => {
  fetch('some-api').then(response => {
    // 处理响应
  });
}, []); // 空数组表示只在组件挂载时执行一次
  • 如果不传递第二个参数(依赖项数组),副作用函数将在每次渲染后都执行。
  • 如果依赖项数组为空([]),这意味着副作用函数仅在组件第一次渲染(挂载)后执行一次,并且在组件卸载时执行清理(如果提供了清理函数)
  • 如果依赖项数组中包含变量或属性,副作用函数将在这些依赖项改变时执行。

5.useEffect 是怎么判断依赖项变化的?

useEffect的依赖项是使用了 === 全等符号来进行判断的,如果依赖项是数组或者对象,即使值没有变化,引用地址也会变化,所以每次判断的结果都不一样,就会每次都会调用回调函数了。 由于这种浅比较的机制,当使用对象或数组作为依赖项时,需要特别注意。如果这些对象或数组在每次组件渲染时都被重新创建,即使它们包含的数据实际上没有变化,useEffect 也会重复执行。为了避免这种情况,可以使用 useMemo 或 useCallback 来缓存这些对象或函数,以确保它们的引用在渲染之间保持不变。

6.useState为什么返回数组而不是对象?

如果useState返回的是数组,你可以按照自己的想法对变量进行命名,代码看起来也会更加干净。而如果是对象,你的命名必须要和useState 内部实现返回的对象同名,比较麻烦,而且如果你要多次使用 useState ,就必须得重命名返回值。 避免解构时的命名冲突、灵活的命名避免记忆负担、遵循 Hooks 设计的简洁性

7.useState和useReducer有啥区别?

  • useState和useReducer都是用于函数组件内部定义状态的,区别在于,useState用于简单的状态管理和局部状态更新,而useReducer用于复杂的状态逻辑和全局状态管理。
  • useState实际是一个自带了reducer功能的useReducer语法糖。
  • 当你使用useState时,如果state没有发生变化,那么组件就不会更新。而使用了useReducer时,在state没有发生变化时,组件依然会更新。大家在使用时候,千万要注意这点的区别。

案例:

import React, { useReducer } from 'react';
​
// 定义 reducer 函数
const countReducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
};
​
function Counter() {
  // 使用 useReducer 初始化状态和派发动作函数
  const [state, dispatch] = useReducer(countReducer, { count: 0 });
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>增加</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>减少</button>
    </div>
  );
}
​
export default Counter;

8.setState到底是同步还是异步?

  • React 18之前,React采用了一种同步和异步处理的机制,进入React调度流程的操作是异步处理,包括合成事件,而未进入调度流程的原生事件(如setTimeout、setInterval)是同步处理。这种同步处理方式可能会导致性能浪费,因为多次调用setState会重复触发多次渲染,即使只需要渲染最后一次的结果。
  • 从React 18开始,通过使用createRoot创建应用,所有事件都会自动进行批量处理,而不再区分同步和异步。这意味着无论是合成事件还是原生事件,都会进入React的调度流程,以实现性能的优化。但如果仍然使用render方法进行渲染,事件处理流程仍然与React 18之前的机制相同,可能会导致不必要的性能问题。react18引入了Automatic Batching(自动批处理机制)。

9.setState 调用的原理:

  1. 调用setState入口函数,入口函数类似一个分发器,根据入参不同,将其分发到不同的功能函数中。
  2. enqueueSetState 方法将新的 state 放进组件的状态队列里,并调用 enqueueUpdate 来处理将要更新的实例对象;
  3. 调用enqueueUpdate函数执行更新。该函数中有个关键对象:batchingStrategy,该对象所具备的isBatchingUpdates 属性直接决定了当下是要走更新流程,还是应该排队等待;如果轮到执行,就调用 batchedUpdates 方法来直接发起更新流程。

10.setState的第二个参数作用是什么?

setState 的第二个参数是一个可选的回调函数。这个回调函数将在组件重新渲染后执行。等价于在 componentDidUpdate 生命周期内执行。

11.useRef:在多次渲染之间共享数据:

定义: 我们可以把 useRef 看作是在函数组件之外创建的一个容器空间。在这个容器上,我们可以通过唯一的 current 属设置一个值,从而在函数组件的多次渲染之间共享这个值。

  • 使用 useRef 保存的数据一般是和 UI 的渲染无关的,因此当 ref 的值发生变化时,是不会触发组件的重新渲染的,这也是 useRef 区别于 useState 的地方。
  • 除了存储跨渲染的数据之外,useRef 还有一个重要的功能,就是保存某个 DOM 节点的引用。

12.useContext:定义全局状态

Context 提供了一个方便在多个组件之间共享数据的机制。 缺点:

  • 会让调试变得困难,因为你很难跟踪某个 Context 的变化究竟是如何产生的。
  • 让组件的复用变得困难,因为一个组件如果使用了某个 Context,它就必须确保被用到的地方一定有这个 Context 的 Provider 在其父组件的路径上。

13.React18中为什么要用 createRoot 代替 render?

  1. 优化性能:createRoot 为 React 引入的并发特性打下了基础,使得 React 可以更加智能地调度和优化渲染过程。
  2. 更好的错误处理和警告:新的渲染器提供了更好的错误处理和警告机制,有助于识别和解决开发中的问题。

14.React Hook 的使用限制有哪些?为什么要加这些限制?

限制:

  • 只能在函数组件的最顶层调用 Hooks: 不能在循环、条件语句或嵌套函数内调用 Hooks。这是为了确保 Hooks 在每次组件渲染时都以相同的顺序被调用,这对于 React 正确地追踪 Hook 状态非常重要。
  • 只能在 React 函数组件或自定义 Hooks 中调用 Hooks: 不应在普通的 JavaScript 函数中调用 Hooks。这是为了确保你只在 React 的上下文中使用 Hooks,从而使得状态管理和副作用的处理符合 React 的模式。

为什么要加这些限制? React中每个组件都有一个对应的 FiberNode对象,这个对象上有个属性叫 memoizedState,在函数组件中,fiber.memoizedState存储的就是Hooks单链表,单链表中每个hook节点没有名字和key,只能通过顺序来记录他们的唯一性。 如果在循环、条件或者嵌套中使用hook,当组件更新时,这个hooks顺序会乱套,单链表的稳定性就破坏了。 总结:

  • 无命名和 Key: Hooks 在单链表中是没有名字和 key 的,它们完全依赖于声明的顺序来维持唯一性和状态的连续性。
  • 顺序依赖: React 依赖于 Hooks 被调用的顺序来正确地映射和更新状态。这个顺序在组件的多次渲染之间应该是一致的。

十一、React如何加载异步组件

使用 React.lazy 和 Suspense:

  1. React.lazy: 这是一个函数,允许你定义一个动态加载的组件。它接收一个函数,这个函数必须调用 import() ,返回一个 Promise,该 Promise 需要 resolve 一个 default export 的 React 组件。
  2. Suspense: Suspense 组件用于包裹懒加载的组件。它允许你指定一个加载指示器(比如一个 spinner),这个指示器会在懒加载的组件被加载和渲染之前显示。。。。
import React, { Suspense } from 'react';
​
const LazyComponent = React.lazy(() => import('./LazyComponent'));
​
function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
      </Suspense>
    </div>
  );
}

十二、React事件系统

1.react为什么需要合成事件?

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

2.react合成事件特点:

  1. 我们在 jsx 中绑定的事件(demo中的handerClick,handerChange),根本就没有注册到真实的dom上。是绑定在document上统一管理的。
  2. 真实的dom上的click事件被单独处理,已经被react底层替换成空函数。
  3. react并不是一开始,把所有的事件都绑定在document上,而是采取了一种按需绑定,比如发现了onClick事件,再去绑定document click事件。

3.react事件原理:

react对事件是如何合成的:构建初始化React合成事件和原生事件的对应关系,合成事件和对应的事件处理插件关系。 react事件是怎么绑定的(在react中,我们写了一个button,上面绑定了一个click事件,请问react是怎么处理的?): 第一步,会将你写的click事件,绑定到button对应的fiber树上,就像这样:

// button 对应 fiber
memoizedProps = {
   onClick:function handerClick(){},
   className:'button'
}

第二步,进入diff阶段,会先判断该事件是不是合成事件,如果是合成事件,则会将用户写的事件向document注册。 第三步,在注册事件监听器函数中,先找到 React 合成事件对应的原生事件集合,比如 onClick -> ['click'] , onChange -> [blur , change , input , keydown , keyup],然后遍历依赖项的数组,绑定事件。 第四步,统一对所有事件进行处理,添加事件监听器addEventListener,绑定对应事件。 react事件触发流程:

  1. 首先通过统一的事件处理函数 dispatchEvent,进行批量更新batchUpdate。
  2. 然后执行事件对应的处理插件中的extractEvents,合成事件源对象,每次React会从事件源开始,从上遍历类型为 hostComponent即 dom类型的fiber,判断props中是否有当前事件比如onClick,最终形成一个事件执行队列,React就是用这个队列,来模拟事件捕获->事件源->事件冒泡这一过程。
  3. 最后通过runEventsInBatch执行事件队列,如果发现阻止冒泡,那么break跳出循环,最后重置事件源,放回到事件池中,完成整个流程。 image.png

4.react 17对事件系统的改变:

  1. 事件绑定到根容器上: 事件统一绑定container上,ReactDOM.render(app, container);而不是document上,有利于多个 React 版本共存,例如微前端的场景。 2.原生捕获事件的支持: 支持了原生捕获事件的支持, 对齐了浏览器原生标准。同时 onScroll 事件不再进行事件冒泡。onFocus 和 onBlur 使用原生 focusin, focusout 合成。
  2. 取消事件池 React 17 取消事件池复用,也就解决了在setTimeout打印,找不到e.target的问题。

十三、React-fiber

1.什么是并发?和并行有啥区别?

  • 并发:具备处理多个任务的能力,但不是在同一时刻处理,每次只会处理一个任务,交替处理多个任务。
  • 并行:具备处理多个任务的能力,同一时刻可以处理多个任务。

具体举例:

  • 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
  • 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
  • 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。

2.为什么要推出Fiber 架构?

Fiber 是对 React核心算法(即调和过程)的重写。 首先 React 组件的渲染主要经历两个阶段:

  • 协调阶段(Reconciler): 这个阶段 React 用新数据生成新的虚拟 DOM, 遍历新旧虚拟DOM, 然后通过 Diff 算法, 快速找出需要更新的元素, 放到更新队列中去
  • 渲染阶段(Renderer): 这个阶段 React 根据所在的渲染环境, 遍历更新队列, 将对应元素更新(在浏览器中, 就是更新对应的 DOM 元素)
React 传统的协调机制

在引入 Fiber 之前,React 使用的是一种递归的方式来遍历组件树,对比旧的虚拟 DOM 和新的虚拟 DOM 来确定哪些部分需要更新。这个过程称为协调(Reconciliation)。虽然虚拟 DOM 提高了更新的效率,但这种递归处理方式有一些缺点:

  1. 无法中断:一旦开始,整个虚拟 DOM 的对比过程就必须一气呵成,无法中断。对于大型应用,这可能导致主线程被长时间占用,从而影响到用户的交互体验。
  2. UI阻塞问题:由于无法中断,所有的更新都有相同的优先级,React 无法优化那些更紧急的任务(如动画或用户输入)。

Fiber 是 React 16 中采用的新协调(reconciliation)引擎,主要目标是支持虚拟 DOM 的渐进式渲染。 Fiber 将原有的 Stack Reconciler 替换为 Fiber Reconciler,提高了复杂应用的可响应性和性能。主要通过以下方式达成目标:

  • 对大型复杂任务的分片。
  • 对任务划分优先级,优先调度高优先级的任务。
  • 调度过程中,可以对任务进行暂停、挂起、恢复等操作。

参考:《React Fiber详细解析》

3.Fiber和虚拟DOM的区别?

每一个DOM节点对应一个Fiber对象,Fiber通过多向链表树的形式来记录节点之间的关系,它与传统的虚拟DOM最大的区别是多加了几个属性,通过这种链表的形式,可以很轻松的找到每一个节点的下一个节点或上一个节点,更好的去实现时间切片功能。 React** 用空间换时间**,更高效的操作可以方便根据优先级进行操作。同时可以根据当前节点找到其他节点,在下面提到的挂起和恢复过程中起到了关键作用。

  • return:向上链接整颗树
  • child:向下链接整棵树
  • sibling:横向链接整颗树

链表的好处:

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

链表的缺点:

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

关键词:Fiber节点、链表结构、任务的中断与恢复 区别总结:

  1. 数据结构:在传统的虚拟 DOM 中,React 使用递归的方式处理组件树。这种方式虽然简单,但它不能中断。Fiber 架构通过链表和可中断的任务单元,提供了更灵活的更新机制。
  2. 优先级调度:Fiber 架构允许 React 对不同的更新任务分配不同的优先级。高优先级的任务(如用户输入)可以打断低优先级的任务(如后台数据同步),从而提高应用的响应性。
  3. 空间换时间:虽然 Fiber 节点的链表结构比传统虚拟 DOM 更占用空间(因为每个节点都需要额外的指针),但它提供了更高效的操作方式,尤其是在处理大量节点和复杂更新时。

4.Scheduler(调度器)是什么?

参考《自底向上盘一盘Scheduler》 Fiber 架构中的 Scheduler(调度器),React 在 setState 后不再直接启动“协调”过程,而是把本次更新注册到 Scheduler,再由 Scheduler 根据浏览器剩余空闲时间、优先级等因素派发给 Reconciler(协调器),并通过中断查询控制协调的中断重启。(协调就是我们说的包含 Diffing 的虚拟 DOM 构建计算过程)

5.Scheduler(调度器)有哪些模块?

5.1 SchedulerHostConfig:基于浏览器API,实现时间片管理,有两个关键方法,主要解决两个问题:
  1. 浏览器什么时候有空?有空的话,通知我

  2. 什么时候让出线程给浏览器? requestHostCallback:注册一个在帧间空闲时间执行的回调函数(及其过期时间),并可以通过 cancelHostCallback 取消它。

    1. 因requestIdleCallback兼容性堪忧,所以用MessageChannel的空闲回调模拟实现
    2. 可能当前帧已经不够执行回调,就需要挪到下一帧。为此引入了 requestAnimationFrame API来实现

shouldYieldToHost:随时判断是否需要让出线程(避免卡帧)

  1. React 能相对准确获取到当前帧的结束时间戳,如果当前时间超过帧结束时间,说明已经卡到帧了,需要让出。
5.2Scheduler 调度实现,主要做以下事情:
  1. 维护一个任务池
  2. 定义应用优先级决定任务池的调用顺序
  3. 派发任务(调 requestHostCallback)
  4. 及时中断(在 shouldYieldToHost 时终止派发)
5.3React 优先级管理: 两套优先级体系 一套转换体系

参考《React 中的优先级管理

  • 2套优先级体系

    1. fiber优先级(LanePriority) ,位于react-reconciler包, 也就是Lane(车道模型),用来处理与fiber构造过程相关的优先级

      1. Lane是对于expirationTime的重构,Lane类型被定义为二进制变量, 利用了位掩码的特性, 在频繁运算的时候占用内存少, 计算速度快。参考=》React算法之位运算

      2. lane可以简单理解为一些数字,数值越小,表明优先级越高。但是为了计算方便,采用二进制的形式来表示。

        1. 场景题:
        2. 先点击B按钮,然后快速点击A按钮,请问Reatc对这段代码的更新流程是什么样?
<p>You clicked {count} times</p>
  <button onClick={() => setCount(count + 1)}>
A按钮
  </button>
  <button onClick={() => startTransition(() => { setCount(count + 1) })}>
B按钮
  </button>
​

假设B按钮先点击, B更新开始,中途触发了A按钮点击,进而触发A更新。那么此时就会通过lane进行对比,A按钮是属于紧急更新,而B按钮的startTransition是过渡更新,紧急更新优先级高于过渡更新。此时会中断B更新,开始A更新。直到A更新完成时,再重新开始B更新。 (startTransition 是 React 18 中引入的一个新特性,用于标记更新的优先级较低,允许React延迟这些更新的处理以保持应用的响应性。) React17 Lanes模型相比React16 expirationTime模型有什么优势? 1.expirationTimes模型只能区分是否>=expirationTimes决定节点是否更新 2.lanes模型可以选定一个更新区间,并且动态的向区间中增减优先级,可以处理更细粒度的更新。

// 判断: 单task与batchTask的优先级是否重叠
//1. 通过expirationTime判断
const isTaskIncludedInBatch = priorityOfTask >= priorityOfBatch;
//2. 通过Lanes判断
const isTaskIncludedInBatch = (task & batchOfTasks) !== 0;
​
// 当同时处理一组任务, 该组内有多个任务, 且每个任务的优先级不一致
// 1. 如果通过expirationTime判断. 需要维护一个范围(在Lane重构之前, 源码中就是这样比较的)
const isTaskIncludedInBatch =
  taskPriority <= highestPriorityInRange &&
  taskPriority >= lowestPriorityInRange;
//2. 通过Lanes判断
const isTaskIncludedInBatch = (task & batchOfTasks) !== 0;
  1. 调度优先级(SchedulerPriority) : 位于scheduler包,用来处理与scheduler调度中心相关的优先级:

    1. 定义了五种优先级,以及它们对应的过期时间

image.png

  • 1套转换体系:

    • 优先级等级(ReactPriorityLevel) : 位于react-reconciler包中的SchedulerWithReactIntegration.js, 负责fiber优先级和调度优先级的转换,好实现协同处理。

6.React fiber 是如何实现时间切片的?

6.1概念:

本质上是将渲染任务拆分为多个小任务,以便提高应用程序的响应性和性能,主要依赖于两个功能:任务分割和任务优先级。

  • 任务分割(Time Slicing) :任务分割是指将一个大的渲染任务切割成多个小任务,每个小任务只负责一小部分 DOM 更新。React Fiber 使用 Fiber 节点之间的父子关系,将一个组件树分割成多个”片段“,每个“片段”内部是一颗 Fiber 子树,通过在 Fiber 树上进行遍历和操作,实现时间切片。
  • 任务优先级(Prioritization) :React Fiber 提供了一套基于优先级的算法来决定哪些任务应该先执行,哪些任务可以放到后面执行。React Fiber 将任务分成多个优先级级别,较高优先级的任务在进行渲染时会优先进行,从而确保应用程序的响应性和性能。
6.2基本流程:
  1. React Fiber 会将渲染任务划分成多个小任务,每个小任务一般只负责一小部分 DOM 更新。
  2. React Fiber 将这些小任务保存到任务队列中,并按照优先级进行排序和调度。
  3. 当浏览器处于空闲状态时,React Fiber 会从任务队列中取出一个高优先级的任务并执行,直到任务完成或者时间片用完。
  4. 如果任务完成,则将结果提交到 DOM 树上并开始下一个任务。如果时间片用完,则将任务挂起,并将未完成的工作保存到 Fiber 树中,返回控制权给浏览器。
  5. 当浏览器再次处于空闲状态时,React Fiber 会再次从任务队列中取出未完成的任务并继续执行,直到所有任务完成。
6.3React实现时间切片的发展历史
  1. 使用requestIdleCallback:

    • 初始尝试使用requestIdleCallback来实现时间切片。

    • 问题:

      • 不稳定:一帧的执行时间存在偏差,导致工作执行不稳定。
      • 兼容性:不同浏览器(特别是Safari)支持不佳。
  2. 初步方案:requestAnimationFrame + MessageChannel:

    • 通过requestAnimationFrame来计算一帧的过期时间。

    • 使用MessageChannel创建宏任务,确保任务在下次事件循环中执行,从而不阻塞页面渲染。

    • 问题:

      • 过于依赖显示器的刷新率,存在设备依赖性和不稳定性。
  3. 新方案:高频短间隔调度:

    • 利用宏任务机制,以高频(5ms间隔setTimeout(()=>{},5))对任务进行切片执行。

    • 目的是在每个宏任务执行间让出控制权给浏览器,以便进行必要的渲染工作。

    • 选择宏任务而非微任务,因为宏任务允许在每次事件循环后将控制权交还给浏览器。

    • API执行优先级:

      • 首选setImmediate(仅在特定环境下可用)。
      • 其次选择MessageChannel。
      • 兜底方案为setTimeout。
      • 优先使用MessageChannel而不是setTimeout的原因是MessageChannel能更快地被触发,相较于setTimeout即使设置为0,执行间隔也更短。

十四、React中两大工作循环scheduler任务调度循环和fiber构造循环有什么区别?

区别:

  1. scheduler任务调度循环源码位于scheduler.js,控制了所有任务的调度,包括fiber构造循环,fiber构造循环源码位于ReactFiberWorkLoop.js,控制fiber的构造。
  2. 「任务调度循环」是以「最小顶堆」为数据结构,堆顶是优先级最高的任务,循环执行堆的顶点, 直到堆被清空.
  3. 任务调度循环的逻辑偏向宏观, 它调度的是每一个任务(task), 而不关心这个任务具体是干什么的,具体任务就是调用函数去执行「fiber的构造循环」和「消费任务调度循环的任务」 联系: fiber构造循环是任务调度循环中的任务的一部分.它们是从属关系,每个任务都会重新构造一个fiber树.更具体一点,fiber构造循环(ReactFiberWorkLoop())被封装到了一个task里,给到任务调度循环,然后由任务调度循环决定什么时候执行.

十五、React 18都新增了哪些新特性和新API?

1.新特性:

  • ReactDOM.createRoot:启用 React 18 中的并发功能(concurrency并发渲染)。

  • setState 自动批处理(Automatic Batching):

    1. 在React 18 之前,我们只在 React 事件处理函数 中进行批处理更新。默认情况下,在promise、setTimeout、原生事件处理函数中、或任何其它事件内的更新都不会进行批处理。
    2. 在 18 之后,任何情况都会自动执行批处理,多次更新始终合并为一次。
  • flushSync:

    • 批量更新是一个破坏性的更新,如果想退出批量更新,可以使用flushSync。
  • 关于 React 组件的返回值:

    • 在 React 17 中,如果你需要返回一个空组件,React只允许返回null。如果你显式的返回了 undefined,控制台则会在运行时抛出一个错误。
    • 在 React 18 中,不再检查因返回 undefined 而导致崩溃。既能返回 null,也能返回 undefined(但是 React 18 的dts文件还是会检查,只允许返回 null,你可以忽略这个类型错误)。
  • Strict Mode:

    • 当你使用严格模式时,React 会对每个组件进行两次渲染,以便你观察一些意想不到的结果。在 React 17 中,取消了其中一次渲染的控制台日志,以便让日志更容易阅读。为了解决社区对这个问题的困惑,在 React 18 中,官方取消了这个限制。如果你安装了React DevTools,第二次渲染的日志信息将显示为灰色,以柔和的方式显示在控制台。
  • 去掉了对IE浏览器的支持,react18引入的新特性全部基于现代浏览器,如需支持需要退回到react17版本。

2.新API:

  • useId: 同一个组件在客户端和服务端生成相同的唯一的 ID。

  • Suspense: 在v16/v17中,Suspense主要是配合React.lazy进行code spliting。在v18中,Suspense加入了fallback属性,用于将读取数据和指定加载状态分离,可以实现自定义loading内容,数据加载成功后,loading自动消失。

  • useDeferredValue:

    • 可以让我们延迟渲染不紧急的部分,类似于防抖但没有固定的延迟时间,延迟的渲染会在紧急的部分先出现在浏览器屏幕以后才开始,并且可中断不会阻塞用户输入
    • 场景:一遍输入input,一边根据输入内容渲染结果,传统方案中渲染列表会占据线程,阻塞用户输入,形成卡顿,useDeferredValue会降低优先级,实现渲染列表不卡顿input输入。
  • useTransition: 用于改进用户体验,特别是在执行可能导致界面延迟的操作。

  • useSyncExternalStore: 能够通过强制同步更新数据让 React 组件在 CM 下安全地有效地读取外接数据源。

  • useInsertionEffect: 这个 Hooks 执行时机在 DOM 生成之后,useLayoutEffect 之前,它的工作原理大致和 useLayoutEffect 相同,只是此时无法访问 DOM 节点的引用,一般用于提前注入 脚本。

15.2.1. 什么是Automatic Batching机制?

答:当连续两个state发生变更的时候,放在一起执行处理,这样页面只更新渲染一次。在18之前也有类似的机制,不过是只在react合成事件中有效,在setTimeout,promise,自定义事件中无效。现在在18版本中,可以全面生效。

15.2.2. 什么是concurrency并发渲染?

答:concurrency是一种理念,它把用户的行为分为低优先级和高优先级,这样就会有更多的cpu资源来优先渲染高优先级的任务。可以通过useTransition这个hook对执行的函数进行降级处理,或者通过useDeferredValue把数据带入到慢速渲染通道中。

15.2.3 useTranstion和useDeferredValue的区别?

答:useTranstion是处理一个执行的函数或多个执行的函数,使之降级。useDeferredValue是把数据进入到慢速渲染通道中,只对单个数据进行处理。

十六、Vue和React的区别

相同点:

  1. 都使用Virtural DOM。
  2. 都使用组件化思想,流程基本一致。
  3. 都是响应式,数据驱动视图。
  4. 都有成熟的社区,都支持服务端渲染。

不同点:

  • 核心思想不同:

    • Vue推崇灵活易用(渐进式开发体验),数据可变,双向数据绑定(依赖收集)。
    • React推崇函数式编程(纯组件),数据不可变以及单向数据流。
  • 响应式原理不同:

  • vue:

    1. Vue依赖收集,自动优化,数据可变。
    2. Vue递归监听data的所有属性,直接修改。
    3. 当数据改变时,自动找到引用组件重新渲染。
  • react:

    1. React基于状态机,手动优化,数据不可变,需要setState驱动新的State替换老的State。
    2. 当数据改变时,以组件为根目录,默认全部重新渲染。
  • diff算法不同:

    • 相似点:

      1. 都是基于两个假设(使得算法复杂度降为O(n)):
      2. 不同的组件产生不同的 DOM 结构。当type不相同时,对应DOM操作就是直接销毁老的DOM,创建新的DOM。
      3. 同一层次的一组子节点,可以通过唯一的 key 区分。
    • 源码实现不同:

      • Vue基于snabbdom库,它有较好的速度以及模块机制。Vue Diff使用双向链表,边patch,边更新DOM。(v2使用双向链表,v3使用最长递增子序列)
      • React主要使用diff队列保存需要更新哪些DOM,得到patch树,再统一操作批量更新DOM。(仅向右移动)

参考文章: Vue2和Vue3和React三者的diff算法有什么区别? 认识 React、Vue2、Vue3 三者的 diff 算法与对比

十七、为什么React不能像Vue一样,渲染时候精确到当前组件的粒度?

  • 在react中,组件的状态是不能被修改的,setState没有修改原来那块内存中的变量,而是去新开辟一块内存; 而vue则是直接修改保存状态的那块原始内存。(不可变数据VS可变数据)
  • react中,调用setState方法后,会自顶向下重新渲染组件,自顶向下的含义是,该组件以及它的子组件全部需要渲染;而vue使用Object.defineProperty(vue@3迁移到了Proxy)对数据的设置(setter)和获取(getter)做了劫持,也就是说,vue能准确知道视图模版中哪一块用到了这个数据,并且在这个数据修改时,告诉这个视图,你需要重新渲染了。
  • 所以当一个数据改变,react的组件渲染是很消耗性能的——父组件的状态更新了,所有的子组件得跟着一起渲染,它不能像vue一样,精确到当前组件的粒度。

十八、说一说你对React中Fragment的理解?

在React中,组件返回的元素只能有一个根元素,Fragment它允许组件返回多个元素而不需要额外的父DOM元素来包裹它们。这个特性在React 16中被引入,为组件的渲染提供了更多的灵活性和效率。

Fragment的作用
  1. 避免额外的DOM元素

    • 在使用Fragment之前,如果你想从一个组件返回多个元素,你需要用一个额外的DOM元素(如
      )来包裹它们。这可能导致不必要的DOM层级和性能问题。
    • 使用Fragment,你可以返回一个元素列表而不添加额外的DOM节点。这在某些布局中非常有用,尤其是当你不希望因为额外的包裹元素而破坏CSS样式或布局时。
  2. 简化DOM结构

    • Fragment帮助保持DOM结构的简洁和清晰,这对于维护和理解代码是非常重要的。
  3. 关键列表渲染

    • 在渲染列表时,Fragment可以用来避免额外的包裹元素,同时仍然可以在列表项周围添加key属性,这对于React的列表渲染性能优化是重要的。

十九、对 React context 的理解

当你不想在组件树中通过逐层传递props或者state的方式来传递数据时,可以使用Context来实现跨层级的组件数据传递。 当React组件提供的Context对象其实就好比一个提供给子组件访问的作用域,而 Context对象的属性可以看成作用域上的活动对象。由于组件 的 Context 由其父节点链上所有组件通 过 getChildContext()返回的Context对象组合而成,所以,组件通过Context是可以访问到其父组件链上所有节点组件提供的Context的属性。

二十、 类组件与函数组件有什么异同?

相同:

组件是 React 的最小编码单位,所以无论是函数组件还是类组件,在使用方式和最终呈现效果上都是完全一致的。

不同:
  1. 它们在开发时的心智模型上却存在巨大的差异。类组件是基于面向对象编程的,它主打的是继承、生命周期等核心概念;而函数组件内核是函数式编程,主打的是 immutable、没有副作用、引用透明等特点。
  2. 性能优化上,类组件主要依靠 shouldComponentUpdate 阻断渲染来提升性能,而函数组件依靠 React.memo 缓存渲染结果来提升性能。
  3. 从上手程度而言,类组件更容易上手,从未来趋势上看,由于React Hooks 的推出,函数组件成了社区未来主推的方案。
  4. 类组件在未来时间切片与并发模式中,由于生命周期带来的复杂度,并不易于优化。而函数组件本身轻量简单,且在 Hooks 的基础上提供了比原先更细粒度的逻辑组织与复用,更能适应 React 的未来发展。

二十一、react-router路由

React-Router的实现原理是什么?
客户端路由的实现思想:
  1. 基于 Hash 的路由:

    • 这种路由方式利用了 URL 的 hash(#后面的部分)。
    • 当 hash 变化时,页面不会重新加载,但可以通过监听 hashchange 事件来响应 URL 的变化。
    • 这种方式的优点是兼容性好,适用于老旧浏览器。
  2. 基于 H5 History 路由:

    • HTML5 引入了一个新的 History API,允许开发者直接操作浏览器的历史记录栈
    • 使用 history.pushState 和 history.replaceState 方法可以改变 URL 而不重新加载页面。
    • 通过 history.go、history.forward、history.back 等 API 可以控制浏览器的历史记录导航。
React-Router 的实现思想:
  1. 基于 History 库:

    • React Router 使用了 history 库来抽象化这些底层机制,提供了一个统一的 API。
    • 通过这个库,React Router 能够无缝地在不同类型的历史记录(hash、browser、memory)之间切换,同时磨平了浏览器之间的差异。
  2. 路由匹配和渲染:

    • React Router 维护了一个路由配置的列表。
    • 每次 URL 发生变化时,React Router 会根据当前的 URL 和这个列表匹配,找到对应的组件(Component),然后进行渲染。
  3. History 类型:

    • createHashHistory:用于老版本浏览器,基于 URL 的 hash 部分。
    • createBrowserHistory:用于现代浏览器,基于 HTML5 History API。
    • createMemoryHistory:主要用于非浏览器环境,如服务器端渲染(SSR)或测试,历史记录保存在内存中。

总的来说,React Router 的实现依赖于底层的** URL 变化机制**,通过 history 库对这些机制进行封装和抽象,提供了一套易用且功能强大的路由管理解决方案。无论是基于 hash 还是基于 HTML5 History API 的路由,React Router 都能够提供一致的开发体验,并且使得路由的变化和组件的渲染能够高效地协同工作。

react-router 里的 Link 标签和 a 标签的区别?

从最终渲染的 DOM 来看,这两者都是链接,都是 a标签,区别是∶ 是react-router 里实现路由跳转的链接,一般配合 使用,react-router接管了其默认的链接跳转行为,区别于传统的页面跳转, 的“跳转”行为只会触发相匹配的对应的页面内容更新,而不会刷新整个页面。

二十二、在react中如何处理异常

在React中处理异常主要依赖于错误边界(Error Boundary)。错误边界是React组件,它可以捕获其子组件树中发生的JavaScript错误,并记录这些错误,并显示一个备用UI,而不是使整个组件树崩溃。 错误边界是一个使用了static getDerivedStateFromError() 或 componentDidCatch() 这两个生命周期方法的类组件。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
​
  static getDerivedStateFromError(error) {
    // 更新状态,以便下一次渲染能够显示备用UI
    return { hasError: true };
  }
​
  componentDidCatch(error, errorInfo) {
    // 你也可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
  }
​
  render() {
    if (this.state.hasError) {
      // 你可以渲染任何自定义的备用UI
      return <h1>出错了。</h1>;
    }
​
    return this.props.children;
  }
}
<ErrorBoundary>
  <MyComponent />
</ErrorBoundary>

错误边界主要用于捕获以下类型的错误:

  • 渲染期间的错误。
  • 生命周期方法中的错误。
  • 构造函数中的错误(在渲染树中)。

二十三、React性能优化方案

1.跳过不必要的组件更新:

  • PureComponent:用于类组件中,同时对 props 和 state 的变化前和变化后的值进行浅对比,对每个 props 值进行基本的值对比,如果值类型是复杂类型,如引用类型(对象),并不会深入遍历每个属性的变化,如果都没发生变化则会跳过重渲染。

  • React.memo:用于函数组件中,功能和PureComponent一样。

  • shouldComponentUpdate:

    • 在每次渲染(render)之前被调用,并且根据该函数的返回值(true/false)来决定是否调用渲染函数(return true 触发渲染,return false 阻止渲染)。
    • 但是组件的首次渲染或者调用 forceUpdate() 方法时不会触发调用。
  • 使用useCallback和useMemo缓存函数的引用或值:

    • useCallback:是useMemo的语法糖,是「useMemo 的返回值为函数」时的特殊情况,缓存的是函数的引用。
    • useMemo:缓存计算数据的值。
  • 状态下放,缩小状态影响范围:如果一个状态只在某部分子树中使用,那么可以将这部分子树提取为组件,并将该状态移动到该组件内部。

  • 列表项使用 key 属性;

  • useMemo 返回虚拟 DOM:利用 useMemo 可以缓存计算结果的特点,如果 useMemo 返回的是组件的虚拟 DOM,则将在 useMemo 依赖不变时,跳过组件的 Render 阶段。

  • Hooks 按需更新:

    • 如果自定义 Hook 暴露多个状态,而调用方只关心某一个状态,那么其他状态改变就不应该触发组件重新 Render。
    • codesandbox.io/s/hooks-anx…
  • 使用immutable解决memo浅比较陷阱:

    • memo使用Object.is()进行浅比较,深拷贝又需要递归耗费性能。
    • immutable.js会将引用对象变成一个immutable对象,改变某一属性的时候,会更新当前属性以及它所有的父节点属性,其余属性保持不变,实现数据复用,提高深层次比较效率。

2.减少提交阶段耗时:

  • React 工作流提交阶段的第二步就是执行提交阶段钩子,它们的执行会阻塞浏览器更新页面。如果在提交阶段钩子函数中更新组件 State,会再次触发组件的更新流程,造成两倍耗时。
  • 避免在 didMount、didUpdate、willUnmount、useLayoutEffect 和特殊场景下的 useEffect中更新组件 State。

3.前端通用优化

  • debounce、throttle 优化频繁触发的回调。

  • 组件按需挂载:

    • 懒加载
    • 懒渲染:当组件进入或即将进入可视区域时才渲染组件:通过 react-visibility-observer 进行监听
    • 虚拟列表:react-window

二十四、React动态import怎么实现?

动态 Import() 与 ESM
  1. ESM(ECMAScript Modules):

    • ESM是ECMAScript(JavaScript标准)的官方模块系统。
    • import语句用于导入模块中的绑定(函数、对象、原始值等)。
  2. 动态 Import():

    • 动态import()是一种异步加载ESM模块的方式。
    • 它返回一个Promise对象,该对象解析为一个模块命名空间对象,加载完成后获取 Module 并在 then 中注册回调函数,其中包含了导入模块的所有导出。
    • 与静态import声明不同,动态import()可以在代码的任何地方调用,提供更灵活的加载方式。
Webpack中的Code-Splitting
  1. 检测动态 Import():

    • 当Webpack构建过程中检测到import()语句时,它会自动将相关模块作为一个新的代码块(chunk)进行处理。
    • 这意味着该模块会被分离到独立的文件中,而非包含在主bundle文件中。
  2. 优化加载:

    • 这种代码分割的技术允许应用仅在需要时加载某些代码块,而不是一开始就加载整个应用的所有代码。
    • 这对于提高应用的初始加载速度和整体性能非常有帮助,尤其是对于大型应用。
  3. 实例:

import("./moduleA").then(moduleA => {
  // 使用模块A的代码
  moduleA.doSomething();
});

在这个例子中,moduleA只有在import()调用时才会被加载。

实践中的应用

在React等前端框架中,结合React.lazy和Suspense使用动态import(),可以实现组件级别的懒加载,从而进一步优化应用的性能。

注意事项
  • 确保使用的构建工具(如Webpack)和环境支持ESM和动态import()。
  • 动态导入的模块可能不会立即可用,需要处理好异步加载的状态,如加载中、加载失败等。

二十五、React的patch流程:

  1. React新版架构新增了一个Scheduler调度器主要用于调度Fiber节点的生成和更新任务
  2. 当组件更新时,Reconciler协调器执行组件的render方法生成一个Fiber节点之后再递归的去生成Fiber节点的子节点
  3. 每一个Fiber节点的生成都是一个单独的任务,会以回调的形式交给Scheduler进行调度处理,在Scheduler里会根据任务的优先级去执行任务
  4. 任务的优先级的指定是根据车道模型,将任务进行分类,每一类拥有不同的优先级,所有的分类和优先级都在React中进行了枚举
  5. Scheduler按照优先级执行任务时,会异步的执行,同时每一个任务执行完成之后,都会通过requestIdleCallBack去判断下一个任务是否能在当前渲染帧的剩余时间内完成
  6. 如果不能完成就发生中断,把线程的控制权交给浏览器,剩下的任务则在下一个渲染帧内执行
  7. 整个Reconciler和Scheduler的任务执行完成之后,会生成一个新的workInProgressFiber的新的节点树,之后Reconciler触发Commit阶段通知Render渲染器去进行diff操作,也就是我们说的patch流程
1. Scheduler(调度器)
  • 任务调度:Scheduler是React新架构中的一个关键部分,它负责调度组件的渲染更新任务。Scheduler会根据任务的优先级来决定执行顺序,确保更重要的更新(如用户交互)能够优先处理。
  • 优先级和车道模型:React中的任务分为不同的优先级,这些优先级是根据所谓的“车道模型”来分类的。不同类型的更新(如同步更新、异步更新)会被分配到不同的车道,并拥有不同的优先级。
2. Reconciler(协调器)
  • Fiber节点的生成:当组件状态更新时,Reconciler开始工作,执行组件的render方法来生成新的Fiber节点。Fiber架构允许Reconciler以单个任务的形式处理组件树的更新。
  • 递归子节点:生成Fiber节点后,Reconciler递归地处理子节点,为每个子节点创建新的Fiber任务。
3. 任务的执行与中断
  • 异步执行和中断:Scheduler会根据优先级异步执行任务。在执行每个任务后,通过requestIdleCallback检查当前帧的剩余时间,如果时间不足以完成下一个任务,则该任务会被中断,控制权交回给浏览器。
  • 任务继续:中断的任务会在下一个渲染帧继续执行。
4. Commit阶段
  • workInProgressFiber树:完成所有任务后,Reconciler会生成一棵新的workInProgressFiber树。
  • Diff操作:在Commit阶段,Reconciler通知渲染器进行DOM更新的Diff操作,即patch流程。这包括添加、删除或更新DOM节点。
5. 总结
  • 这个过程提高了React应用的性能和响应性,使其能够处理大量的更新,同时保持良好的用户交互体验。
  • 通过将任务分解并利用浏览器的空闲时间,React能够更智能地安排工作,避免长时间阻塞主线程,减少界面卡顿。