说明:本文是不间断更新的,关注不迷路。最近更新时间 2025.02.26。
最近参与由双越老师带队,和多名精英博主共建的前端面试派:www.mianshipai.com
面试总结系列:
- HTML 面试知识点总结: juejin.cn/post/693310…
- CSS 面试题:juejin.cn/post/705457…
- 前端面试真题:juejin.cn/spost/69531…
- 前端网络安全整理:juejin.cn/post/711059…
- 前端手写代码整理:juejin.cn/post/711094…
- Vue 面试题:juejin.cn/post/705448…
- React 面试题:juejin.cn/post/734865…
1. React18 新特性
18 不在支持 IE
批处理
18 之前,批处理只限于 React 原生事件内部
的更新。
18 中,批处理支持处理的操作范围扩大了:Promise,setTimout,native event handler
等这些非 React 原生事件。
Transitions
-
starTransition
:用于标记非紧急的更新,用 starTransition 包裹起来就是告诉 React,这部分代码渲染的优先级不高,可以优先处理其它更重要的渲染。 -
useTransition
:除了能提供 startTransition 以外,还能提供一个变量来跟踪当前渲染的执行状态。
Suspense
ReactDom.createRoot
ReactDom.hydrateRoot
New Hooks
useTransition
:用来标记低优先的 state 更新useDeferredValue
:可以用来标记低优先的变量
架构演进
-
React 15 主要分为
Reconciler 协调器
和Renderer 渲染器
两部分:- Reconciler 负责生成虚拟 DOM 并进行 diff,找出变动的虚拟 DOM,
- 然后 Renderer 负责将变化的组件渲染到不同的宿主环境中。
-
React 16 多了一层
Scheduler 调度器
,并且Reconciler 协调器
的部分基于Fiber
完成了重构。 -
React 17 是一个用以稳定
concurrent mode 并行模式
的过渡版本,另外,它使用Lanes
重构了优先级算法。- Lane 用二进制位表示任务的优先级,方便优先级的计算(位运算),不同优先级占用不同位置的“赛道”,而且存在批的概念,优先级越低,“赛道”越多。高优先级打断低优先级,新建的任务需要赋予什么优先级等问题都是 Lane 所要解决的问题。
流程
整个 Reconciliation
的流程可以简单地分为两个阶段:
-
Render 阶段
:当 React 需要进行 re-render 时,会遍历 Fiber 树的节点,根据 diff 算法将变化应用到workInProgress
树上,这个阶段是随时可中断的。 -
Commit 阶段
:当workInProgress
树构建完成之后,将其作为Current
树,并把 DOM 变动绘制到页面上,这个阶段是不可中断的,必须一气呵成,类似操作系统中「原语」的概念。 -
workInProgress tree 代表当前正在执行更新的 Fiber 树
-
currentFiber tree 表示上次渲染构建的 Filber 树
Scheduler
对于大部分浏览器来说,每 1s 会有 60 帧
,所以每一帧差不多是 16.6 ms
,如果 Reconciliation 的 Render 阶段的更新时间过长,挤占了主线程其它任务的执行时间,就会导致页面卡顿。
思路:
- 将 re-render 时的 JS 计算拆分成更小粒度的任务,可以随时暂停、继续和丢弃执行的任务。
- 当 JS 计算的时间达到 16 毫秒之后使其暂停,把主线程让给 UI 绘制,防止出现渲染掉帧的问题。
- 在浏览器空闲的时候继续执行之前没执行完的小任务。
React 给出的解决方案是将整次 Render 阶段的长任务拆分成多个小任务:
- 每个任务执行的时间控制在 5ms。
- 把每一帧 5ms 内未执行的任务分配到后面的帧中。
- 给任务划分优先级,同时进行时优先执行高优任务。
如何把每个任务执行的时间控制在 5ms?
Scheduler 提供的 shouldYield
方法在 源码 中叫 shouldYieldToHost
,它通过综合判断已消耗的时间(是否超过 5ms)、是否有用户输入等高优事件来决定是否需要中断遍历,给浏览器渲染和处理其它任务的时间,防止页面卡顿。
如何把每一帧 5ms 内未执行的任务分配到后面的帧中?
时间切片
如果任务的执行因为超过了 5ms 等被中断了,那么 React Scheduler 会借助一种效果接近于 setTimeout
的方式来开启一个宏任务,预定下一次的更新。
React 是在借助 MessageChannel 模拟 setTimeout
的行为,将未完成的任务以宏任务的形式发放给浏览器,被动地让浏览器自行安排执行时间。
而 requestIdleCallback 是主动从浏览器处获取空闲信息并执行任务,个人感觉不太像是一种对 requestIdleCallback 的 polyfill。
如何给任务划分优先级?
基于 Lanes
的优先级控制。
不同的 Lanes 可以简单理解为不同的数值,数值越小,表明优先级越高。比如:
- 用户事件比较紧急,那么可以对应比较高的优先级如 SyncLane;
- UI 界面过渡的更新不那么紧急,可以对应比较低的优先级如 TransitionLane;
- 网络加载的更新也不那么紧急,可以对应低优先级 RetryLane。
2. 对 React Hook 的闭包陷阱的理解?
React Hooks 的闭包陷阱发生在 useState
钩子函数中的示例:
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount(count + 1);
}, 1000);
};
const handleReset = () => {
setCount(0);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
<button onClick={handleReset}>Reset</button>
</div>
);
}
改进方法:
const handleClick = () => {
setTimeout(() => {
setCount(count => count + 1);
}, 1000);
};
React Hooks 中的闭包陷阱通常发生在 useEffect
钩子函数中的示例:
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(timer);
}, []);
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
改进方法:
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(timer);
}, [count]);
为什么不能将hooks写到if else语句中了把?
react 用链表来严格保证hooks的顺序。
因为这样可能会导致顺序错乱,导致当前 hooks 拿到的不是自己对应的 Hook 对象。
3. 让 useEffect 支持 async/await ?
- 创建一个异步函数(async...await 的方式),然后执行该函数。
useEffect(() => {
const asyncFun = async () => {
setPass(await mockCheck());
};
asyncFun();
}, []);
- 也可以使用 IIFE,如下所示:
useEffect(() => {
(async () => {
setPass(await mockCheck());
})();
}, []);
- ahooks useAsyncEffect
function useAsyncEffect(
effect: (isCanceled: () => boolean) => Promise<void>,
dependencies?: any[]
) {
return useEffect(() => {
let canceled = false;
effect(() => canceled);
return () => {
canceled = true;
}
}, dependencies)
}
4. React 性能优化
减少计算量
- 减少渲染的节点/降低渲染计算量(复杂度)
- 不要在渲染函数都进行不必要的计算
- 比如不要在渲染函数(render)中进行数组排序、数据转换、订阅事件、创建事件处理器等等.
- 减少不必要的嵌套
- 虚拟列表
- 惰性渲染
- CSS > 大部分 CSS-in-js > inline style
利用缓存
- 避免重新渲染
- shouldComponentUpdate
- React.memo
- 简化的 props 更容易理解, 且可以提高组件缓存的命中率
- 不变的事件处理器
- useCallback
- 不可变数据
- Immutable.js、Immer、immutability-helper 以及 seamless-immutable。
- 简化 state
精确重新计算的范围
- 响应式数据的精细化渲染
- 不要滥用 Context
- 一旦 Context 的 Value 变动,所有依赖该 Context 的组件会全部 forceUpdate.
5. 为什么 useState 返回的是数组而不是对象?
因为解构赋值的原因:
- 返回数组,可以对数组中的变量命名,代码看起来也比较干净。
- 返回对象,那就必须和返回的值同名,不能重复使用了。
6. React 懒加载的实现原理?
React.lazy
React 16.6 之后,React 提供了 React.lazy
方法来支持组件的懒加载。配合 webpack 的 code-splitting 特性,可以实现按需加载。
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
如上代码中,通过 import()、React.lazy 和 Suspense
共同一起实现了 React 的懒加载,也就是我们常说了运行时动态加载,即 OtherComponent 组件文件被拆分打包为一个新的包(bundle)文件,并且只会在 OtherComponent 组件渲染时,才会被下载到本地。
React.lazy 需要配合 Suspense 组件一起使用,在 Suspense 组件中渲染 React.lazy 异步加载的组件。如果单独使用 React.lazy,React 会给出错误提示。
Webpack 动态加载
import() 函数
是由 TS39 提出的一种动态加载模块的规范实现,其返回是一个 promise
。
webpack 检测到这种import() 函数
语法会自动代码分割。使用这种动态导入语法代替以前的静态引入,可以让组件在渲染的时候,再去加载组件对应的资源,
webpack 通过创建 script 标签
来实现动态加载的,找出依赖对应的 chunk
信息,然后生成 script 标签来动态加载 chunk,每个 chunk 都有对应的状态:未加载、加载中、已加载
。
Suspense 组件
Suspense 内部主要通过捕获组件的状态去判断如何加载,React.lazy 创建的动态加载组件具有 Pending、Resolved、Rejected
三种状态,当这个组件的状态为 Pending 时显示的是 Suspense 中 fallback 的内容,只有状态变为 resolve 后才显示组件。
7. React VS Vue
- 组件化方式不同
- React 组件包含状态和行为,所有组件共享一个状态树
- Vue 每个组件都有自己的状态和行为,并且可以很容易将数据和行为绑定在一起
- 数据驱动方式不同
- React 单项数据流
- Vue 双向数据绑定
- 模板语法不同
- React 模板语法是 JSX,all in js
- Vue 模板语法是 Template、js、css,支持指令
- 生命周期不同
- React 生命周期:初始化、更新、卸载
- Vue 生命周期:创建、挂载、更新、销毁
- 状态管理方式不同
- React 状态管理:Redux、Mobx、zustand
- Vue 状态管理:Vuex、Pinia
- 性能优化方式不同
- React 性能优化:React.memo、shouldComponentUpdate
- Vue 性能优化:keep-alive、v-if
8. React 组件通信
父组件调子组件
- 如果是类组件,可以在子组件类中定义一个方法,并将其挂载到实例上
- 如果是类组件,可以使用
createRef 创建一个 ref 对象
,并将其传递给子组件的 ref prop - 如果是函数式组件,可以使用
useImperativeHandle
Hook 将指定的方法暴露给父组件 - 如果是函数式组件,可以使用
useRef
创建一个 ref 对象,并将其传递给子组件的 ref prop
9. 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 可以被用来操作组件的状态,以及处理用户的交互事件等。
10. Redux
核心描述:
单一数据源
:整个应用的全局 state 被存储在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。State 是只读的
:唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事情的普通对象。使用纯函数来执行修改
:为了描述 action 如何改变 state tree,你需要编写纯的 reducers。
11. React Hooks 实现生命周期?
相对于传统class, Hooks 有哪些优势?
- State Hook 使得组件内的状态的设置和更新相对独立,这样便于对这些状态单独测试并复用。
- Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分,这样使得各个逻辑相对独立和清晰。
|生命周期方法 | Hooks 组件 |
---|---|
constructor | useState |
getDerivedStateFromProps | useEffect 手动对比 props, 配合 useState 里面 update 函数 |
shouldComponentUpdate | React.memo |
render | 函数本身 |
componentDidMount | useEffect 第二个参数为[] |
componentDidUpdate | useEffect 配合useRef |
componentWillUnmount | useEffect 里面返回的函数 |
componentDidCatch | 无 |
getDerivedStateFromError | 无 |
import React, { useState, useEffect, useRef, memo } from 'react';
// 使用 React.memo 实现类似 shouldComponentUpdate 的优化,
// React.memo 只对 props 进行浅比较
const UseEffectExample = memo((props) => {
console.log("===== UseStateExample render=======");
// 声明一个叫 “count” 的 state 变量。
const [count, setCount] = useState(0);
const [count2, setCount2] = useState(0);
const [fatherCount, setFatherCount] = useState(props.fatherCount)
console.log(props);
// 模拟 getDerivedStateFromProps
useEffect(() => {
// props.fatherCount 有更新,才执行对应的修改,没有更新执行另外的逻辑
if(props.fatherCount == fatherCount ){
console.log("======= 模拟 getDerivedStateFromProps=======");
console.log(props.fatherCount, fatherCount);
}else{
setFatherCount(props.fatherCount);
console.log(props.fatherCount, fatherCount);
}
})
// 模拟 componentDidMount
useEffect(() => {
console.log("=======只渲染一次(相当于DidMount)=======");
console.log(count);
}, [])
// 模拟 componentDidUpdate
const mounted = useRef();
useEffect(() => {
console.log(mounted);
if (!mounted.current) {
mounted.current = true;
} else {
console.log("======count 改变时才执行(相当于DidUpdate)=========");
console.log(count);
}
}, [count])
// 模拟 componentDidMount 和 componentDidUpdate、componentWillUnmount
useEffect(() => {
// 在 componentDidMount,以及 count 更改时 componentDidUpdate 执行的内容
console.log("======初始化、或者 count 改变时才执行(相当于Didmount和DidUpdate)=========");
console.log(count);
return () => {
console.log("====unmount=======");
console.log(count);
}
}, [count])
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={() => setCount2(count2 + 1)}>
Click me2
</button>
</div>
);
});
export default UseEffectExample;
注意事项:
-
useState
只在初始化时执行一次,后面不再执行; -
useEffect
相当于是 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合,可以通过传参及其他逻辑,分别模拟这三个生命周期函数; -
useEffect
第二个参数是一个数组,- 如果数组为空时,则只执行一次(相当于 componentDidMount);
- 如果数组中有值时,则该值更新时,useEffect 中的函数才会执行;
- 如果没有第二个参数,则每次 render 时,useEffect 中的函数都会执行;
-
React 保证了每次运行 effect 的同时,DOM 都已经更新完毕,也就是说 effect 中的获取的 state 是最新的,但是需要注意的是,effect 中返回的函数(其清除函数)中,获取到的 state 是更新前的。
-
传递给 useEffect 的函数在每次渲染中都会有所不同,这是刻意为之的。事实上这正是我们可以在 effect 中获取最新的 count 的值,而不用担心其过期的原因。每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。
-
effect 的清除阶段(返回函数)在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次。它会在调用一个新的 effect 之前对前一个 effect 进行清理,从而避免了我们手动去处理一些逻辑。
12. React.memo() 和 useMemo()
React.memo()
随 React v16.6 一起发布。
虽然类组件已经允许您使用 PureComponent
或 shouldComponentUpdate
来控制重新渲染,但 React 16.6 引入了对函数组件执行相同操作的能力。
React.memo() 是一个高阶组件 (HOC)
,它接收一个组件 A 作为参数并返回一个组件 B,如果组件 B 的 props(或其中的值)没有改变,则组件 B 会阻止组件 A 重新渲染 。
useMemo() 是一个 React Hook。
- 可以依赖 useMemo() 作为性能优化,而不是语义保证
- 函数内部引用的每个值也应该出现在依赖项数组中
React.memo() 和 useMemo() 之间的主要区别:
- React.memo() 是一个高阶组件,可以使用它来包装不想重新渲染的组件,除非其中的 props 发生变化
- useMemo() 是一个 React Hook,可以使用它在组件中包装函数。 可以使用它来确保该函数中的值仅在其依赖项之一发生变化时才重新计算。
虽然 memoization 似乎是一个可以随处使用的巧妙小技巧,但只有在绝对需要这些性能提升时才应该使用它。 Memoization 会占用运行它的机器上的内存空间,因此可能会导致意想不到的效果。
13. 实现 useTimeout hook
useTimeout 是可以在函数式组件中,处理 setTimeout 计时器函数
// callback 回调函数, delay 延迟时间
function useTimeout(callback, delay) {
const memorizeCallback = useRef();
useEffect(() => {
memorizeCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay !== null) {
const timer = setTimeout(() => {
memorizeCallback.current();
}, delay);
return () => {
clearTimeout(timer);
};
}
}, [delay]);
};
14. 对 useReducer 的理解
useReducer 是 React Hooks 中的一个函数,用于管理和更新组件的状态。它可以被视为 useState 的一种替代方案,适用于处理更复杂的状态逻辑。
import { useReducer } from 'react';
const initialState = {
count: 0,
};
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error('Unsupported action type');
}
};
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
const increment = () => {
dispatch({ type: 'increment' });
};
const decrement = () => {
dispatch({ type: 'decrement' });
};
return (
<div>
<p>Count: {state.count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
相比于 useState,useReducer 在处理复杂状态逻辑时更有优势,因为它允许我们将状态更新的逻辑封装在 reducer 函数中,并根据不同的动作类型执行相应的逻辑。这样可以使代码更具可读性和可维护性,并且更容易进行状态追踪和调试。
15. React.memo() VS JS 的 memorize 函数
适用范围不同
:
- React.memo() 主要适用于优化 React 组件的性能表现,
- 而 memorize 函数可以用于任何 JavaScript 函数的结果缓存。
实现方式不同
:
- React.memo() 是一个 React 高阶组件(HOC),通过浅层比较 props 是否发生变化来决定是否重新渲染组件。- 而 memorize 函数则是通过将函数的输入参数及其计算结果保存到一个缓存对象中,以避免重复计算相同的结果。
缓存策略不同
:
- React.memo() 的缓存策略是浅比较(shallow compare),只比较 props 的第一层属性值是否相等,不会递归比较深层嵌套对象或数组的内容。
- 而 memorize 函数的缓存策略是将输入参数转换成字符串后,作为缓存的键值。如果传入的参数不是基本类型时,则需要自己实现缓存键值的计算。
应用场景不同
:- `React.memo() 主要适用于对不经常变化的组件进行性能优化,
- 对于状态不变的组件或纯函数,可以使用 React.memo() 进行优化;
- 而 memorize 函数则主要适用于对计算量大、执行时间长的函数进行结果缓存。
- 对于递归计算、复杂数学运算等耗时操作,可以使用 memorize 函数进行结果缓存。
- `React.memo() 主要适用于对不经常变化的组件进行性能优化,
16. 类组件 VS React Hooks
跨组件复用
:- 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;
- 相比而言,
类组件的实现更为复杂
- 不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;
- 时刻需要关注 this 的指向问题;
- 代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;
状态与 UI 隔离
:- 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。
注意:
- 避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定;
- 不能在 useEffect 中使用 useState,React 会报错提示;
- 类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存
17. setState 是同步还是异步?
react 18 之前:
- 在Promise的状态更新、js原生事件、setTimeout、setInterval..中是同步的。
- 在react的合成事件中,是异步的。
setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
react 18 之后:
- setState都会表现为异步(即批处理)。
18. React-Router 的 <Link />
组件和 <a>
有什么区别?
Link 的 “跳转” 行为只会触发相匹配的对应的页面内容更新,而不会刷新整个页面。
Link 跳转做了三件事情:
- 有onclick那就执行onclick
- click的时候阻止a标签默认事件
- 根据跳转 href,用 history 跳转,此时只是链接变了,并没有刷新页面
而 a 标签就是普通的超链接了,用于从当前页面跳转到href指向的另一个页面(非锚点情况)。
19. PureComponent 和 Component的区别是?
- Component 需要手动实现 shouldComponentUpdate,
- PureComponent 通过浅对比默认实现了 shouldComponentUpdate 方法。
注意: PureComponent 不仅会影响本身,而且会影响子组件,所以 PureComponent 最佳情况是展示组件。
20. React 事件和原生事件的执行顺序
为什么要有合成事件?
- 在传统的事件里,不同的浏览器需要兼容不同的写法,在合成事件中React提供统一的事件对象,抹平了浏览器的兼容性差异
- React通过顶层监听的形式,通过事件委托的方式来统一管理所有的事件,可以在事件上区分事件优先级,优化用户体验
事件委托
-
事件委托的意思就是可以通过给父元素绑定事件委托,通过事件对象的target属性可以获取到当前触发目标阶段的dom元素,来进行统一管理
-
比如写原生dom循环渲染的时候,我们要给每一个子元素都添加dom事件,这种情况最简单的方式就是通过事件委托在父元素做一次委托,通过target属性判断区分做不同的操作
事件监听
事件监听主要用到了addEventListener这个函数,事件监听和事件绑定的最大区别就是事件监听可以给一个事件监听多个函数操作,而事件绑定只有一次
// 可以监听多个,不会被覆盖
eventTarget.addEventListener('click', () => {});
eventTarget.addEventListener('click', () => {});
eventTarget.onclick = function () {};
eventTarget.onclick = function () {}; // 第二个会把第一个覆盖
- 16版本先执行原生事件,当冒泡到document时,统一执行合成事件,
- 17版本在原生事件执行前先执行合成事件捕获阶段,原生事件执行完毕执行冒泡阶段的合成事件,通过根节点来管理所有的事件
原生的阻止事件流会阻断合成事件的执行,合成事件阻止后也会影响到后续的原生执行
21. Redux VS Vuex
相同点
- state 共享数据
- 流程一致:定义全局state,触发,修改state
- 原理相似,通过全局注入store。
不同点
-
从实现原理上来说:
- Redux 使用的是不可变数据,
- 而Vuex的数据是可变的。
- Redux每次都是用新的state替换旧的state,
- 而Vuex是直接修改
- Redux 在检测数据变化的时候,是通过 diff 的方式比较差异的,
- 而Vuex其实和Vue的原理一样,是通过 getter/setter来比较的
-
从表现层来说:
- vuex定义了state、getter、mutation、action四个对象;
- redux定义了state、reducer、action。
- vuex中state统一存放,方便理解;
- redux中state依赖所有reducer的初始值
- vuex有getter,目的是快捷得到state;
- redux没有这层,react-redux mapStateToProps参数做了这个工作。
- vuex中mutation只是单纯赋值(很浅的一层);
- redux中reducer只是单纯设置新state(很浅的一层)。
- vuex中action有较为复杂的异步ajax请求;
- redux中action中可简单可复杂,简单就直接发送数据对象({type:xxx, your-data}),复杂需要调用异步ajax(依赖redux-thunk插件)。
- vuex触发方式有两种commit同步和dispatch异步;
- redux同步和异步都使用dispatch
-
vuex 弱化 dispatch,通过commit进行 store状态的一次更变;
-
取消了action概念,不必传入特定的 action形式进行指定变更;
-
弱化reducer,基于commit参数直接对数据进行转变,使得框架更加简易;
共同思想
- 单一的数据源
- 变化可以预测
- 本质上∶ redux与vuex都是对mvvm思想的服务,将数据从视图中抽离的一种方案。
22. Mobx VS Redux
- redux将数据保存在单一的store中,
- mobx将数据保存在分散的多个store中
- redux使用plain object保存数据,需要手动处理变化后的操作;
- mobx适用observable保存数据,数据变化后自动处理响应的操作
- redux使用不可变状态,这意味着状态是只读的,不能直接去修改它,而是应该返回一个新的状态,同时使用纯函数;
- mobx中的状态是可变的,可以直接对其进行修改
- mobx相对来说比较简单,在其中有很多的抽象,mobx更多的使用面向对象的编程思维;
- redux会比较复杂,因为其中的函数式编程思想掌握起来不是那么容易,同时需要借助一系列的中间件来处理异步和副作用
- mobx中有更多的抽象和封装,调试会比较困难,同时结果也难以预测;
- 而redux提供能够进行时间回溯的开发工具,同时其纯函数以及更少的抽象,让调试变得更加的容易
23. React 中,父子组件的生命周期执行顺序
观察父子组件的挂载生命周期函数,可以发现:
- 挂载时,子组件的挂载钩子先被触发;
- 卸载时,子组件的卸载钩子后被触发。
我们经常在挂载函数上注册监听器,说明此时是可以与页面交互的,也就是说其实所有挂载钩子都是在父组件实际挂载到dom树上才触发的,不过是在父组件挂载后依次触发子组件的 componentDidmount ,最后再触发自身的挂载钩子。
相反,卸载的时候父节点先被移除,再从上至下依次触发子组件的卸载钩子;
但是我们也经常在卸载钩子上卸载监听器,这说明 componentWillUnmount 其实在父组件从dom树上卸载前触发的,先触发自身的卸载钩子,但此时并未从dom树上剥离,然后依次尝试触发所有子组件的卸载钩子,最后,父组件从dom树上完成实际卸载。
24. React render 方法原理?在什么时候触发?
render函数里面可以编写JSX,转化成createElement这种形式,用于生成虚拟DOM,最终转化成真实DOM
在 React 中,类组件只要执行了 setState 方法,就一定会触发 render 函数执行,函数组件使用useState更改状态不一定导致重新render
组件的 props 改变了,不一定触发 render 函数的执行,
但是如果 props 的值来自于父组件或者祖先组件的 state,在这种情况下,父组件或者祖先组件的 state 发生了改变,就会导致子组件的重新渲染
所以,一旦执行了setState就会执行render方法,useState 会判断当前值有无发生改变确定是否执行render方法,一旦父组件发生渲染,子组件也会渲染
25. React-router 几种模式,以及实现原理?
主要分成了两种模式:
hash 模式
:在url后面加上#,如http://127.0.0.1:5500/home/#/page1history 模式
:允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录
hash 原理
hash 值改变,触发全局 window 对象上的 hashchange
事件。所以 hash 模式路由就是利用 hashchange 事件监听 URL 的变化,从而进行 DOM 操作来模拟页面跳转
通过window.addEventListener('hashChange',callback)
监听hash值的变化,并传递给其嵌套的组件
26. React JSX 转换成真实 DOM 的过程?
- 使用
React.createElement
或JSX编写React组件,实际上所有的 JSX 代码最后都会转换成React.createElement(...) ,Babel
帮助我们完成了这个转换的过程。 - createElement函数对key和ref等特殊的props进行处理,并获取defaultProps对默认props进行赋值,并且对传入的孩子节点进行处理,最终构造成一个虚拟DOM对象
ReactDOM.render
将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理、事务等机制
并且对特定浏览器进行了性能优化,最终转换为真实DOM
27. React 服务端渲染(SSR)原理?
-
node server 接收客户端请求,得到当前的请求 url 路径,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 props、context或者store 形式传入组件
-
然后基于 react 内置的服务端渲染方法 renderToString() 把组件渲染为 html 字符串在把最终的 html 进行输出前需要将数据注入到浏览器端
-
浏览器开始进行渲染和节点对比,然后执行完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 html 节点,整个流程结束
28. 常用的 React Hooks
状态钩子 (useState)
: 用于定义组件的 State,类似类定义中 this.state 的功能useReducer
:用于管理复杂状态逻辑的替代方案,类似于 Redux 的 reducer。生命周期钩子 (useEffect)
: 类定义中有许多生命周期函数,而在 React Hooks 中也提供了一个相应的函数 (useEffect),这里可以看做componentDidMount、componentDidUpdate和componentWillUnmount的结合。useLayoutEffect
:与 useEffect 类似,但在浏览器完成绘制之前同步执行。useContext
: 获取 context 对象,用于在组件树中获取和使用共享的上下文。useCallback
: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;useMemo
: 用于缓存计算结果,避免重复计算昂贵的操作。useRef
: 获取组件的真实节点;用于在函数组件之间保存可变的值,并且不会引发重新渲染。useImperativeHandle
:用于自定义暴露给父组件的实例值或方法。useDebugValue
:用于在开发者工具中显示自定义的钩子相关标签。
29. useEffect VS useLayoutEffect
使用场景
:useEffect
在 React 的渲染过程中是被异步调用的,用于绝大多数场景;useLayoutEffect
会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。- 也正因为是同步处理,所以需要避免在 useLayoutEffect 做计算量较大的耗时任务从而造成阻塞。
使用效果
:useEffect
是按照顺序执行代码的,改变屏幕像素之后执行(先渲染,后改变DOM),当改变屏幕内容时可能会产生闪烁;useLayoutEffect
是改变屏幕像素之前就执行了(会推迟页面显示的事件,先改变DOM后渲染),不会产生闪烁。useLayoutEffect总是比useEffect先执行。
在未来的趋势上,两个 API 是会长期共存的,暂时没有删减合并的计划,需要开发者根据场景去自行选择。React 团队的建议非常实用,如果实在分不清,先用 useEffect,一般问题不大;如果页面有异常,再直接替换为 useLayoutEffect 即可。
30. JSX 的本质是什么?
JSX(JavaScript XML) 是一个 JavaScript 的语法扩展,允许在 JavaScript 代码中通过类 HTML 语法创建 React 元素。它需要通过 Babel 等工具编译为标准的 JavaScript 代码,最终生成 React 元素对象(React Element),这些元素共同构成虚拟 DOM(Virtual DOM)树。
核心原理
-
JSX 编译为 React 元素
JSX 会被转换为React.createElement()
调用(或 React 17+ 的_jsx
函数),生成描述 UI 结构的对象(React 元素),而非直接操作真实 DOM。// JSX const element = <h1 className="title">Hello, world!</h1> // 编译后(React 17 之前) const element = React.createElement('h1', { className: 'title' }, 'Hello, world!') // 编译后(React 17+,自动引入 _jsx) import { jsx as _jsx } from 'react/jsx-runtime' const element = _jsx('h1', { className: 'title', children: 'Hello, world!' })
-
虚拟 DOM 的运作
- React 元素组成虚拟 DOM 树,通过 Diff 算法对比新旧树差异,最终高效更新真实 DOM。
- 虚拟 DOM 是内存中的轻量对象,避免频繁操作真实 DOM 的性能损耗。
JSX 的核心特性
-
类 HTML 语法与 JavaScript 的融合
- 表达式嵌入:通过
{}
嵌入 JavaScript 表达式(如变量、函数调用、三元运算符):const userName = 'Alice' const element = <p>Hello, {userName.toUpperCase()}</p>
- 禁止语句:
{}
内不支持if
/for
等语句,需改用表达式(如三元运算符或逻辑与):<div>{isLoggedIn ? 'Welcome' : 'Please Login'}</div>
- 表达式嵌入:通过
-
语法规则
- 属性命名:使用驼峰命名(如
className
代替class
,htmlFor
代替for
)。 - 闭合标签:所有标签必须显式闭合(如
<img />
)。 - 单一根元素:JSX 必须有唯一根元素(或用
<></>
空标签包裹)。
- 属性命名:使用驼峰命名(如
-
安全性
- 默认 XSS 防护:JSX 自动转义嵌入内容中的特殊字符(如
<
转为<
)。 - 例外场景:如需渲染原始 HTML,需显式使用
dangerouslySetInnerHTML
(需谨慎):<div dangerouslySetInnerHTML={{ __html: userContent }} />
- 默认 XSS 防护:JSX 自动转义嵌入内容中的特殊字符(如
编译与工具链
-
编译流程
JSX 需通过 Babel 编译为浏览器可执行的 JavaScript。典型配置如下:// .babelrc { "presets": ["@babel/preset-react"] }
-
React 17+ 的优化
- 无需手动导入 React:编译器自动引入
_jsx
函数。 - 更简洁的编译输出:减少代码体积,提升可读性。
- 无需手动导入 React:编译器自动引入
31. 如何理解 React Fiber 架构?
- Fiber 架构的本质与设计目标
Fiber 是 React 16+ 的核心算法重写,本质是基于链表的增量式协调模型。其核心目标并非单纯提升性能,而是重构架构以实现:
- 可中断的异步渲染:将同步递归的调和过程拆解为可暂停/恢复的异步任务。
- 优先级调度:高优先级任务(如用户输入)可打断低优先级任务(如数据更新)。
- 并发模式基础:为
Suspense
、useTransition
等特性提供底层支持。
- Fiber 节点的核心设计
每个组件对应一个 Fiber 节点,构成双向链表树结构,包含以下关键信息:
- 组件类型:函数组件、类组件或原生标签。
- 状态与副作用:Hooks 状态(如
useState
)、生命周期标记(如useEffect
)。 - 调度信息:任务优先级(
lane
模型)、到期时间(expirationTime
)。 - 链表指针:
child
(子节点)、sibling
(兄弟节点)、return
(父节点)。
// Fiber 节点结构简化示例
const fiberNode = {
tag: FunctionComponent, // 组件类型
stateNode: ComponentFunc, // 组件实例或 DOM 节点
memoizedState: {
/* Hooks 链表 */
},
pendingProps: {
/* 待处理 props */
},
lanes: Lanes.HighPriority, // 任务优先级
child: nextFiber, // 子节点
sibling: null, // 兄弟节点
return: parentFiber, // 父节点
}
- Fiber 协调流程(两阶段提交)
阶段 1:Reconciliation(协调/渲染阶段)
- 可中断的增量计算:
React 将组件树遍历拆解为多个 Fiber 工作单元,通过循环(而非递归)逐个处理。- 每次循环执行一个 Fiber 节点,生成子 Fiber 并连接成树。
- 通过
requestIdleCallback
(或 Scheduler 包)在浏览器空闲时段执行,避免阻塞主线程。
- 对比策略:
根据key
和type
复用节点,标记Placement
(新增)、Update
(更新)、Deletion
(删除)等副作用。
阶段 2:Commit(提交阶段)
- 不可中断的 DOM 更新:
同步执行所有标记的副作用(如 DOM 操作、生命周期调用),确保 UI 一致性。 - 副作用分类:
- BeforeMutation:
getSnapshotBeforeUpdate
。 - Mutation:DOM 插入/更新/删除。
- Layout:
useLayoutEffect
、componentDidMount
/Update
。
- BeforeMutation:
- 优先级调度机制
React 通过 Lane 模型 管理任务优先级(共 31 个优先级车道):
- 事件优先级:
// 优先级从高到低 ImmediatePriority(用户输入) UserBlockingPriority(悬停、点击) NormalPriority(数据请求) LowPriority(分析日志) IdlePriority(非必要任务)
- 调度策略:
- 高优先级任务可抢占低优先级任务的执行权。
- 过期任务(如 Suspense 回退)会被强制同步执行。
- Fiber 架构的优势与局限性
优势
- 流畅的用户体验:异步渲染避免主线程阻塞,保障高优先级任务即时响应。
- 复杂场景优化:支持大规模组件树的高效更新(如虚拟滚动、动画串联)。
- 未来特性基础:为并发模式(Concurrent Mode)、离线渲染(SSR)提供底层支持。
局限性
- 学习成本高:开发者需理解底层调度逻辑以优化性能。
- 内存开销:Fiber 树的双向链表结构比传统虚拟 DOM 占用更多内存。
- 与旧架构的关键差异
特性 | Stack Reconciler(React 15-) | Fiber Reconciler(React 16+) |
---|---|---|
遍历方式 | 递归(不可中断) | 循环(可中断 + 恢复) |
任务调度 | 同步执行,阻塞主线程 | 异步分片,空闲时段执行 |
优先级控制 | 无 | 基于 Lane 模型的优先级抢占 |
数据结构 | 虚拟 DOM 树 | Fiber 链表树(含调度信息) |
32. Fiber 结构和普通 VNode 有什么区别?
- 本质差异
维度 | 普通 VNode(虚拟 DOM) | Fiber 结构 |
---|---|---|
设计目标 | 减少真实 DOM 操作,提升渲染性能 | 实现可中断的异步渲染 + 优先级调度 |
数据结构 | 树形结构(递归遍历) | 双向链表树(循环遍历) |
功能范畴 | 仅描述 UI 结构 | 描述 UI 结构 + 调度任务 + 副作用管理 |
- 数据结构对比
普通 VNode(React 15 及之前)
const vNode = {
type: 'div', // 节点类型(组件/原生标签)
props: { className: 'container' }, // 属性
children: [vNode1, vNode2], // 子节点(树形结构)
key: 'unique-id', // 优化 Diff 性能
// 无状态、调度、副作用信息
}
- 核心字段:仅包含 UI 描述相关属性(type、props、children)。
Fiber 节点(React 16+)
const fiberNode = {
tag: HostComponent, // 节点类型(函数组件/类组件/DOM元素)
type: 'div', // 原生标签或组件构造函数
key: 'unique-id', // Diff 优化标识
stateNode: domNode, // 关联的真实 DOM 节点
pendingProps: { className: 'container' }, // 待处理的 props
memoizedProps: {}, // 已生效的 props
memoizedState: {
// Hooks 状态(函数组件)
hooks: [state1, effectHook],
},
updateQueue: [], // 状态更新队列(类组件)
lanes: Lanes.HighPriority, // 调度优先级(Lane 模型)
child: childFiber, // 第一个子节点
sibling: siblingFiber, // 下一个兄弟节点
return: parentFiber, // 父节点(构成双向链表)
effectTag: Placement, // 副作用标记(插入/更新/删除)
nextEffect: nextEffectFiber, // 副作用链表指针
}
- 核心扩展:
- 调度控制:
lanes
优先级、任务到期时间。 - 状态管理:Hooks 链表(函数组件)、类组件状态队列。
- 副作用追踪:
effectTag
标记和副作用链表。 - 遍历结构:
child
/sibling
/return
构成双向链表。
- 调度控制:
- 协调机制对比
流程 | VNode(Stack Reconciler) | Fiber Reconciler |
---|---|---|
遍历方式 | 递归遍历(不可中断) | 循环遍历链表(可中断 + 恢复) |
任务调度 | 同步执行,阻塞主线程 | 异步分片,空闲时间执行 |
优先级控制 | 无 | Lane 模型(31 个优先级车道) |
副作用处理 | 统一提交 DOM 更新 | 构建副作用链表,分阶段提交 |
- Fiber 两阶段提交:
- 协调阶段(可中断):
- 增量构建 Fiber 树,标记副作用(
effectTag
)。 - 通过
requestIdleCallback
或 Scheduler 包分片执行。
- 增量构建 Fiber 树,标记副作用(
- 提交阶段(同步不可中断):
- 遍历副作用链表,执行 DOM 操作和生命周期方法。
- 协调阶段(可中断):
-
能力扩展示例
a. 支持 Hooks 状态管理
- Fiber 节点通过
memoizedState
字段存储 Hooks 链表:
// 函数组件的 Hooks 链表
fiberNode.memoizedState = {
memoizedState: 'state value', // useState 的状态
next: {
// 下一个 Hook(如 useEffect)
memoizedState: { cleanup: fn },
next: null,
},
}
- VNode 无状态管理能力,仅描述 UI。
b. 优先级调度实战
- 高优先级任务抢占:
// 用户输入触发高优先级更新 input.addEventListener('input', () => { React.startTransition(() => { setInputValue(e.target.value) // 低优先级 }) // 高优先级更新立即执行 })
- VNode 架构无法实现任务中断和优先级插队。
c. 副作用批处理
- Fiber 通过
effectList
链表收集所有变更,统一提交:// 提交阶段遍历 effectList let nextEffect = fiberRoot.firstEffect while (nextEffect) { commitWork(nextEffect) nextEffect = nextEffect.nextEffect }
- VNode 架构在 Diff 后直接操作 DOM,无批处理优化。
- 性能影响对比
场景 | VNode 架构 | Fiber 架构 |
---|---|---|
大型组件树渲染 | 主线程阻塞导致掉帧 | 分片渲染,保持 UI 响应 |
高频更新(如动画) | 多次渲染合并困难 | 基于优先级合并或跳过中间状态 |
SSR 水合(Hydration) | 全量同步处理 | 增量水合,优先交互部分 |
33. 简述 React diff 算法过程
React Diff 算法通过 分层对比策略 和 启发式规则 减少树对比的时间复杂度(从 O(n³) 优化至 O(n))。其核心流程如下:
1. 分层对比策略
React 仅对 同一层级的兄弟节点 进行对比,若节点跨层级移动(如从父节点 A 移动到父节点 B),则直接 销毁并重建,而非移动。
原因:跨层操作在真实 DOM 中成本极高(需递归遍历子树),而实际开发中跨层移动场景极少,此策略以概率换性能。
2. 节点类型比对规则
a. 元素类型不同
若新旧节点类型不同(如 <div>
→ <span>
或 ComponentA
→ ComponentB
),则:
- 销毁旧节点及其子树。
- 创建新节点及子树,并插入 DOM。
// 旧树
<div>
<ComponentA />
</div>
// 新树 → 直接替换
<span>
<ComponentB />
</span>
b. 元素类型相同
若类型相同,则复用 DOM 节点并更新属性:
- 原生标签:更新
className
、style
等属性。 - 组件类型:
- 类组件:保留实例,触发
componentWillReceiveProps
→shouldComponentUpdate
等生命周期。 - 函数组件:重新执行函数,通过 Hooks 状态判断是否需更新。
- 类组件:保留实例,触发
// 旧组件(保留实例并更新 props)
<Button className="old" onClick={handleClick} />
// 新组件 → 复用 DOM,更新 className 和 onClick
<Button className="new" onClick={newClick} />
3. 列表节点的 Key 优化
处理子节点列表时,React 依赖 key 进行最小化更新:
a. 无 key 时的默认行为
默认使用 索引匹配(index-based diff),可能导致性能问题:
// 旧列表
;[<div>A</div>, <div>B</div>][
// 新列表(首部插入)→ 索引对比导致 B 被误判更新
((<div>C</div>), (<div>A</div>), (<div>B</div>))
]
此时 React 会认为索引 0 从 A → C(更新),索引 1 从 B → A(更新),并新增索引 2 的 B,实际应仅插入 C。
b. 使用 key 的优化匹配
通过唯一 key 标识节点身份,React 可精准识别移动/新增/删除:
// 正确使用 key(如数据 ID)
<ul>
{items.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
匹配规则:
-
遍历新列表,通过 key 查找旧节点:
- 找到且类型相同 → 复用节点。
- 未找到 → 新建节点。
-
记录旧节点中未被复用的节点 → 执行删除。
c. 节点移动优化
若新旧列表节点仅顺序变化,React 通过 key 匹配后,仅执行 DOM 移动操作(非重建),例如:
// 旧列表:A (key=1), B (key=2)
// 新列表:B (key=2), A (key=1)
// React 仅交换 DOM 顺序,而非销毁重建
4. 性能边界策略
- 子树跳过:若父节点类型变化,其子节点即使未变化也会被整体销毁。
- 相同组件提前终止:若组件
shouldComponentUpdate
返回false
,则跳过其子树 Diff。
34. 简述 React 和 Vue diff 算法的区别
React 和 Vue 的 Diff 算法均基于虚拟 DOM,但在实现策略、优化手段和设计哲学上存在显著差异:
1. 核心算法策略对比
维度 | React | Vue 2/3 |
---|---|---|
遍历方式 | 单向递归(同层顺序对比) | 双端对比(头尾指针优化) |
节点复用 | 类型相同则复用,否则销毁重建 | 类型相同则尝试复用,优先移动而非重建 |
静态优化 | 需手动优化(如 React.memo ) | 编译阶段自动标记静态节点 |
更新粒度 | 组件级更新(默认) | 组件级 + 块级(Vue3 Fragments) |
2. 列表 Diff 实现细节
a. React 的索引对比策略
- 无 key 时:按索引顺序对比,可能导致无效更新
// 旧列表:[A, B, C] // 新列表:[D, A, B, C](插入头部) // React 对比结果:更新索引 0-3,性能低下
- 有 key 时:通过 key 匹配节点,减少移动操作
// key 匹配后,仅插入 D,其他节点不更新
b. Vue 的双端对比策略
分四步优化对比效率(Vue2 核心逻辑,Vue3 优化为最长递增子序列):
- 头头对比:新旧头指针节点相同则复用,指针后移
- 尾尾对比:新旧尾指针节点相同则复用,指针前移
- 头尾交叉对比:旧头 vs 新尾,旧尾 vs 新头
- 中间乱序对比:建立 key-index 映射表,复用可匹配节点
// 旧列表:[A, B, C, D]
// 新列表:[D, A, B, C]
// Vue 通过步骤3头尾对比,仅移动 D 到头部
3. 静态优化机制
a. Vue 的编译时优化
-
静态节点标记:
模板中的静态节点(无响应式绑定)会被编译为常量,跳过 Diff<!-- 编译前 --> <div>Hello Vue</div> <!-- 编译后 --> _hoisted_1 = createVNode("div", null, "Hello Vue")
-
Block Tree(Vue3):
动态节点按区块(Block)组织,Diff 时仅对比动态部分
b. React 的运行时优化
- 手动控制更新:
需通过React.memo
、shouldComponentUpdate
或useMemo
避免无效渲染const MemoComp = React.memo(() => <div>Static Content</div>)
4. 响应式更新触发
框架 | 机制 | Diff 触发条件 |
---|---|---|
React | 状态变化触发组件重新渲染 | 父组件渲染 → 子组件默认递归 Diff |
Vue | 响应式数据变更触发组件更新 | 依赖收集 → 仅受影响组件触发 Diff |
// Vue:只有 data.value 变化才会触发更新
const vm = new Vue({ data: { value: 1 } })
// React:需显式调用 setState
const [value, setValue] = useState(1)
5. 设计哲学差异
维度 | React | Vue |
---|---|---|
控制粒度 | 组件级控制(开发者主导) | 细粒度依赖追踪(框架主导) |
优化方向 | 运行时优化(Fiber 调度) | 编译时优化(模板静态分析) |
适用场景 | 大型动态应用(需精细控制) | 中小型应用(快速开发) |
35. 为何 React JSX 循环需要使用 key
?
- 元素的高效识别与复用
React 通过 key
唯一标识列表中的每个元素。当列表发生变化(增删改排序)时,React 会通过 key
快速判断:
- 哪些元素是新增的(需要创建新 DOM 节点)
- 哪些元素是移除的(需要销毁旧 DOM 节点)
- 哪些元素是移动的(直接复用现有 DOM 节点,仅调整顺序)
如果没有 key
,React 会默认使用数组索引(index
)作为标识,这在动态列表中会导致 性能下降 或 状态错误。
- 避免状态混乱
如果列表项是 有状态的组件(比如输入框、勾选框等),错误的 key
会导致状态与错误的内容绑定。例如:
// 如果初始列表是 [A, B],用索引 index 作为 key:
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
// 在头部插入新元素变为 [C, A, B] 时:
// React 会认为 key=0 → C(重新创建)
// key=1 → A(复用原 key=0 的 DOM,但状态可能残留)
// 此时,原本属于 A 的输入框状态可能会错误地出现在 C 中。
- 提升渲染性能
通过唯一且稳定的 key
(如数据 ID),React 可以精准判断如何复用 DOM 节点。如果使用随机数或索引,每次渲染都会强制重新创建所有元素,导致性能浪费。
36. React 事件和 DOM 事件有什么区别?
- 事件绑定方式
-
React 事件
使用驼峰命名法(如onClick
、onChange
),通过 JSX 属性直接绑定函数:<button onClick={handleClick}>点击</button>
-
DOM 事件
使用全小写命名(如onclick
、onchange
),通过字符串或addEventListener
绑定:<button onclick="handleClick()">点击</button>
button.addEventListener('click', handleClick)
- 事件对象(Event Object)
-
React 事件
使用合成事件(SyntheticEvent),是原生事件对象的跨浏览器包装。- 通过
e.nativeEvent
访问原生事件。 - 事件对象会被复用(事件池机制),异步访问需调用
e.persist()
。
const handleClick = (e) => { e.persist() // 保持事件对象引用 setTimeout(() => console.log(e.target), 100) }
- 通过
-
DOM 事件
直接使用浏览器原生事件对象,无复用机制。button.addEventListener('click', (e) => { console.log(e.target) // 直接访问 })
- 事件传播与默认行为
-
React 事件
- 阻止默认行为:必须显式调用
e.preventDefault()
。 - 阻止冒泡:调用
e.stopPropagation()
。
const handleSubmit = (e) => { e.preventDefault() // 阻止表单默认提交 e.stopPropagation() // 阻止事件冒泡 }
- 阻止默认行为:必须显式调用
-
DOM 事件
- 阻止默认行为:可调用
e.preventDefault()
或return false
(在 HTML 属性中)。 - 阻止冒泡:调用
e.stopPropagation()
或return false
(仅部分情况)。
<form onsubmit="return false"> <!-- 阻止默认提交 --> <button onclick="event.stopPropagation()">按钮</button> </form>
- 阻止默认行为:可调用
- 性能优化
-
React 事件
采用事件委托机制:- React 17 之前将事件委托到
document
层级。 - React 17+ 改为委托到渲染的根容器(如
ReactDOM.render
挂载的节点)。 - 减少内存占用,动态添加元素无需重新绑定事件。
- React 17 之前将事件委托到
-
DOM 事件
直接绑定到元素,大量事件监听时可能导致性能问题。
- 跨浏览器兼容性
-
React 事件
合成事件抹平了浏览器差异(如event.target
的一致性),无需处理兼容性问题。 -
DOM 事件
需手动处理浏览器兼容性(如 IE 的attachEvent
vs 标准addEventListener
)。
this
绑定
-
React 事件
类组件中需手动绑定this
或使用箭头函数:class MyComponent extends React.Component { handleClick() { console.log(this) // 需绑定,否则为 undefined } render() { return <button onClick={this.handleClick.bind(this)}>点击</button> } }
-
DOM 事件
事件处理函数中的this
默认指向触发事件的元素:button.addEventListener('click', function () { console.log(this) // 指向 button 元素 })
特性 | React 事件 | DOM 事件 |
---|---|---|
命名规则 | 驼峰命名(onClick ) | 全小写(onclick ) |
事件对象 | 合成事件(SyntheticEvent ) | 原生事件对象 |
默认行为阻止 | e.preventDefault() | e.preventDefault() 或 return false |
事件委托 | 自动委托到根容器 | 需手动实现 |
跨浏览器兼容 | 内置处理 | 需手动适配 |
this 指向 | 类组件中需手动绑定 | 默认指向触发元素 |
React 事件系统通过抽象和优化,提供了更高效、一致的事件处理方式,避免了直接操作 DOM 的繁琐和兼容性问题。
37. 简述 React batchUpdate 机制
React 的 batchUpdate(批处理更新)机制 是一种优化策略,旨在将多个状态更新合并为一次渲染,减少不必要的组件重新渲染次数,从而提高性能。
核心机制
-
异步合并更新
当在 同一执行上下文(如同一个事件处理函数、生命周期方法或 React 合成事件)中多次调用状态更新(如setState
、useState
的setter
函数),React 不会立即触发渲染,而是将多个更新收集到一个队列中,最终合并为一次更新,统一计算新状态并渲染。 -
更新队列
React 内部维护一个更新队列。在触发更新的代码块中,所有状态变更会被暂存到队列,直到代码执行完毕,React 才会一次性处理队列中的所有更新,生成新的虚拟 DOM,并通过 Diff 算法高效更新真实 DOM。
触发批处理的场景
-
React 合成事件
如onClick
、onChange
等事件处理函数中的多次状态更新会自动批处理。const handleClick = () => { setCount(1) // 更新入队 setName('Alice') // 更新入队 // 最终合并为一次渲染 }
-
React 生命周期函数
在componentDidMount
、componentDidUpdate
等生命周期方法中的更新会被批处理。 -
React 18+ 的自动批处理增强
React 18 引入createRoot
后,即使在异步操作(如setTimeout
、Promise
、原生事件回调)中的更新也会自动批处理:setTimeout(() => { setCount(1) // React 18 中自动批处理 setName('Alice') // 合并为一次渲染 }, 1000)
绕过批处理的场景
-
React 17 及之前的异步代码
在setTimeout
、Promise
或原生事件回调中的更新默认不会批处理,每次setState
触发一次渲染:// React 17 中会触发两次渲染 setTimeout(() => { setCount(1) // 渲染一次 setName('Alice') // 渲染第二次 }, 1000)
-
手动强制同步更新
使用flushSync
(React 18+)可强制立即更新,绕过批处理:import { flushSync } from 'react-dom' flushSync(() => { setCount(1) // 立即渲染 }) setName('Alice') // 再次渲染
设计目的
-
性能优化
避免频繁的 DOM 操作,减少浏览器重绘和回流,提升应用性能。 -
状态一致性
确保在同一个上下文中多次状态变更后,组件最终基于最新的状态值渲染,避免中间状态导致的 UI 不一致。
示例对比
-
自动批处理(React 18+)
const handleClick = () => { setCount((prev) => prev + 1) // 更新入队 setCount((prev) => prev + 1) // 更新入队 // 最终 count 增加 2,仅一次渲染 }
-
非批处理(React 17 异步代码)
setTimeout(() => { setCount((prev) => prev + 1) // 渲染一次 setCount((prev) => prev + 1) // 再渲染一次 // React 17 中触发两次渲染,count 仍为 2 }, 1000)
场景 | React 17 及之前 | React 18+(使用 createRoot ) |
---|---|---|
合成事件/生命周期 | 自动批处理 | 自动批处理 |
异步操作 | 不批处理 | 自动批处理 |
原生事件回调 | 不批处理 | 自动批处理 |
React 的批处理机制通过合并更新减少了渲染次数,但在需要即时反馈的场景(如动画)中,可通过 flushSync
强制同步更新。
:::
38. 简述 React 事务机制
React 的 事务机制(Transaction) 是早期版本(React 16 之前)中用于 批量处理更新 和 管理副作用 的核心设计模式,其核心思想是通过“包装”操作流程,确保在更新过程中执行特定的前置和后置逻辑(如生命周期钩子、事件监听等)。随着 React Fiber 架构的引入,事务机制逐渐被更灵活的调度系统取代。
核心概念
-
事务的定义
事务是一个包含 初始化阶段、执行阶段 和 收尾阶段 的流程控制单元。每个事务通过Transaction
类实现,提供initialize
和close
方法,用于在操作前后插入逻辑。例如:const MyTransaction = { initialize() { /* 前置操作(如记录状态) */ }, close() { /* 后置操作(如触发更新) */ }, }
-
包装函数
事务通过perform
方法执行目标函数,将其包裹在事务的生命周期中:function myAction() { /* 核心逻辑(如调用 setState) */ } MyTransaction.perform(myAction)
在 React 中的应用场景
-
批量更新(Batching Updates)
在事件处理或生命周期方法中,多次调用setState
会被事务合并为一次更新。例如:class Component { onClick() { // 事务包裹下的多次 setState 合并为一次渲染 this.setState({ a: 1 }) this.setState({ b: 2 }) } }
-
生命周期钩子的触发
在组件挂载或更新时,事务确保componentWillMount
、componentDidMount
等钩子在正确时机执行。 -
事件系统的委托
合成事件(如onClick
)的处理逻辑通过事务绑定和解绑,确保事件监听的一致性和性能优化。
事务的工作流程
- 初始化阶段
执行所有事务的initialize
方法(如记录当前 DOM 状态、锁定事件监听)。 - 执行目标函数
运行核心逻辑(如用户定义的setState
或事件处理函数)。 - 收尾阶段
执行所有事务的close
方法(如对比 DOM 变化、触发更新、解锁事件)。
事务机制的局限性
- 同步阻塞
事务的执行是同步且不可中断的,无法支持异步优先级调度(如 Concurrent Mode 的时间切片)。 - 复杂性高
事务的嵌套和组合逻辑复杂,难以维护和扩展。
Fiber 架构的演进 React 16 引入的 Fiber 架构 替代了事务机制,核心改进包括:
- 异步可中断更新
通过 Fiber 节点的链表结构,支持暂停、恢复和优先级调度。 - 更细粒度的控制
将渲染拆分为多个阶段(如render
和commit
),副作用管理更灵活。 - 替代批量更新策略
使用调度器(Scheduler)和优先级队列实现更高效的批处理(如 React 18 的自动批处理)。
特性 | 事务机制(React <16) | Fiber 架构(React 16+) |
---|---|---|
更新方式 | 同步批量更新 | 异步可中断、优先级调度 |
副作用管理 | 通过事务生命周期控制 | 通过 Effect Hook、提交阶段处理 |
复杂度 | 高(嵌套事务逻辑复杂) | 高(但更模块化和可扩展) |
适用场景 | 简单同步更新 | 复杂异步渲染(如动画、懒加载) |
事务机制是 React 早期实现批量更新的基石,但其同步设计无法满足现代前端应用的复杂需求。Fiber 架构通过解耦渲染过程,为 Concurrent Mode 和 Suspense 等特性奠定了基础,成为 React 高效渲染的核心。 :::
39. 如何理解 React concurrency 并发机制
React 的并发机制(Concurrency)是 React 18 引入的一项重要特性,旨在提升应用的响应性和性能。
1. 什么是 React 的并发机制?
React 的并发机制允许 React 在渲染过程中根据任务的优先级进行调度和中断,从而确保高优先级的更新能够及时渲染,而不会被低优先级的任务阻塞。
2. 并发机制的工作原理:
-
时间分片(Time Slicing): React 将渲染任务拆分为多个小片段,每个片段在主线程空闲时执行。这使得浏览器可以在渲染过程中处理用户输入和其他高优先级任务,避免长时间的渲染阻塞用户交互。
-
优先级调度(Priority Scheduling): React 为不同的更新分配不同的优先级。高优先级的更新(如用户输入)会被优先处理,而低优先级的更新(如数据预加载)可以在空闲时处理。
-
可中断渲染(Interruptible Rendering): 在并发模式下,React 可以中断当前的渲染任务,处理更高优先级的任务,然后再恢复之前的渲染。这确保了应用在长时间渲染过程中仍能保持响应性。
3. 并发机制的优势:
-
提升响应性: 通过优先处理高优先级任务,React 能够更快地响应用户输入,提升用户体验。
-
优化性能: 将渲染任务拆分为小片段,避免长时间的渲染阻塞,提升应用的整体性能。
-
更好的资源利用: 在主线程空闲时处理低优先级任务,充分利用系统资源。
4. 如何启用并发模式:
要在 React 应用中启用并发模式,需要使用 createRoot
API:
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<App />)
在并发模式下,React 会自动根据任务的优先级进行调度和渲染。
40. 简述 React reconciliation 协调的过程
41. React 组件渲染和更新的全过程
React 组件的渲染和更新过程涉及多个阶段,包括 初始化、渲染、协调、提交、清理 等。以下是 React 组件渲染和更新的全过程,结合源码逻辑和关键步骤进行详细分析。
1. 整体流程概述 React 的渲染和更新过程可以分为以下几个阶段:
- 初始化阶段:创建 Fiber 树和 Hooks 链表。
- 渲染阶段:生成新的虚拟 DOM(Fiber 树)。
- 协调阶段:对比新旧 Fiber 树,找出需要更新的部分。
- 提交阶段:将更新应用到真实 DOM。
- 清理阶段:重置全局变量,准备下一次更新。
2. 详细流程分析
(1)初始化阶段
- 触发条件:组件首次渲染或状态/属性更新。
- 关键函数:
render
、createRoot
、scheduleUpdateOnFiber
。 - 逻辑:
- 通过
ReactDOM.render
或createRoot
初始化应用。 - 创建根 Fiber 节点(
HostRoot
)。 - 调用
scheduleUpdateOnFiber
,将更新任务加入调度队列。
- 通过
(2)渲染阶段
- 触发条件:调度器开始执行任务。
- 关键函数:
performSyncWorkOnRoot
、beginWork
、renderWithHooks
。 - 逻辑:
- 调用
performSyncWorkOnRoot
,开始渲染任务。 - 调用
beginWork
,递归处理 Fiber 节点。 - 对于函数组件,调用
renderWithHooks
,执行组件函数并生成新的 Hooks 链表。 - 对于类组件,调用
instance.render
,生成新的虚拟 DOM。 - 对于 Host 组件(如
div
),生成对应的 DOM 节点。
- 调用
(3)协调阶段
- 触发条件:新的虚拟 DOM 生成后。
- 关键函数:
reconcileChildren
、diff
。 - 逻辑:
- 调用
reconcileChildren
,对比新旧 Fiber 节点。 - 根据
diff
算法,找出需要更新的节点。 - 为需要更新的节点打上
Placement
、Update
、Deletion
等标记。
- 调用
(4)提交阶段
- 触发条件:协调阶段完成后。
- 关键函数:
commitRoot
、commitWork
。 - 逻辑:
- 调用
commitRoot
,开始提交更新。 - 调用
commitWork
,递归处理 Fiber 节点。 - 根据节点的标记,执行 DOM 操作(如插入、更新、删除)。
- 调用生命周期钩子(如
componentDidMount
、componentDidUpdate
)。
- 调用
(5)清理阶段
- 触发条件:提交阶段完成后。
- 关键函数:
resetHooks
、resetContext
。 - 逻辑:
- 重置全局变量(如
currentlyRenderingFiber
、currentHook
)。 - 清理上下文和副作用。
- 准备下一次更新。
- 重置全局变量(如
42. 为何 React Hooks 不能放在条件或循环之内?
一个组件中的hook会以链表的形式串起来, FiberNode 的 memoizedState 中保存了 Hooks 链表中的第一个Hook。
在更新时,会复用之前的 Hook,如果通过了条件或循环语句,增加或者删除 hooks,在复用 hooks 过程中,会产生复用 hooks 状态和当前 hooks 不一致的问题。