面试题
编程题 1
给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的 排列。如果是,返回 true ;否则,返回 false 。
换句话说,s1 的排列之一是 s2 的 子串 。
示例 1:
输入:s1 = "ab" s2 = "eidbaooo"
输出:true
解释:s2 包含 s1 的排列之一 ("ba").
示例 2:
输入:s1= "ab" s2 = "eidboaoo"
输出:false解答: 要解决这个问题,我们需要判断字符串s2是否包含s1的任意排列作为其子串。这里的"排列"指的是s1的字符重新排列后形成的字符串,因此子串的长度必须与s1相同,且字符出现的频率也必须完全一致。
我们可以使用滑动窗口结合字符频率统计的方法来高效解决这个问题,具体思路如下:
/**
* 判断s2是否包含s1的排列
* @param {string} s1 - 源字符串
* @param {string} s2 - 目标字符串
* @return {boolean} - 是否包含排列
*/
var checkInclusion = function(s1, s2) {
const len1 = s1.length;
const len2 = s2.length;
// 如果s1比s2长,直接返回false
if (len1 > len2) return false;
// 初始化两个数组用于统计字符频率(假设都是小写字母)
const count1 = new Array(26).fill(0);
const count2 = new Array(26).fill(0);
// 统计s1和s2中前len1个字符的频率
for (let i = 0; i < len1; i++) {
count1[s1.charCodeAt(i) - 'a'.charCodeAt(0)]++;
count2[s2.charCodeAt(i) - 'a'.charCodeAt(0)]++;
}
// 检查初始窗口是否匹配
if (arraysEqual(count1, count2)) return true;
// 滑动窗口遍历s2剩余部分
for (let i = len1; i < len2; i++) {
// 移除窗口最左侧的字符
count2[s2.charCodeAt(i - len1) - 'a'.charCodeAt(0)]--;
// 添加当前字符到窗口
count2[s2.charCodeAt(i) - 'a'.charCodeAt(0)]++;
// 检查当前窗口是否匹配
if (arraysEqual(count1, count2)) return true;
}
// 所有窗口都不匹配
return false;
};
/**
* 辅助函数:判断两个数组是否相等
* @param {Array} arr1 - 数组1
* @param {Array} arr2 - 数组2
* @return {boolean} - 是否相等
*/
function arraysEqual(arr1, arr2) {
for (let i = 0; i < 26; i++) {
if (arr1[i] !== arr2[i]) return false;
}
return true;
}代码解析
边界处理:如果
s1的长度大于s2,直接返回false,因为不可能存在符合条件的子串。字符频率统计:
- 使用两个长度为26的数组(对应26个小写字母)分别统计
s1和s2中字符的出现频率。 - 这样做比使用哈希表更高效,因为可以直接通过字符的ASCII码计算索引。
- 使用两个长度为26的数组(对应26个小写字母)分别统计
滑动窗口初始化:
- 首先统计
s1的字符频率,以及s2中前len1个字符(与s1长度相同)的频率。 - 比较这两个频率数组,如果相同则说明
s2的前len1个字符就是s1的一个排列,返回true。
- 首先统计
滑动窗口移动:
- 从
s2的第len1个字符开始,每次移动窗口时:- 移除窗口最左侧的字符(减少其频率计数)
- 添加当前字符到窗口右侧(增加其频率计数)
- 每次移动后都比较两个频率数组,如果匹配则返回
true。
- 从
辅助函数:
arraysEqual用于比较两个频率数组是否完全相同,只需检查26个位置的计数是否一致。
时间复杂度和空间复杂度
- 时间复杂度:O(n),其中n是
s2的长度。每个字符最多被处理两次(加入窗口和移出窗口),比较两个数组的操作是O(26),可视为常数时间。 - 空间复杂度:O(1),只使用了两个固定大小的数组(26个元素),空间消耗与输入规模无关。
这种方法高效地利用了滑动窗口和字符频率统计,避免了生成所有可能的排列,大大提高了算法性能。
编程题 2
2:用React hooks设计一个useFetch实现api调用
要求:
1)传入参数,url:string, options: object(fetch函数第二个参数)
2)返回data,loading,error
3)考虑终止请求等情况解答:
import { useState, useEffect, useRef } from 'react';
function useFetch(url, options = {}) {
// 状态管理
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// 用于存储当前请求的AbortController
const abortControllerRef = useRef(null);
useEffect(() => {
// 创建新的AbortController
abortControllerRef.current = new AbortController();
const { signal } = abortControllerRef.current;
// 标记为加载中
setLoading(true);
// 重置错误状态
setError(null);
// 执行请求
const fetchData = async () => {
try {
// 发起请求,传入signal用于取消请求
const response = await fetch(url, {
...options,
signal
});
// 检查请求是否成功
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 解析响应数据,支持JSON和文本
const contentType = response.headers.get('content-type');
let result;
if (contentType && contentType.includes('application/json')) {
result = await response.json();
} else {
result = await response.text();
}
// 更新数据状态
setData(result);
} catch (err) {
// 忽略AbortError(主动取消的错误)
if (err.name !== 'AbortError') {
setError(err);
}
} finally {
// 无论成功失败,都标记为加载完成
setLoading(false);
}
};
// 执行请求
fetchData();
// 清理函数:组件卸载或依赖变化时取消请求
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, [url, JSON.stringify(options)]); // 当url或options变化时重新请求
return { data, loading, error };
}
export default useFetch;输出下面的结果
要理解这些 instanceof 表达式的结果,首先需要明确 instanceof 的核心逻辑:判断左侧的对象是否是右侧构造函数的实例,具体来说,它会检查“右侧构造函数的 prototype 属性”是否存在于“左侧对象的原型链上”。
同时需要注意:instanceof 左侧必须是对象(非基本类型),右侧必须是构造函数(本质是对象,且有 prototype 属性),否则会报错。
1. Object instanceof Object // true
- 左侧:
Object是构造函数(本质是函数对象)。 - 右侧:
Object构造函数,其prototype是Object.prototype(所有对象的原型链终点)。
原型链分析:Object 作为构造函数,它的原型链是:Object → Function.prototype(因为所有构造函数都是 Function 的实例)→ Object.prototype(Function.prototype 的原型是 Object.prototype)→ null。
右侧 Object 的 prototype 是 Object.prototype,而 Object 的原型链中包含 Object.prototype,因此返回 true。
2. Function instanceof Function // true
- 左侧:
Function是构造函数(函数对象)。 - 右侧:
Function构造函数,其prototype是Function.prototype(所有函数的原型)。
原型链分析:Function 是一个特殊的构造函数,它的原型是 Function.prototype(即“自身的 prototype”)。因此其原型链是:Function → Function.prototype → Object.prototype → null。
右侧 Function 的 prototype 是 Function.prototype,而 Function 的原型链中包含 Function.prototype,因此返回 true。
3. Number instanceof Number // false
- 左侧:
Number是构造函数(函数对象)。 - 右侧:
Number构造函数,其prototype是Number.prototype(数字包装对象的原型)。
原型链分析:Number 作为构造函数,是 Function 的实例,其原型链是:Number → Function.prototype → Object.prototype → null。
右侧 Number 的 prototype 是 Number.prototype,但 Number 的原型链中不包含 Number.prototype(它的原型是 Function.prototype),因此返回 false。
4. Number instanceof Function // true
- 左侧:
Number是构造函数(函数对象)。 - 右侧:
Function构造函数,其prototype是Function.prototype。
原型链分析:
所有构造函数(Number、String、Object、Function 等)本质都是 Function 的实例,因此 Number 的原型链起点是 Function.prototype:Number → Function.prototype → Object.prototype → null。
右侧 Function 的 prototype 是 Function.prototype,而 Number 的原型链中包含 Function.prototype,因此返回 true。
5. 1 instanceof Number // false
- 左侧:
1是基本数据类型(number类型),不是对象。 - 右侧:
Number是构造函数。
关键原因:instanceof 仅对对象有效,基本数据类型(number、string、boolean 等)不是对象,因此 1 不可能是 Number 的实例(Number 的实例是通过 new Number(1) 创建的包装对象)。因此返回 false。
6. 1 instanceof 1 // 报错:Uncaught TypeError: Right-hand side of 'instanceof' is not an object
- 左侧:
1是基本数据类型(非对象)。 - 右侧:
1是基本数据类型(非对象)。
关键原因:instanceof 要求右侧必须是对象(且必须是构造函数,因为需要访问其 prototype 属性)。而 1 是基本数据类型,不是对象,因此右侧不满足要求,直接报错。
总结
instanceof的核心是检查“右侧构造函数的prototype是否在左侧对象的原型链上”。- 左侧必须是对象(基本类型返回
false或报错),右侧必须是构造函数(对象),否则报错。 - 构造函数的原型链需要特别注意:所有构造函数的原型都是
Function.prototype,而Function自身的原型是Function.prototype。
在 JavaScript 中,{} + [] 的结果是 0,这一结果由 JavaScript 的语法解析规则和类型转换机制共同决定,具体原因如下:
问: {}+[] 得什么
1. 解析器对 {} 的处理:代码块而非对象
{} 在 JavaScript 中可能有两种含义:
- 作为对象字面量(如
var obj = {}); - 作为空代码块(类似
if(){}中的{},仅表示一段无操作的代码区域)。
在 {} + [] 这个表达式中,解析器会将开头的 {} 优先解析为空代码块(而非对象),因为 JavaScript 语法规定:当表达式以 { 开头时,默认按代码块解析(除非被括号包裹强制为对象,如 ({}))。
因此,{} + [] 实际被解析为两部分:
- 空代码块
{}(无实际操作,可忽略); - 剩余的表达式
+ [](一元+运算符作用于空数组[])。
2. 一元 + 运算符与类型转换
一元 + 运算符的作用是将操作数转换为数字。对于空数组 [],转换过程如下:
- 数组的
ToPrimitive转换(将对象转为原始值):数组默认先调用valueOf()(返回数组本身),若结果不是原始值,再调用toString()。空数组[]的toString()结果为空字符串""。 - 空字符串
""转换为数字:根据规则,空字符串转为数字是0。
因此,+ [] 的结果为 0。
总结
{} + [] 被解析为“空代码块 + 表达式 + []”,最终结果为 0。
如果想让 {} 被解析为对象(而非代码块),可以用括号包裹,如 ({}) + [],此时结果会变为 "[object Object]"(对象 {} 的 toString() 结果与空数组的 toString() 结果拼接)。
React Fiber 架构的主要目标是什么?它与之前的 Stack Reconciler 相比有哪些关键改进
React Fiber 是 React 16 中引入的全新协调(Reconciliation)引擎,其设计核心是为了解决传统 Stack Reconciler 在复杂应用中导致的性能瓶颈。以下从主要目标和关键改进两方面详细说明:
一、React Fiber 架构的主要目标
Fiber 的核心目标是提升 React 应用在处理大型组件树时的响应性,具体来说:
当 React 处理复杂组件的更新(如大量 DOM 操作、深层嵌套组件渲染)时,传统架构会因长时间占用主线程(JavaScript 执行与 UI 渲染共用主线程)导致用户交互(如点击、输入)、动画等高频操作出现卡顿。
Fiber 通过设计一套可中断、可恢复、带优先级的任务调度机制,让 React 能够灵活控制渲染过程,优先处理高优先级任务(如用户输入),从而避免 UI 阻塞,提升应用的流畅度。
二、与 Stack Reconciler 相比的关键改进
Stack Reconciler 是 React 15 及之前的协调引擎,基于 JavaScript 调用栈实现递归遍历组件树,其核心问题是渲染过程不可中断。Fiber 针对这些问题做了根本性改进:
1. 从“不可中断的递归”到“可中断的增量渲染”
- Stack Reconciler:采用递归方式遍历组件树(类似深度优先搜索),一旦开始渲染,会持续占用主线程直到整个树处理完成。如果组件树过深或复杂,会导致主线程阻塞(如 100ms 以上),此时用户输入、动画等操作无法响应,造成卡顿。
- Fiber:将渲染工作拆分为小单元任务(每个 Fiber 节点对应一个任务单元),每个任务执行一小段时间后,会主动“让出”主线程,检查是否有更高优先级任务(如用户输入)。若有则暂停当前任务,先执行高优先级任务;若无则继续处理下一个任务单元。这种“增量渲染”机制确保主线程不会被长时间占用。
2. 从“调用栈管理”到“链表结构的任务调度”
- Stack Reconciler:依赖 JavaScript 引擎的调用栈管理组件遍历,递归过程中无法暂停或恢复(调用栈状态由引擎维护,无法手动干预)。
- Fiber:采用双向链表结构重新设计了组件树的遍历方式:每个 Fiber 节点包含
child(子节点)、sibling(兄弟节点)、return(父节点)指针,通过指针跳转实现遍历,而非递归。这种结构允许 React 随时暂停遍历(记录当前节点指针即可保存进度),后续可通过指针恢复遍历,实现“可中断/可恢复”。
3. 引入“优先级调度机制”
- Stack Reconciler:所有更新任务无优先级区分,一旦触发就会同步执行,即使是低优先级任务(如网络请求后的列表更新)也可能阻塞高优先级任务(如输入框打字)。
- Fiber:为不同类型的更新任务定义了优先级(如:用户输入 > 动画 > 网络请求更新),通过
Scheduler模块(React 内部调度器)管理任务队列。当高优先级任务进入时,可打断正在执行的低优先级任务,优先处理高优先级任务,执行完成后再恢复低优先级任务。这确保了用户交互等关键操作的即时响应。
4. 更好的错误隔离与边界处理
- Stack Reconciler:递归过程中若某个组件抛出错误,可能导致整个组件树渲染中断,且错误难以隔离。
- Fiber:结合 React 16 引入的“错误边界(Error Boundary)”特性,Fiber 架构在任务单元执行过程中可捕获组件错误,并将错误限制在局部(通过错误边界组件处理),避免整个应用崩溃,提升了容错性。
总结
Fiber 架构通过任务分解、可中断调度、优先级管理三大核心改进,解决了 Stack Reconciler 因“不可中断的递归渲染”导致的主线程阻塞问题,最终实现了 React 应用在复杂场景下的高性能和高响应性。
context 和一般的状态管理有什么区别?性能方面
在 React 中,Context 和一般的状态管理工具(如 Redux、MobX、Zustand 等)都用于解决跨组件状态共享问题,但它们的设计目标、适用场景和性能表现有显著区别。
一、核心区别:设计目标与功能定位
1. Context:解决“属性透传”的轻量机制
Context 是 React 内置的 API,其核心目标是简化跨层级组件的状态共享,避免“props drilling”(属性透传链过长)的问题。
- 它本质是一种状态共享的“管道”:允许父组件定义一个“上下文”,所有子组件(无论层级深浅)都能直接访问该上下文,无需通过 props 逐层传递。
- 功能局限:仅提供“状态共享”能力,不包含状态的修改逻辑规范(如何修改、何时修改)、状态追踪(历史记录、时间旅行)、副作用处理(异步操作)等复杂功能。
- 适用场景:共享“全局配置类”状态(如主题、用户登录状态、语言设置),这类状态通常变化频率低,且修改逻辑简单。
2. 一般的状态管理工具:完整的状态治理方案
以 Redux、MobX 为代表的状态管理工具,是针对复杂应用的“状态全生命周期管理”方案,核心目标是解决“多组件协同修改复杂状态”的问题。
- 功能全面:不仅提供状态共享,还包含:
- 状态的集中存储(单一数据源,避免状态分散);
- 严格的状态修改规则(如 Redux 的 reducer 纯函数、MobX 的 observable 响应式更新);
- 副作用管理(如 Redux Thunk/Saga 处理异步操作);
- 状态追踪与调试(如 Redux DevTools 支持时间旅行)。
- 适用场景:大型应用中复杂、高频变化的状态(如购物车、表单数据、列表筛选条件),需要多组件共同读写,且状态修改逻辑复杂。
二、性能差异:更新机制与渲染粒度
两者的性能差异主要体现在状态更新时的组件重渲染控制上:
1. Context 的性能特点:更新粒度较粗,易引发冗余渲染
Context 的更新机制是“全量触发”:当 Context.Provider 的 value 发生变化时,所有消费该 Context 的组件(通过 useContext 或 Class.contextType)都会强制重渲染,无论组件是否实际依赖变化的部分。
- 原因:React 无法跟踪组件具体依赖
value中的哪些字段,只能简单判断value引用是否变化,一旦变化则全量更新。 - 示例:若
Context.value是一个对象{ theme: 'dark', user: { name: 'xxx' } },即使只修改user.name,所有使用theme的组件也会被强制重渲染。 - 优化难点:需手动拆分 Context(按更新频率拆分多个 Context),或结合
memo/useMemo减少冗余渲染,但会增加代码复杂度。
2. 一般状态管理工具的性能特点:更新粒度精细,冗余渲染少
专业状态管理工具通常通过**“依赖追踪”或“精确选择器”**实现更精细的更新控制:
- Redux:通过
useSelector(或connect的mapStateToProps)的浅比较机制,只让依赖“变化的状态片段”的组件重渲染。例如,若全局状态是{ a: 1, b: 2 },组件仅依赖a,则b变化时该组件不会重渲染。 - MobX:基于响应式追踪,组件会自动跟踪其使用的 observable 状态,只有当依赖的状态实际变化时才重渲染,无需手动优化。
- Zustand/Jotai:通过“原子化状态”或“选择器函数”,支持按需订阅状态片段,避免无关更新。
这种精细的更新机制,在状态频繁变化的场景(如表单输入、实时数据刷新)中,性能优势明显。
三、总结:如何选择?
用 Context:
- 场景:共享低频变化的全局配置(主题、权限);
- 优势:轻量(无需引入第三方库),原生支持;
- 注意:避免用 Context 存储高频变化的复杂状态,否则易引发性能问题。
用专业状态管理工具:
- 场景:大型应用,状态复杂(多字段、嵌套深)、变化频繁,需要多组件协同修改或追踪状态历史;
- 优势:更精细的性能控制、更规范的状态修改流程、更好的可维护性和可调试性。
简言之,Context 是“解决属性透传的轻量方案”,而状态管理工具是“复杂状态的完整治理方案”,性能差异本质是更新机制的精细度不同。
fiber 架构主要目标,为什么可以中断,时间切片是怎么做的
你提的这三个问题正好击中了Fiber架构的核心设计,理解它们就能掌握React性能优化的关键逻辑。Fiber架构的核心目标是实现“可中断、可恢复”的渲染,从而避免主线程阻塞,提升页面响应速度。
一、Fiber架构的主要目标
Fiber架构本质是为了解决React旧架构(栈调和)的“同步渲染阻塞主线程”问题,核心目标有两个:
- 实现可中断、可恢复的渲染:将渲染过程拆分成小块任务,不再一次性执行到底,允许在中间暂停、恢复或终止。
- 支持优先级调度:给不同类型的任务(如用户输入、动画、数据更新)分配优先级,高优先级任务(如点击事件)可以打断低优先级任务(如列表渲染),优先执行,避免用户操作卡顿。
二、为什么Fiber架构可以中断
旧架构用“栈调和”(Stack Reconciliation),渲染过程是递归调用,一旦开始就无法暂停;而Fiber架构通过“Fiber节点”和“链表遍历”重构了渲染流程,实现了可中断,核心原因有两点:
- Fiber节点保存完整上下文:每个Fiber节点对应一个组件,不仅包含组件的类型、props、状态,还保存了渲染所需的“指针”(父、子、兄弟节点)和“执行状态”(如是否已完成、是否需要重试)。中断后,只要找到对应的Fiber节点,就能恢复之前的渲染进度。
- 渲染流程拆分为“双阶段”:
- 调度阶段(Reconciliation):遍历组件树、对比新旧节点、标记需要更新的操作(如插入、删除、修改)。这一阶段完全可中断,因为只做“标记”不操作DOM。
- 提交阶段(Commit):根据调度阶段的标记,实际操作DOM(如插入节点、更新样式)。这一阶段不可中断,因为DOM操作是同步的,中断会导致页面状态不一致。
三、时间切片(Time Slicing)是怎么做的
时间切片的核心是“把渲染任务拆分成不超过16ms的小块,在浏览器空闲时间执行,避免阻塞主线程”,具体实现依赖React的Scheduler(调度器) 和浏览器的“空闲时间检测”,步骤如下:
- Scheduler分配时间片:Scheduler是Fiber架构的“调度核心”,它会给每个渲染任务分配一个“时间片”(默认约16ms,对应屏幕60fps的刷新频率)。
- 利用requestIdleCallback的polyfill:浏览器原生的
requestIdleCallback能在主线程空闲时触发回调,但兼容性和触发频率不足,React自己实现了它的polyfill(基于requestAnimationFrame和setTimeout),精准检测主线程的空闲时间。 - 任务执行与超时检查:
- 每次只执行一个“小块任务”(如处理一个Fiber节点的对比)。
- 任务执行前,通过
performance.now()记录开始时间;执行中定期检查“已用时间是否超过时间片”。 - 若超时,立即暂停任务,将控制权交还给浏览器(让浏览器处理UI绘制、用户输入等),待下一次空闲时间再恢复执行。
最新版是如何实现时间切片的
在React 18及之后的版本中,时间切片(Time Slicing)的实现基于并发渲染(Concurrent Rendering) 架构,并与调度器(Scheduler)深度整合,相比旧版(React 17及之前)在灵活性和性能上有显著优化。其核心思路依然是“将渲染任务拆分为小块,在浏览器空闲时执行,避免阻塞主线程”,但实现细节更适配现代浏览器特性和并发场景。
最新版时间切片的核心实现逻辑
1. 基于“Scheduler”的优先级调度引擎
React的时间切片依赖独立的Scheduler模块(位于react-scheduler包),它是调度任务的“大脑”,负责:
- 管理不同优先级的任务(如用户输入、动画、数据更新等);
- 控制任务的执行时机(利用浏览器空闲时间);
- 决定任务是否需要中断或继续。
Scheduler将任务优先级分为多个级别(从高到低):
Immediate:同步执行,不可中断(如用户输入的同步回调);UserBlocking:高优先级(如点击、输入等用户交互,需在25ms内响应);Normal:普通优先级(如网络请求后的UI更新);Low:低优先级(如非紧急的数据计算);Idle:极低优先级(如日志上报,可在浏览器完全空闲时执行)。
时间切片主要针对Normal及以下优先级的任务,高优先级任务会直接打断低优先级任务的执行(这是并发渲染的核心能力)。
2. 时间切片的触发与中断机制
最新版通过可中断的工作循环(workLoop) 实现时间切片,核心步骤如下:
(1)任务拆分:将渲染任务分解为“Fiber单元”
渲染过程(协调阶段,Reconciliation)被拆分为一个个独立的“Fiber单元”(每个Fiber对应一个组件的处理逻辑,如diff对比、状态更新等)。每个单元的执行时间极短(通常几毫秒),确保单个单元不会阻塞主线程。
(2)利用浏览器空闲时间执行任务
React 18优先使用浏览器原生的**scheduler.postTask**(如果浏览器支持,如Chrome 87+),这是一个专门用于调度任务的API,能更精准地利用浏览器的空闲时间。若浏览器不支持,则降级为基于setTimeout和requestAnimationFrame的polyfill。
postTask的优势在于:
- 可以指定任务优先级,浏览器会根据系统负载自动调整执行时机;
- 能更好地配合浏览器的渲染帧(60fps),避免任务执行与UI绘制冲突。
(3)超时检查与任务中断
每次执行一个“Fiber单元”后,Scheduler会检查:
- 当前任务已执行时间是否超过“剩余空闲时间”(由浏览器动态计算,通常不超过16ms,对应60fps的单帧时间);
- 是否有更高优先级的任务插入(如用户突然点击按钮)。
如果满足任一条件,当前任务会被暂停,并将控制权交还给浏览器(让浏览器处理UI绘制、用户输入等)。待下一次浏览器空闲时,Scheduler会从暂停的Fiber节点继续执行(利用Fiber节点保存的上下文信息恢复进度)。
3. 与并发渲染的协同
React 18的时间切片不是孤立的,而是并发渲染的基础。并发渲染允许同一时间存在多个“渲染版本”(如一个低优先级的列表渲染和一个高优先级的按钮点击渲染),时间切片通过以下方式支持这一特性:
- 低优先级任务被中断后,其已执行的中间状态会被保存在内存中(不影响当前DOM);
- 高优先级任务执行完毕后,若低优先级任务仍有必要继续,会从暂停处恢复,而非重新开始;
- 若低优先级任务已无必要(如用户导航到新页面),则直接丢弃,避免无效计算。
总结:最新版时间切片的核心改进
相比旧版,React 18的时间切片实现更“智能”:
- 从“固定时间片”到“动态空闲时间”:不再严格依赖16ms固定阈值,而是根据浏览器实时负载动态调整;
- 与原生API深度结合:优先使用
postTask等现代浏览器API,调度精度更高; - 服务于并发渲染:时间切片不再仅是“避免阻塞”,而是支撑“多版本渲染并行”的核心机制,让高优先级任务(如用户交互)始终能快速响应。
这种设计使得React在复杂UI场景(如大数据列表、频繁动画)中,依然能保持流畅的用户体验。
浏览器一帧帧渲染,浏览器都要做什么动作
浏览器以每秒60帧(60fps) 的频率刷新屏幕(部分高刷设备更高,如120fps),每帧的渲染过程是一个“流水线”式的工作流,目的是将HTML/CSS/JS描述的内容转化为屏幕上的像素。一帧内,浏览器主要完成以下核心动作,按顺序可分为5个关键步骤:
1. 处理输入事件(Input Handling)
作用:优先响应用户交互(如点击、触摸、滚动、键盘输入等),避免延迟导致“卡顿感”。
细节:
- 浏览器会收集过去一帧内积累的所有输入事件(如鼠标移动、按钮点击),并在当前帧开始时优先处理(因为用户交互的响应延迟超过100ms就会被感知到卡顿)。
- 输入事件处理后可能触发JS回调(如
onClick、onScroll),这些回调会在这一步执行,可能修改DOM或样式。
2. 执行定时器(Timer Execution)
作用:处理setTimeout、setInterval等定时器任务,按触发时间顺序执行。
细节:
- 浏览器会检查当前帧中已到期的定时器,执行其回调函数。
- 注意:定时器的“延迟时间”是“最早执行时间”,而非“精确时间”——如果前序任务(如输入处理)耗时过长,定时器会被推迟到下一帧。
3. 布局(Layout,又称“回流/重排”)
作用:计算元素的几何信息(位置、大小、形状),确定每个元素在屏幕上的具体坐标。
细节:
- 输入事件或JS可能修改DOM结构(如添加元素)或样式(如
width、left),导致元素的几何属性变化,此时必须重新计算布局。 - 布局是“全局性”的:一个元素的位置变化可能影响其他元素(如父元素、兄弟元素),因此浏览器会从根节点(
<html>)开始遍历DOM树,递归计算所有元素的几何信息,这一步是性能消耗的“重灾区”(耗时可能超过10ms)。
4. 绘制(Paint,又称“重绘”)
作用:根据布局结果,为元素填充像素(如颜色、阴影、渐变、文本等),生成“图层位图”。
细节:
- 绘制不涉及几何计算,只关心“如何填充像素”,例如给一个
div涂红色、绘制文本的字形。 - 绘制是“局部性”的:浏览器会将DOM树拆分为多个“图层”(如
position: fixed的元素、opacity < 1的元素会单独成层),每个图层独立绘制,避免整个屏幕重绘。 - 绘制可以被“合并”:如果多个元素在同一图层且不重叠,浏览器会批量绘制以提升效率。
5. 合成(Composite)
作用:将所有绘制好的图层按正确的顺序合并成一张完整的屏幕图像,最终显示到屏幕上。
细节:
- 图层合并由GPU(图形处理器)负责,效率远高于CPU(CPU擅长逻辑计算,GPU擅长并行像素处理)。
- 合成时需要处理图层的“层级关系”(如
z-index),避免元素显示顺序错误(例如弹窗应覆盖在其他内容上方)。 - 常见的“GPU加速”操作(如
transform: translate、opacity变化)只触发合成阶段,不涉及布局和绘制,因此性能极高。
一帧的时间分配与卡顿原因
- 正常情况下,每帧需在16ms内完成(1000ms/60≈16.6ms),才能保证画面流畅。
- 如果某一步耗时过长(如布局计算用了20ms),浏览器会“掉帧”——这一帧的内容无法及时显示,屏幕会保持上一帧的画面,用户会感知到“卡顿”。
总结
浏览器一帧的渲染流程是“输入处理→定时器执行→布局→绘制→合成”的流水线,核心是将逻辑(JS)转化为视觉(像素)。其中,布局和绘制是性能优化的重点(尽量避免频繁修改几何属性),而合成阶段借助GPU可实现高效的动画效果(如用transform代替left做位移动画)。
宏任务和微任务都有哪些
宏任务(Macro Task)和微任务(Micro Task)是JavaScript事件循环(Event Loop)中的核心任务类型,二者的执行顺序决定了代码的运行逻辑——每执行完一个宏任务后,会先清空所有微任务队列,再执行下一个宏任务。以下是两者的具体分类及场景说明:
一、宏任务(Macro Task)
宏任务是“粗粒度”的任务,触发时机通常与浏览器渲染、JS执行、外部事件相关,执行间隔较长,常见类型如下:
| 任务类型 | 所属环境 | 核心场景说明 |
|---|---|---|
script 整体代码 | 浏览器/Node.js | 整个JS脚本的执行是“第一个宏任务”,脚本内的代码会优先启动执行。 |
setTimeout | 浏览器/Node.js | 延迟指定时间后执行回调,回调会被加入宏任务队列(延迟时间是“最早执行时间”,非精确)。 |
setInterval | 浏览器/Node.js | 按指定间隔重复执行回调,每次回调均作为独立宏任务。 |
requestAnimationFrame | 浏览器专属 | 与浏览器渲染帧同步(通常60fps),用于优化动画,回调在“布局→绘制”前执行。 |
| 用户交互事件回调 | 浏览器专属 | 如 click、scroll、input 等,用户操作触发后,回调会被加入宏任务队列。 |
postMessage | 浏览器/Node.js | 跨窗口/跨线程通信的回调(如iframe间通信),属于宏任务。 |
MessageChannel | 浏览器/Node.js | 更高效的跨线程通信(如Worker间),onmessage 回调为宏任务。 |
setImmediate | Node.js专属 | Node环境下的“立即执行”宏任务,优先级低于微任务,高于 setTimeout。 |
| Service Worker 事件 | 浏览器专属 | 如 fetch 拦截、push 通知等Service Worker的回调,属于宏任务。 |
二、微任务(Micro Task)
微任务是“细粒度”的任务,通常由JS引擎内部触发,执行时机在“当前宏任务结束后、下一个宏任务开始前”,优先级高于宏任务,常见类型如下:
Promise 相关回调
Promise.then()、Promise.catch()、Promise.finally():Promise状态改变后触发的回调,是最常用的微任务。- 注意:
Promise构造函数内的代码(如new Promise((resolve) => { ... }))是“同步执行”的,只有then/catch/finally回调属于微任务。
async/await语法糖await关键字后面的代码,本质是Promise.then()的语法糖,会被转化为微任务执行。- 例:
async function fn() { await Promise.resolve(); console.log('微任务') }中,console.log属于微任务。
queueMicrotaskAPI- 浏览器和Node.js均支持的“手动添加微任务”的API,直接将回调加入微任务队列,语法:
queueMicrotask(() => { ... })。
- 浏览器和Node.js均支持的“手动添加微任务”的API,直接将回调加入微任务队列,语法:
MutationObserver- 浏览器专属的微任务,用于监听DOM节点变化(如节点新增、属性修改),回调会在DOM更新后、浏览器渲染前执行(避免频繁触发布局)。
process.nextTick- Node.js专属的微任务,优先级高于所有其他微任务(包括Promise回调),用于在当前微任务队列清空前快速执行回调(需注意避免嵌套导致阻塞)。
三、核心执行顺序总结
- 执行第一个宏任务:
script整体代码。 - 执行
script内所有同步代码后,清空当前所有微任务(按优先级:Node下process.nextTick先于Promise,浏览器下按添加顺序)。 - 微任务清空后,浏览器开始执行“布局→绘制→合成”(一帧渲染流程)。
- 渲染完成后,从宏任务队列取出下一个宏任务(如
setTimeout回调),重复步骤2-3,形成事件循环。
vue 的 nextTick 是怎么实现
Vue 的 nextTick 是一个核心 API,用于在 DOM 更新完成后执行回调函数(比如修改数据后需要立即操作更新后的 DOM 时使用)。其实现核心是利用 JavaScript 的异步任务队列(微任务优先,宏任务兜底),确保回调在 DOM 重新渲染后执行。
一、nextTick 的核心目标
Vue 中数据更新后,DOM 并不会立即更新,而是会将 DOM 更新操作放入一个“异步更新队列”中,等当前同步代码执行完毕后批量处理(避免频繁 DOM 操作导致性能浪费)。nextTick 的作用就是:让回调函数等待这个“异步更新队列”执行完毕(DOM 已更新)后再执行。
二、实现原理:优先微任务,宏任务兜底
nextTick 的实现依赖于 JavaScript 的事件循环机制,其核心逻辑是:将回调函数放入“异步任务队列”,并优先选择执行时机更早的微任务,若环境不支持则降级为宏任务。
具体步骤如下:
1. 检测环境支持的异步 API,优先微任务
Vue 会根据当前运行环境(浏览器、Node.js 等),自动选择最合适的异步 API 来触发回调,优先级从高到低为:
| 异步任务类型 | 优先使用的 API | 适用场景 |
|---|---|---|
| 微任务 | Promise.then | 支持 Promise 的环境(现代浏览器、Node.js),执行时机最早(DOM 更新后立即执行)。 |
| 微任务 | MutationObserver | 不支持 Promise 的低版本浏览器(如 IE11),通过监听 DOM 变化触发回调(本质是微任务)。 |
| 宏任务 | setImmediate | Node.js 环境或 IE 浏览器,宏任务中执行时机较早。 |
| 宏任务 | setTimeout(fn, 0) | 所有环境的兜底方案,兼容性最好,但执行时机较晚(延迟约 4ms)。 |
2. 维护一个回调队列,批量执行
nextTick 不会为每个回调单独创建异步任务,而是将所有回调收集到一个队列中,等异步任务触发时一次性执行所有回调(避免多次创建异步任务的性能开销)。
核心流程:
- 调用
nextTick(cb)时,将cb推入回调队列。 - 检查是否已创建异步任务:若未创建,则根据环境选择上述异步 API,创建一个“触发器”(如
Promise.resolve().then(flushCallbacks))。 - 异步任务触发时,执行
flushCallbacks函数,依次调用队列中所有回调,并清空队列。
3. 源码核心逻辑(简化版)
以 Vue 2 源码为例,核心逻辑如下:
const callbacks = []; // 回调队列
let pending = false; // 标记是否已创建异步任务
// 执行所有回调
function flushCallbacks() {
pending = false;
const copies = callbacks.slice(0); // 复制一份,避免执行中新增回调被重复处理
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i](); // 执行回调
}
}
// 选择合适的异步 API
let timerFunc;
if (typeof Promise !== 'undefined') {
// 优先使用 Promise(微任务)
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
};
} else if (typeof MutationObserver !== 'undefined') {
// 降级使用 MutationObserver(微任务)
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode('1');
observer.observe(textNode, { characterData: true });
timerFunc = () => {
textNode.data = '2'; // 修改文本触发 MutationObserver 回调
};
} else if (typeof setImmediate !== 'undefined') {
// Node.js 或 IE 环境,使用 setImmediate(宏任务)
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 兜底方案:setTimeout(宏任务)
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
// 对外暴露的 nextTick 函数
function nextTick(cb) {
callbacks.push(cb); // 推入回调队列
if (!pending) {
pending = true;
timerFunc(); // 触发异步任务
}
}三、Vue 3 中的优化
Vue 3 对 nextTick 的实现逻辑与 Vue 2 一致(微任务优先),但做了细节优化:
- 移除了对
MutationObserver的依赖(因为现代浏览器基本都支持 Promise)。 - 优先使用
queueMicrotaskAPI(浏览器原生的微任务队列 API,比Promise.then更直接),不支持则降级为Promise.then。 - 源码更简洁,聚焦于微任务的高效触发。
总结
nextTick 的核心是利用异步任务队列(微任务优先),确保回调在 DOM 异步更新完成后执行。其实现逻辑体现了 Vue 对性能的优化(批量处理回调)和兼容性的考虑(多环境降级方案)。在实际开发中,当需要在数据更新后操作 DOM 时,nextTick 是最可靠的选择。
如何排查页面白屏的问题,从哪些方面入手
页面白屏是前端开发中常见的问题,可能由网络、代码、资源、服务器、浏览器兼容等多个环节的问题导致。排查时需要按“从表层到深层、从基础到复杂”的顺序逐步定位,以下是具体的排查方向和方法:
一、优先检查网络与资源加载(最基础原因)
页面白屏的直接原因可能是核心资源(HTML/CSS/JS)未加载或加载失败,导致浏览器无法渲染内容。可通过浏览器开发者工具(F12)的 Network 面板排查:
1. 检查 HTML 文档是否加载成功
- 查看 Network 面板的“Doc”分类,找到页面的主 HTML 请求(通常是第一个请求):
- 状态码:若为
404(未找到)、500(服务器错误)、403(权限不足)等,说明服务器未返回有效的 HTML,直接导致白屏(需联系后端排查接口或服务器配置)。 - 响应内容:点击该请求的“Response”标签,查看返回的 HTML 是否完整(例如是否只有
<html></html>空标签,或被错误截断)。若 HTML 不完整,可能是服务器渲染失败(如模板引擎报错)。
- 状态码:若为
2. 检查关键资源(CSS/JS)是否加载失败
- 筛选 Network 面板的“CSS”“JS”分类,查看是否有资源:
- 状态码异常:
404(文件路径错误)、500(资源生成失败)、400(请求参数错误)等。 - 跨域错误:资源请求的“Status”显示“(blocked: CORS policy)”,说明跨域配置错误(需后端在资源服务器添加 CORS 响应头)。
- 加载被拦截:请求状态为“failed”且无状态码,可能被浏览器插件(如广告拦截器)或网络安全工具拦截(可暂时关闭插件测试)。
- 资源体积异常:JS/CSS 体积为 0KB 或远小于预期,可能是打包工具(如 Webpack)漏打包或资源损坏。
- 状态码异常:
3. 检查资源加载顺序与阻塞
- 若 HTML 已加载,但 CSS/JS 加载过慢(如超过 10s),可能导致页面长时间白屏(浏览器需等关键 CSS 解析后才渲染,JS 可能阻塞解析)。
- 查看资源的“Time”列,确认是否有资源加载超时(可结合 CDN 节点、网络环境排查,如切换网络测试)。
- 检查 JS 是否被标记为
defer或async:若关键 JS(如框架初始化代码)未正确设置,可能阻塞 HTML 解析,导致白屏。
二、检查HTML 结构与解析(渲染基础)
即使 HTML 成功加载,若结构错误或关键节点缺失,也会导致白屏。可通过Elements 面板和HTML 源码排查:
1. 检查 DOM 树是否为空或缺失关键节点
- 打开 Elements 面板,查看
<body>内是否有内容:- 若
<body>为空(仅<script>标签),可能是 JS 动态渲染失败(如框架未挂载组件)。 - 若存在节点但被隐藏(如
<div id="app" style="display: none">),可能是 CSS 或 JS 错误设置了隐藏样式。
- 若
2. 检查 HTML 语法错误
- 服务器返回的 HTML 若有语法错误(如未闭合的标签
<div>、错误的注释<!-- -->),可能导致浏览器解析异常,忽略部分内容。- 可通过在线 HTML 校验工具(如 W3C HTML Validator)检查 HTML 合法性。
三、排查JavaScript 执行错误(最常见原因)
JS 执行过程中抛出未捕获的错误,可能中断渲染逻辑(如框架初始化、组件挂载),导致白屏。可通过Console 面板和Sources 面板定位:
1. 查看 Console 错误信息
- Console 会直接显示错误类型(如
ReferenceError、TypeError)、错误位置(文件名+行号)和原因:ReferenceError: xxx is not defined:变量未定义(可能是漏引入依赖、拼写错误,或依赖加载顺序错误)。TypeError: Cannot read property 'xxx' of undefined:访问了undefined/null的属性(常见于数据初始化错误,如接口返回null但代码直接读取属性)。- 框架初始化错误:如 Vue 的
[Vue warn]: Failed to mount component: template or render function not defined(组件未正确注册),React 的Uncaught Error: Target container is not a DOM element(挂载节点不存在)。 - 语法错误:如低版本浏览器不支持
let/const、箭头函数=>(未通过 Babel 转译),会报Unexpected token错误。
2. 调试 JS 执行流程
- 若错误信息不明确,可在 Sources 面板:
- 在关键代码处(如
new Vue()、ReactDOM.render())设置断点,逐步执行,观察变量是否正常。 - 检查是否有无限循环/死循环:若 JS 主线程被阻塞(可在 Performance 面板记录,查看是否有长任务),会导致页面无法渲染(表现为白屏)。
- 在关键代码处(如
四、检查CSS 样式问题(视觉层面白屏)
有时页面内容已渲染,但被 CSS 隐藏或覆盖,导致“视觉白屏”:
1. 检查全局隐藏样式
- 在 Elements 面板选中
<body>或根节点(如<div id="app">),查看右侧“Styles”面板:- 是否有
display: none、visibility: hidden或opacity: 0样式(可能是 JS 条件渲染逻辑错误,或 CSS 选择器冲突)。 - 背景色是否与内容色一致(如
background: white且文字颜色也是white,导致内容“隐形”)。
- 是否有
2. 检查 CSS 加载与解析
- 若关键 CSS 未加载(如 Network 中 CSS 失败),可能导致页面无样式(但通常不会完全白屏,除非内容被默认样式隐藏)。
- 若 CSS 有语法错误(如未闭合的
{、错误的属性名),可能导致后续样式失效,极端情况下可能影响布局(如body { overflow: hidden; height: 0; }导致内容不可见)。
五、排查框架/库的特殊问题
使用 Vue、React 等框架时,白屏可能与框架的渲染机制相关:
1. Vue 项目
- 路由问题:初始路由未匹配(如
routes配置错误,或mode: 'history'但服务器未配置 fallback),导致<router-view>无内容。 - 状态管理错误:如 Vuex/Pinia 初始化失败,或异步数据获取错误导致组件渲染时依赖
undefined。 - SSR 相关:服务端渲染(SSR)时,客户端与服务端渲染的虚拟 DOM 不匹配(报
Hydration mismatch错误),可能导致客户端重新渲染失败。
2. React 项目
- 组件挂载失败:
ReactDOM.render()的目标节点不存在(如document.getElementById('root')为null)。 - Hooks 使用错误:如在条件语句中调用 Hooks(违反 Hooks 规则),导致组件渲染崩溃。
- 路由配置错误:
react-router中path匹配错误,或Redirect死循环,导致无组件渲染。
排查方法
- 使用框架调试工具:Vue Devtools(查看组件树是否挂载)、React Devtools(查看组件渲染状态)。
- 检查框架版本与依赖兼容性:如 React 18 不兼容旧版
ReactDOM.render写法,可能导致白屏。
六、服务器与环境配置问题
1. 服务器响应头异常
- 查看 HTML 请求的“Response Headers”:
Content-Type错误:若为application/json或text/plain(而非text/html; charset=utf-8),浏览器会将 HTML 当作文本解析,不渲染为页面(表现为白屏或显示源码)。X-Frame-Options限制:若设置DENY且页面被嵌入 iframe,可能导致 iframe 白屏。
2. 缓存与 Cookie 问题
- 浏览器缓存了旧的、有问题的资源(如缓存了未打包完成的 JS):可清除浏览器缓存(Ctrl+Shift+Del),或在 URL 后加随机参数(如
?v=123)强制刷新。 - Cookie 或 Token 过期:部分页面依赖登录状态,若认证失败,服务器可能返回空白 HTML(需检查接口返回的用户状态)。
七、浏览器兼容性与环境问题
1. 浏览器版本兼容
- 低版本浏览器(如 IE 11)不支持现代 JS/CSS 特性:
- JS:
Promise、async/await、class等未转译(需检查 Babel 配置是否包含目标浏览器)。 - CSS:
flex、grid等布局未加前缀,或使用:has()等新选择器(需通过 Autoprefixer 处理)。 - 测试方法:在目标浏览器中打开页面,查看 Console 是否有语法错误。
- JS:
2. 浏览器模式/插件影响
- 浏览器处于“无痕模式”或“隐私模式”:部分 API(如
localStorage)被禁用,若代码未兼容,可能报错导致白屏。 - 插件冲突:广告拦截器、脚本拦截器可能误删页面关键节点或 JS(可关闭插件后重试)。
八、性能与阻塞问题(白屏但最终会渲染)
若页面白屏一段时间后恢复,可能是主线程被阻塞:
- 通过Performance 面板记录页面加载过程:
- 查看是否有“长任务”(执行时间 >50ms):如大型循环、复杂计算,阻塞了渲染线程。
- 检查“First Contentful Paint(FCP)”时间:若 FCP 过长(如 >3s),说明关键资源加载或解析太慢(需优化资源体积、使用 CDN、懒加载等)。
总结:排查流程建议
- 先看 Network:确认 HTML、CSS、JS 等核心资源是否加载成功(状态码、响应内容)。
- 再看 Console:检查是否有 JS 错误,定位错误位置和原因。
- 检查 Elements:确认 DOM 树是否正常,是否有隐藏样式。
- 结合框架工具:用 Vue/React Devtools 检查组件挂载和路由状态。
- 排除环境问题:换浏览器、清缓存、关插件,确认是否为环境兼容问题。
- 性能分析:若白屏后恢复,用 Performance 面板分析长任务和渲染阻塞。
通过以上步骤,可逐步缩小范围,定位白屏的具体原因(资源、代码、配置、兼容等),再针对性解决。