高级
JS单线程语言
单线程设计的好处
简单和直观
- 好处:JavaScript 的单线程模型使得语言本身更加简单和直观。开发者不需要关心多线程之间的竞态条件、死锁等复杂的并发问题,这简化了编程模型和语言的学习曲线。
- 示例:对于初学者来说,理解单线程的执行顺序比理解多线程的并发执行更容易。
易于控制
- 好处:由于单线程,JavaScript 可以更容易地管理和控制代码的执行顺序和状态变化。所有的操作都是顺序执行的,没有并发执行带来的不确定性,这有助于避免意外的副作用和错误。
- 示例:在单线程中,变量的值变化是可预测的,因为不会出现多个线程同时修改同一个变量的情况。
节省内存资源
- 好处:在浏览器中,每个标签页或窗口都是一个单独的进程或线程。如果 JavaScript 是多线程的,每个标签页可能需要额外的内存来管理多个线程,而单线程模型可以节省内存资源。
- 示例:单线程模型减少了内存占用,使得浏览器可以同时打开更多标签页而不会过度消耗系统资源。
避免同步问题
- 好处:在多线程编程中,需要处理同步问题(如竞态条件、死锁、数据一致性等),这对于开发者来说是一个复杂和容易出错的部分。单线程模型避免了这些问题,因为不需要考虑多个线程同时访问和修改共享状态的情况。
- 示例:在单线程中,开发者不需要使用锁或其他同步机制来保护共享数据。
更好的响应性
- 好处:JavaScript 在浏览器中通常用于处理用户交互和响应用户操作。单线程模型确保所有的事件和操作都按照其发生的顺序进行处理,这样可以保证 UI 的响应性和一致性。
- 示例:用户点击按钮后,事件处理函数会立即执行,不会被其他线程打断,从而保证了用户体验的流畅性。
设计成单线程的原因
- 适应应用场景
- 原因:JavaScript 最初被设计成单线程主要是为了适应其主要的应用场景:浏览器中的交互式网页。在浏览器中,JavaScript 负责处理用户交互、DOM 操作和网络请求等任务,这些任务不需要多线程并发执行,而是按照事件驱动的方式处理即可。
- 示例:网页中的大多数任务(如点击按钮、输入文本、发送网络请求)都是顺序执行的,不需要并发处理。
- 简化实现和优化性能
- 原因:JavaScript 的单线程模型有助于避免因为多线程带来的复杂性和性能开销,特别是在早期浏览器性能较弱的情况下,单线程可以简化实现和优化执行效率。
- 示例:早期浏览器的 JavaScript 引擎性能有限,单线程模型减少了实现复杂度,使得引擎可以更高效地执行代码。
怎么解决单线程问题
异步编程
- 回调函数:通过回调函数处理异步操作,避免阻塞主线程。
- Promise:提供更优雅的异步编程方式,支持链式调用和错误处理。
- async/await:使异步代码看起来像同步代码,提高可读性和易用性。
事件循环和任务队列
- 事件循环:JavaScript 的事件循环机制确保了即使在单线程环境下,也可以处理多个异步任务。
- 宏任务和微任务队列:通过任务队列管理异步任务,确保任务按顺序执行。
Web Workers
- Web Workers:允许在后台线程中运行 JavaScript 代码,从而实现真正的多线程处理。虽然 Web Workers 仍然运行在独立的线程中,但它们可以与主线程通信,从而实现复杂的并发任务。
js运行需要什么
JavaScript 引擎
JavaScript 引擎是解释和执行 JavaScript 代码的核心组件。不同的浏览器和运行环境有不同的 JavaScript 引擎实现,其中最常见的包括:
- V8:由 Google 开发,用于 Chrome 浏览器和 Node.js。
- SpiderMonkey:由 Mozilla 开发,用于 Firefox 浏览器。
- JavaScriptCore:由苹果开发,用于 Safari 浏览器和 WebKit。 这些引擎负责将 JavaScript 代码解析、编译成可执行代码,并执行代码逻辑。
宿主环境(Host Environment)
JavaScript 是一种脚本语言,它依赖于宿主环境提供的运行时环境和各种 API。常见的宿主环境包括:
- 浏览器环境:在浏览器中运行的 JavaScript 可以访问浏览器提供的 DOM、BOM、Fetch API 等 Web API。
- Node.js 环境:在服务器端运行的 JavaScript,可以访问操作系统的文件系统、网络等功能,还能使用 Node.js 提供的内置模块和第三方模块。
Web API
在浏览器环境中,JavaScript 可以通过 Web API 访问浏览器提供的各种功能和服务,例如:
- DOM API:用于操作和修改 HTML 文档结构。
- XHR / Fetch API:用于进行网络请求和获取数据。
- Canvas 和 WebGL:用于绘制图形和实现复杂的图形渲染。
- Local Storage 和 Session Storage:用于在客户端持久化存储数据等。
运行时
JavaScript 运行时指的是 JavaScript 引擎与宿主环境和其他 API 的整体集合。它提供了一个完整的环境,使得 JavaScript 能够与外部环境进行交互和操作,从而实现丰富的应用程序功能
浏览器下, JS 的进程和线程
进程
浏览器主进程(Browser Process)
- 作用:这是浏览器的核心进程,负责管理浏览器的界面(如标签页、地址栏、书签栏等),并协调其他进程的工作。
- 示例:控制浏览器窗口的显示和关闭。
渲染进程(Rendering Process)
- 作用:每个标签页通常都有一个独立的渲染进程,负责解析和执行 HTML、CSS 和 JavaScript,以及渲染页面内容。
- 示例:加载网页内容并显示给用户。
插件进程(Plugin Process)
- 作用:负责运行浏览器插件,如 Flash 等。
- 示例:运行网页中的 Flash 动画
线程
主线程(Main Thread)
- 作用:在渲染进程中,主线程负责解析 HTML、执行 JavaScript 代码,管理 DOM 和 CSSOM,并处理用户交互事件(如点击、滚动等)。
- 示例:执行页面中的 JavaScript 脚本,更新页面的 DOM 结构。
事件线程(Event Thread)
- 作用:事件线程负责处理异步事件,例如处理鼠标点击、键盘输入、网络请求的回调等。在浏览器中,事件线程通常由 Event Loop 来管理。
- 示例:当用户点击按钮时,事件线程会将事件放入事件队列,等待主线程处理。
定时器线程(Timer Thread)
- 作用:负责管理定时器(如 setTimeout、setInterval)的计时和触发。
- 示例:设置一个 setTimeout,定时器线程会在指定时间后将回调函数放入事件队列。
网络线程(Network Thread)
- 作用:负责处理网络请求和响应。
- 示例:当页面发起一个 HTTP 请求时,网络线程会处理请求和接收响应。
JavaScript 执行的核心位置
总体来说,JavaScript 代码的执行主要发生在浏览器的渲染进程中的主线程中。这个线程负责执行 JavaScript 代码、更新 DOM、处理用户输入等。事件循环(Event Loop)则是负责管理异步操作和事件的执行顺序,确保 JavaScript 在单线程的执行环境下能够处理并发的事件和任务。
JavaScript事件循环机制
核心概念
- 调用栈(Call Stack):同步代码执行
- 任务队列(Task Queue):宏任务(setTimeout, setInterval, I/O)
- 微任务队列(Microtask Queue):Promise, MutationObserver, process.nextTick
执行顺序
同步代码 > 所有微任务 > 一个宏任务 > 所有微任务
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => console.log('5'), 0);
});
console.log('6');
// 输出顺序:1 6 4 2 3 5
实现Promise A+规范
class MyPromise {
/**
* 构造函数,接收一个执行器函数(executor)
* @param {Function} executor - 包含resolve和reject两个参数的函数
*/
constructor(executor) {
// 初始状态为pending
this.state = 'pending';
// 存储成功或失败的值
this.value = undefined;
// 存储成功回调函数数组
this.onFulfilledCallbacks = [];
// 存储失败回调函数数组
this.onRejectedCallbacks = [];
/**
* 定义resolve函数
* @param {any} value - 要解析的值
*/
const resolve = (value) => {
// 如果解析的值是Promise,则等待这个Promise解决
if (value instanceof MyPromise) {
return value.then(resolve, reject);
}
// 只有pending状态可以转换为fulfilled
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
// 执行所有成功回调
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
/**
* 定义reject函数
* @param {any} reason - 拒绝原因
*/
const reject = (reason) => {
// 只有pending状态可以转换为rejected
if (this.state === 'pending') {
this.state = 'rejected';
this.value = reason;
// 执行所有失败回调
this.onRejectedCallbacks.forEach(fn => fn());
}
};
// 立即执行executor函数
try {
// 传入resolve和reject函数
executor(resolve, reject);
} catch (err) {
// 如果执行器抛出错误,直接reject
reject(err);
}
}
/**
* then方法,接收两个可选参数:onFulfilled和onRejected
* @param {Function} onFulfilled - 成功回调
* @param {Function} onRejected - 失败回调
* @returns {MyPromise} 返回一个新的Promise
*/
then(onFulfilled, onRejected) {
// 创建新的Promise实例以实现链式调用
const promise2 = new MyPromise((resolve, reject) => {
/**
* 处理成功情况的函数
*/
const handleFulfilled = () => {
// 使用queueMicrotask确保异步执行
queueMicrotask(() => {
try {
// 如果onFulfilled是函数则执行它,否则直接传递值
const x = typeof onFulfilled === 'function'
? onFulfilled(this.value)
: this.value;
// 解析返回的值
this.resolvePromise(promise2, x, resolve, reject);
} catch (err) {
// 如果回调函数抛出错误,reject新Promise
reject(err);
}
});
};
/**
* 处理失败情况的函数
*/
const handleRejected = () => {
// 使用queueMicrotask确保异步执行
queueMicrotask(() => {
try {
// 如果onRejected是函数则执行它,否则直接传递原因
const x = typeof onRejected === 'function'
? onRejected(this.value)
: this.value;
// 解析返回的值
this.resolvePromise(promise2, x, resolve, reject);
} catch (err) {
// 如果回调函数抛出错误,reject新Promise
reject(err);
}
});
};
// 根据当前状态决定如何处理
if (this.state === 'fulfilled') {
// 如果已经是fulfilled状态,异步执行回调
handleFulfilled();
} else if (this.state === 'rejected') {
// 如果已经是rejected状态,异步执行回调
handleRejected();
} else {
// 如果是pending状态,将回调函数存入数组
this.onFulfilledCallbacks.push(handleFulfilled);
this.onRejectedCallbacks.push(handleRejected);
}
});
return promise2;
}
/**
* 解析Promise返回值的方法
* @param {MyPromise} promise2 - 新的Promise实例
* @param {any} x - then回调返回的值
* @param {Function} resolve - 新Promise的resolve函数
* @param {Function} reject - 新Promise的reject函数
*/
resolvePromise(promise2, x, resolve, reject) {
// 如果返回的Promise和then返回的Promise相同,抛出循环引用错误
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
// 如果x是Promise实例,则等待它的结果
if (x instanceof MyPromise) {
x.then(resolve, reject);
}
// 如果x是对象或函数,可能是一个thenable对象
else if (typeof x === 'object' && x !== null || typeof x === 'function') {
let then;
try {
// 尝试获取x的then方法
then = x.then;
} catch (err) {
// 如果获取then方法时出错,reject
return reject(err);
}
// 如果then是函数,则认为x是thenable对象
if (typeof then === 'function') {
let called = false; // 防止多次调用
try {
// 调用then方法
then.call(
x,
// 成功回调
y => {
if (called) return;
called = true;
// 递归解析y
this.resolvePromise(promise2, y, resolve, reject);
},
// 失败回调
r => {
if (called) return;
called = true;
reject(r);
}
);
} catch (err) {
if (called) return;
reject(err);
}
} else {
// 普通对象,直接resolve
resolve(x);
}
} else {
// 基本类型值,直接resolve
resolve(x);
}
}
/**
* catch方法 - 错误处理快捷方式
* @param {Function} onRejected - 失败回调
* @returns {MyPromise} 返回一个新的Promise
*/
catch(onRejected) {
return this.then(null, onRejected);
}
/**
* finally方法 - 无论成功失败都会执行
* @param {Function} callback - 回调函数
* @returns {MyPromise} 返回一个新的Promise
*/
finally(callback) {
return this.then(
// 成功情况
value => MyPromise.resolve(callback()).then(() => value),
// 失败情况
reason => MyPromise.resolve(callback()).then(() => { throw reason })
);
}
/**
* 静态resolve方法
* @param {any} value - 要解析的值
* @returns {MyPromise} 返回一个已解决的Promise
*/
static resolve(value) {
// 如果已经是Promise实例,直接返回
if (value instanceof MyPromise) {
return value;
}
// 否则创建新的已解决Promise
return new MyPromise(resolve => resolve(value));
}
/**
* 静态reject方法
* @param {any} reason - 拒绝原因
* @returns {MyPromise} 返回一个已拒绝的Promise
*/
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
/**
* 静态all方法
* @param {Iterable} promises - 可迭代的Promise集合
* @returns {MyPromise} 返回一个新的Promise
*/
static all(promises) {
return new MyPromise((resolve, reject) => {
const results = []; // 存储结果
let completed = 0; // 完成计数器
// 空数组直接resolve
if (promises.length === 0) {
return resolve(results);
}
// 遍历所有Promise
promises.forEach((promise, index) => {
// 确保每个值都是Promise
MyPromise.resolve(promise).then(
// 成功处理
value => {
results[index] = value; // 按索引存储结果
completed++;
// 全部完成时resolve
if (completed === promises.length) {
resolve(results);
}
},
// 任何一个失败立即reject
reject
);
});
});
}
/**
* 静态race方法
* @param {Iterable} promises - 可迭代的Promise集合
* @returns {MyPromise} 返回一个新的Promise
*/
static race(promises) {
return new MyPromise((resolve, reject) => {
// 遍历所有Promise
promises.forEach(promise => {
// 确保每个值都是Promise
MyPromise.resolve(promise).then(
// 第一个解决或拒绝的结果决定race的结果
resolve,
reject
);
});
});
}
/**
* 静态allSettled方法
* @param {Iterable} promises - 可迭代的Promise集合
* @returns {MyPromise} 返回一个新的Promise
*/
static allSettled(promises) {
return new MyPromise(resolve => {
const results = []; // 存储结果
let completed = 0; // 完成计数器
// 空数组直接resolve
if (promises.length === 0) {
return resolve(results);
}
// 遍历所有Promise
promises.forEach((promise, index) => {
// 确保每个值都是Promise
MyPromise.resolve(promise).then(
// 成功处理
value => {
results[index] = { status: 'fulfilled', value };
completed++;
// 全部完成时resolve
if (completed === promises.length) {
resolve(results);
}
},
// 失败处理
reason => {
results[index] = { status: 'rejected', reason };
completed++;
// 全部完成时resolve
if (completed === promises.length) {
resolve(results);
}
}
);
});
});
}
/**
* 静态any方法
* @param {Iterable} promises - 可迭代的Promise集合
* @returns {MyPromise} 返回一个新的Promise
*/
static any(promises) {
return new MyPromise((resolve, reject) => {
const errors = []; // 存储所有错误
let rejected = 0; // 拒绝计数器
// 空数组直接reject
if (promises.length === 0) {
return reject(new AggregateError([], 'All promises were rejected'));
}
// 遍历所有Promise
promises.forEach((promise, index) => {
// 确保每个值都是Promise
MyPromise.resolve(promise).then(
// 任何一个成功立即resolve
resolve,
// 失败处理
reason => {
errors[index] = reason; // 存储错误
rejected++;
// 全部失败时reject
if (rejected === promises.length) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
}
);
});
});
}
}
- 状态管理机制:
- Promise有三种不可逆状态: pending → fulfilled/rejected
- 状态一旦改变就不能再变,确保Promise的确定性
- 异步执行保证:
- 使用queueMicrotask实现微任务队列(比setTimeout优先级更高)
- 符合Promise/A+规范的2.2.4条要求回调必须异步执行
- 链式调用实现:
- 每个then都返回新Promise,形成调用链
- 通过resolvePromise方法处理各种返回值(Promise/thenable/普通值)
- 错误处理流程:
- 执行器错误 → 直接reject
- 回调错误 → 捕获并传递给下一个Promise
- 通过catch方法提供便捷的错误处理
- 静态方法设计:
- all: 等待所有成功或第一个失败
- race: 采用第一个解决的结果
- allSettled: 等待所有Promise完成
- any: 采用第一个成功的结果或所有失败
- 边缘情况处理:
- 循环引用检测
- thenable对象处理
- 空数组处理
- 值穿透实现
符合 Promise A+ 规范的重要特性
- then 方法可以被同一个 promise 调用多次:通过回调数组实现多个回调的注册
- then 方法必须返回一个 promise:每次调用 then 都创建并返回新的 Promise
- 值的穿透:当 then 的参数不是函数时,值会穿透到下一个 then
- 错误冒泡:链式调用中,前面的错误会被后面的 catch 捕获
带缓存的函数记忆化工具
- 支持自定义resolver处理复杂参数
- 使用Map而非普通对象存储缓存
- 保留原函数的this绑定
- 提供缓存清除接口
function memoize(fn, resolver) {
// 使用WeakMap保存缓存,键为this对象时更安全
const cache = new Map();
const memoized = function(...args) {
// 生成缓存key:使用resolver或直接JSON.stringify
const key = resolver
? resolver.apply(this, args)
: JSON.stringify(args);
// 检查缓存
if (cache.has(key)) {
return cache.get(key);
}
// 执行原函数并缓存结果
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
// 允许手动清除缓存
memoized.clear = () => cache.clear();
return memoized;
}
// 测试用例
const add = (a, b) => {
console.log('Calculating...');
return a + b;
};
const memoizedAdd = memoize(add);
console.log(memoizedAdd(1, 2)); // Calculating... 3
console.log(memoizedAdd(1, 2)); // 3 (from cache)
// 带对象参数的案例
const getUser = memoize(
(userId, obj) => ({...obj, id: userId}),
(userId, obj) => `user-${userId}-${obj.lang}`
);
console.log(getUser(1, {lang: 'en'})); // Calculating...
console.log(getUser(1, {lang: 'en'})); // From cache
异步任务调度器
- 使用队列管理等待任务
- 通过running计数器控制并发
- 返回Promise保持链式调用
- 任务完成后自动触发下一个
class Scheduler {
constructor(concurrency) {
this.max = concurrency;
this.queue = [];
this.running = 0;
}
add(promiseCreator) {
return new Promise((resolve, reject) => {
// 包装任务以处理执行和计数
const task = () => {
this.running++;
promiseCreator()
.then(resolve)
.catch(reject)
.finally(() => {
this.running--;
this.next();
});
};
// 根据当前运行数量决定立即执行或入队
if (this.running < this.max) {
task();
} else {
this.queue.push(task);
}
});
}
next() {
if (this.queue.length > 0 && this.running < this.max) {
const task = this.queue.shift();
task();
}
}
}
// 测试用例
const timeout = (time) => new Promise(r => setTimeout(r, time));
const scheduler = new Scheduler(2);
const addTask = (time, order) => {
scheduler.add(() => timeout(time))
.then(() => console.log(order));
};
addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
// 输出:2 3 1 4
深度对象比较
- 处理7种基本类型的比较
- 特殊对象(Date/RegExp)的单独处理
- 数组与对象的递归比较
- 属性顺序不影响比较结果
- 循环引用检测(可扩展实现)
function isEqual(obj1, obj2) {
// 基本类型直接比较
if (obj1 === obj2) return true;
// 处理null/undefined
if (obj1 == null || obj2 == null) {
return obj1 === obj2;
}
// 类型不同直接返回false
if (Object.prototype.toString.call(obj1) !==
Object.prototype.toString.call(obj2)) {
return false;
}
// 处理Date/RegExp等特殊对象
if (obj1 instanceof Date) return +obj1 === +obj2;
if (obj1 instanceof RegExp) return obj1.toString() === obj2.toString();
// 处理原始值包装对象
if (typeof obj1 !== 'object') return obj1 === obj2;
// 处理数组
if (Array.isArray(obj1)) {
if (obj1.length !== obj2.length) return false;
return obj1.every((item, i) => isEqual(item, obj2[i]));
}
// 处理普通对象
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
return keys1.every(key => {
// 递归比较每个属性
return isEqual(obj1[key], obj2[key]);
});
}
// 测试用例
console.log(isEqual(
{ a: 1, b: { c: [1,2,3], d: new Date('2020-01-01') } },
{ a: 1, b: { c: [1,2,3], d: new Date('2020-01-01') } }
)); // true
console.log(isEqual(
{ a: 1, b: { c: [1,2,3] } },
{ a: 1, b: { c: [1,2,4] } }
)); // false
内存泄漏常见场景及检测方法
内部函数长期被引用
把大数组保存在外层作用域,内部函数长期被引用,导致大数组无法释放。
<body>
<button id="create">创建闭包泄漏</button>
<button id="release">释放闭包</button>
<script>
let closureLeak; // ① 全局变量始终引用闭包
document.getElementById('create').onclick = () => {
const big = new Array(1e6).fill('leak'); // ~8 MB
closureLeak = function () { // ② 内部函数被长期持有
console.log(big.length); // ③ big 无法被 GC
};
};
document.getElementById('release').onclick = () => {
closureLeak = null; // ④ 解除引用 → 稍后 GC
};
</script>
</body>
泄漏原因
- big 被内部函数捕获,而内部函数又挂在全局变量 closureLeak 上 → V8 认为仍在使用。
- 如果 closureLeak 一直不置空,哪怕页面切走 这 8 MB 也不会释放。
未清理的 DOM 引用
用 JS 对象缓存 DOM,节点从文档移除后,缓存仍引用导致无法回收。
<ul id="list"></ul>
<button id="add">新增 li</button>
<button id="clear">清空 DOM</button>
<script>
const cache = {}; // ① 手动缓存
let counter = 0;
document.getElementById('add').onclick = () => {
const li = document.createElement('li');
li.textContent = 'item ' + (++counter);
cache[counter] = li; // ② 强引用
document.getElementById('list').appendChild(li);
};
document.getElementById('clear').onclick = () => {
document.getElementById('list').innerHTML = ''; // ③ 节点脱离文档
// cache 未清理 → 节点仍存活
};
</script>
泄漏原因:
- 节点虽然不在 DOM 树,但 cache 对象仍强引用它们 → 无法 GC。
- 如果 cache 是全局变量或模块级变量,泄漏将伴随整个页面生命周期。
document.getElementById('clear').onclick = () => {
document.getElementById('list').innerHTML = '';
Object.keys(cache).forEach(k => delete cache[k]); // 解除引用
};
用 Chrome DevTools 检测内存泄漏
- 打开 DevTools → Memory → 选择「Heap snapshot」
- 操作前拍一次快照「Snapshot 1」
- 触发泄漏动作(如点击「创建闭包泄漏」10 次)
- 再拍「Snapshot 2」,然后对比:
- Summary 视图 → 选择「Objects allocated between Snapshot 1 and 2」
- 过滤关键词:(string)、HTMLLIElement、(closure)
- 展开构造函数 → 看 Retained Size(保留大小),如果持续增长即可确认泄漏。
- 进阶:Allocation instrumentation on timeline
- DevTools → Memory → Allocation instrumentation on timeline → 开始录制 → 操作页面 → 停止。
- 红色柱形表示未释放分配,可直接定位到代码行。
大型单页应用的首屏加载速度?
- 代码分割与懒加载策略:减少初始加载的JavaScript体积
- 动态导入:使用 import() 语法实现路由级和组件级分割
- 路由懒加载:配合React Router/Vue Router按需加载路由
- 第三方库分割:通过Webpack的 splitChunks 分离第三方依赖
- 组件级懒加载:非关键组件(如弹窗、Tab内容)延迟加载
- 预加载关键资源:优先加载影响首屏的核心资源
- 资源优先级标记
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="main.js" as="script">
<link rel="prefetch" href="next-page-chunk.js">
- Webpack魔法注释:预加载异步模块
import(/* webpackPreload: true */ 'AnalyticsModule');
- 关键CSS内联:将首屏CSS直接内嵌到HTML中,避免请求阻塞
- 字体预加载
<link rel="preload" href="font.woff2" as="font" crossorigin>
- 资源优先级标记
- SSR与CSR结合方案
- 静态SSG+动态CSR:
- 使用Next.js/Nuxt.js对静态页面预渲染(SSG)
- 动态内容通过客户端 hydration 接管
- 流式SSR:分块传输HTML(如React 18的 renderToPipeableStream)
- 部分SSR:仅对首屏关键部分服务端渲染(如导航栏、骨架屏)
- Edge CDN渲染:利用Cloudflare Workers/Vercel Edge Functions就近渲染
- 静态SSG+动态CSR:
- 其他关键优化
- CDN加速静态资源:压缩并分发JS/CSS/图片
- 缓存策略:强缓存(Cache-Control: max-age=31536000)与版本哈希
- 图片优化:
- 使用WebP/AVIF格式
- 响应式图片(srcset)
- SVG雪碧图替代图标字体
- 减少主线程工作:
- Web Worker处理复杂计算
- 避免同步布局抖动
高性能无限滚动列表
import React, {
useState,
useRef,
useEffect,
useCallback,
useMemo,
} from "react";
interface VirtualListProps<T> {
data: T[]; // 数据源
renderItem: (item: T, index: number) => React.ReactNode; // 单项渲染函数
estimatedItemHeight?: number; // 预估行高(动态高度必传)
bufferSize?: number; // 缓冲区大小
className?: string;
}
/**
* 高性能虚拟滚动列表
* 核心功能:
* 1. 仅渲染可视区域内的元素
* 2. 支持动态高度(自动测量)
* 3. Intersection Observer优化滚动性能
* 4. DOM节点回收复用
*/
function VirtualList<T>({
data,
renderItem,
estimatedItemHeight = 50,
bufferSize = 3,
className,
}: VirtualListProps<T>) {
const containerRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<Map<number, HTMLDivElement>>(new Map()); // 存储已渲染的DOM节点
const [visibleRange, setVisibleRange] = useState({ start: 0, end: 0 });
const [itemPositions, setItemPositions] = useState<number[]>([]); // 每个元素的位置缓存
// 1. 初始化位置缓存(基于预估高度)
const initPositions = useCallback(() => {
const positions = [0];
for (let i = 1; i <= data.length; i++) {
positions[i] = positions[i - 1] + estimatedItemHeight;
}
setItemPositions(positions);
}, [data.length, estimatedItemHeight]);
// 2. 动态更新元素位置(实际测量高度)
const updateItemPosition = useCallback(
(index: number, height: number) => {
if (Math.abs(itemPositions[index + 1] - itemPositions[index] - height) < 1) {
return; // 高度无变化时不更新
}
setItemPositions((prev) => {
const newPositions = [...prev];
const diff = height - (newPositions[index + 1] - newPositions[index]);
for (let i = index + 1; i <= data.length; i++) {
newPositions[i] += diff;
}
return newPositions;
});
},
[data.length, itemPositions]
);
// 3. 计算当前可视区域
const calculateVisibleRange = useCallback(() => {
if (!containerRef.current) return;
const { scrollTop, clientHeight } = containerRef.current;
const startIdx = findNearestIndex(itemPositions, scrollTop);
const endIdx = findNearestIndex(itemPositions, scrollTop + clientHeight);
setVisibleRange({
start: Math.max(0, startIdx - bufferSize),
end: Math.min(data.length - 1, endIdx + bufferSize),
});
}, [itemPositions, bufferSize, data.length]);
// 4. 使用Intersection Observer监听可视区域变化
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting || entry.intersectionRatio > 0) {
calculateVisibleRange();
}
});
},
{
root: containerRef.current,
rootMargin: `${bufferSize * estimatedItemHeight}px`,
threshold: 0.1,
}
);
// 观察底部哨兵元素
const sentinel = document.createElement("div");
sentinel.style.position = "absolute";
sentinel.style.bottom = "0";
sentinel.style.height = "1px";
containerRef.current?.appendChild(sentinel);
observer.observe(sentinel);
return () => {
observer.disconnect();
containerRef.current?.removeChild(sentinel);
};
}, [calculateVisibleRange, bufferSize, estimatedItemHeight]);
// 5. 初始化位置和可视区域
useEffect(() => {
initPositions();
calculateVisibleRange();
}, [initPositions, calculateVisibleRange]);
// 6. 渲染可视区域内的元素
const visibleItems = useMemo(() => {
const items = [];
for (let i = visibleRange.start; i <= visibleRange.end; i++) {
items.push(
<div
key={i}
ref={(node) => {
if (node) {
itemRefs.current.set(i, node);
// 动态高度测量
if (estimatedItemHeight > 0) {
const realHeight = node.getBoundingClientRect().height;
updateItemPosition(i, realHeight);
}
} else {
itemRefs.current.delete(i);
}
}}
style={{
position: "absolute",
top: `${itemPositions[i]}px`,
width: "100%",
}}
>
{renderItem(data[i], i)}
</div>
);
}
return items;
}, [
visibleRange,
data,
renderItem,
itemPositions,
updateItemPosition,
estimatedItemHeight,
]);
return (
<div
ref={containerRef}
className={`virtual-list ${className}`}
style={{
height: "100%",
overflowY: "auto",
position: "relative",
transform: "translateZ(0)", // GPU加速
}}
onScroll={calculateVisibleRange}
>
{/* 撑开容器高度的占位元素 */}
<div
style={{
height: `${itemPositions[data.length] || 0}px`,
position: "relative",
}}
>
{visibleItems}
</div>
</div>
);
}
/** 二分查找最近的元素索引(性能关键) */
function findNearestIndex(arr: number[], value: number): number {
let low = 0;
let high = arr.length - 1;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
if (arr[mid] === value) return mid;
if (arr[mid] < value) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return low;
}
export default React.memo(VirtualList);
- 虚拟列表核心逻辑
- 可视区域计算:通过 containerRef.current.scrollTop 和 clientHeight 确定当前视口位置
- 二分查找优化:findNearestIndex 快速定位起始/结束索引(时间复杂度 O(log n))
- 绝对定位:每个元素通过 top 定位到正确位置,避免重排
- 动态高度处理
- 双高度策略:
- 初始化使用 estimatedItemHeight 估算位置
- 渲染后通过 getBoundingClientRect() 获取实际高度并修正位置
- 差值传播:当某个元素高度变化时,递归更新后续所有元素的位置
- 双高度策略:
- 性能优化手段
优化技术 | 实现方式 | 效果 |
---|---|---|
Intersection Observer | 监听底部哨兵元素,替代滚动事件 | 减少主线程压力 |
GPU加速 | transform: translateZ(0) 强制开启GPU层 | 平滑滚动 |
缓冲区 | bufferSize 扩展渲染范围,防止快速滚动白屏 | 提升用户体验 |
DOM节点复用 | itemRefs Map保存已渲染节点,避免重复创建 | 减少内存占用 |
完整工作流程
- 初始化:根据预估高度生成初始位置数组 [0, h1, h1+h2, ...]
- 滚动触发:计算新 visibleRange → 渲染新元素 → 回收旧节点
- 高度修正:对新渲染元素进行实际测量 → 更新位置缓存 → 触发重计算
- 渲染优化:复用已有DOM → GPU加速滚动 → 异步分批渲染
WebAssembly
WebAssembly(简称 WASM)是一个二进制指令格式,它让 C/C++/Rust/Go 等语言编译的产物能在浏览器或任何 WASM 运行时里以接近原生速度安全地运行,并与 JavaScript 无缝互调。
维度 | 描述 |
---|---|
字节码 | .wasm 文件,体积小、可流式编译,比 JS 解析快 10-30 倍 |
性能 | 接近原生(80%-90%),可利用 SIMD、多线程、64 位内存 |
安全沙盒 | 运行在浏览器或 Node.js 的线性内存 + Capability 模型里,无越界访问 |
语言无关 | 任何能编译到 LLVM 的语言(C/C++、Rust、Go、AssemblyScript、C#、Swift…)皆可 |
JS 互操作 | 通过 import/export 与 JS 共享内存、函数调用,零拷贝 |
应用场景
- 视频解码:FFmpeg编译为WASM处理H.264
- 密码学:SHA-3算法比JS快20倍
- 游戏物理引擎:Box2D/Ammo.js实现碰撞检测
JS预编译
全局预编译
脚本加载时
- 变量声明提升:var 声明被提升,初始值为 undefined
- 函数声明提升:整个函数体被提升
- 函数优先:同名时函数覆盖变量
console.log(a); // 输出: undefined (变量提升)
var a = 1;
function a() {} // 函数声明提升并覆盖变量a
console.log(a); // 输出: 1 (赋值覆盖函数)
// 预编译后的等效代码:
/*
function a() {}
var a;
console.log(a); // 此时a是函数
a = 1;
console.log(a);
*/
函数预编译
函数调用前
- 创建 AO(Activation Object)
- 形参和变量声明提升到AO,值为 undefined
- 实参赋值给形参
- 函数声明提升并覆盖同名变量
function test(b) {
console.log(b); // 输出: function b() {} (函数声明覆盖形参)
var b = 2;
console.log(b); // 输出: 2 (赋值覆盖)
function b() {} // 提升到AO顶部
}
test(1);
// 预编译后的等效代码:
/*
function test(b) {
function b() {} // 函数声明提升
var b; // 已存在同名函数,忽略
b = 1; // 形参赋值
console.log(b); // 输出函数
b = 2; // 重新赋值
console.log(b);
}
*/
ES6+ 的影响
let/const:存在暂时性死区(TDZ),不会提升
console.log(c); // ReferenceError
let c = 3;
函数表达式:不会提升
foo(); // TypeError: foo is not a function
var foo = function() {};
面试题:
var x = 1;
function foo(
x,
y = function() { x = 2; }
) {
var x = 3;
y();
console.log(x); // 输出: 3
}
foo();
console.log(x); // 输出: 1
/*
解析:
1. 函数参数作用域与函数体作用域分离
2. y()修改的是参数x(=2),而内部var x=3是新的局部变量
3. 最外层的x未被影响
*/
手写
实现数组的map方法
function myMap(arr, callback) {
// 检查参数是否正确
if (!Array.isArray(arr) || !arr.length || typeof callback !== 'function') {
return [];
} else {
let result = [];
for (let i = 0, len = arr.length; i < len; i++) {
result.push(callback(arr[i], i, arr))
}
return result
}
}
实现instanceof
const _instanceof = (target, Fn) => {
if ((typeof target !== 'object' && typeof target !== 'function') || target === null)
return false;
let proto = target.__proto__
while (true) {
if (proto === null) return false
if (proto === Fn.prototype) return true
proto = proto.__proto__
}
}
function A() {}
const a = new A()
console.log(_instanceof(a, A)) // true
console.log(_instanceof(1, A)) // false
实现call方法
实现apply方法
var name = '公众号:前端技术营';
var obj = {
name: '张三'
};
function fn(a, b, c) {
console.log(`${a} ${b} ${c} ${this.name}`);
};
// 模拟apply
Function.prototype.myApply = function(){
let target = arguments[0]|| window || global;
target.fn = this;
let args = Array.from(arguments)[1];
let result = target.fn(...args);
delete target.fn;
return result;
}
fn.myApply(obj, ['my', 'name', 'is']); // my name is 张三
fn.myApply(null, ['my', 'name', 'is']); // my name is 公众号:前端技术营
fn.myApply(undefined, ['my', 'name', 'is']); // my name is 公众号:前端技术营
实现bind方法
var name = '公众号:前端技术营';
var obj = {
name: '张三'
};
function fn(a, b, c) {
console.log(`${a} ${b} ${c} ${this.name}`);
};
// 模拟call
Function.prototype.myBind = function(context) {
const _this = this
const args = [...arguments].slice(1);
// 返回一个函数
return function F() {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
if (this instanceof F) {
// 忽略传入的this
return new _this(...args, ...arguments)
}
// 直接调用,将两边的参数拼接起来
return _this.apply(context, args.concat(...arguments))
}
}
const fn1 = fn.myBind(obj, 'my', 'name', 'is');
fn1(); // my name is 张三
const fn2 = fn.myBind(null, 'my', 'name', 'is');
fn2(); // my name is 公众号:前端技术营
const fn3 = fn.myBind(undefined, 'my', 'name', 'is');
fn3(); // my name is 公众号:前端技术营
实现new关键字
function myNew() {
// 创建一个空对象
let obj = new Object();
// shift删除数组第一个元素,并返回一个元素。原有arguments数组第一个参数是构造函数,返回值为构造函数。
const Constructor = [].shift.call(arguments);
// 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
obj.__proto__ = Constructor.prototype;
// 执行构造函数的方法
const ret = Constructor.apply(obj, arguments);
// 返回构造函数中有返回值且返回值是个引用类型,没有返回值则默认返回这个对象
return typeof ret === 'object' ? ret : obj;
}
function Person(name, age) {
this.name = name;
this.age = age
// 模拟有返回值且是个对象
return {
name: name,
address: '上海市浦东新区'
};
}
const a = myNew(Person, '公众号:前端技术营', 18);
console.log(a.name); // 公众号:前端技术营
console.log(a.address); // 上海市浦东新区
console.log(a.age); // undefined
实现函数柯里化
把固定 n 个参数的函数拆成 n 个嵌套的一元函数,让调用可以逐次传参,最后一次性返回结果。
function curry(fn, args) {
// 获取函数需要的参数长度
let length = fn.length;
args = args || [];
return function() {
let subArgs = args.slice(0);
// 拼接得到现有的所有参数
for (let i = 0; i < arguments.length; i++) {
subArgs.push(arguments[i]);
}
// 判断参数的长度是否已经满足函数所需参数的长度
if (subArgs.length >= length) {
// 如果满足,执行函数
return fn.apply(this, subArgs);
} else {
// 如果不满足,递归返回科里化的函数,等待参数的传入
return curry.call(this, fn, subArgs);
}
};
}
// es6 实现
function curry(fn, ...args) {
return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}
实现object.create()方法
function objectCreate(obj){
function F(){}; // 构造函数
F.prototype = obj; // obj 作为构造函数原型的属性
return new F(); // 返回一个实例对象 就像 `let ff = new F(); return ff`
}
数组去重
// 1.利用Set
const res = Array.from(new Set(arr));
// 2.两层for循环+splice
const unique = arr => {
let len = arr.length;
for (let i = 0; i < len; i++) {
for (let j = i + 1; j < len; j++) {
if (arr[i] === arr[j]) {
arr.splice(j, 1);
len--;
j--;
}
}
}
return arr;
}
// 3.利用indexOf
const unique = arr => {
const res = [];
for (let i = 0; i < arr.length; i++) {
if (res.indexOf(arr[i]) === -1) res.push(arr[i]);
}
return res;
}
// 4.利用include
const unique = arr => {
const res = [];
for (let i = 0; i < arr.length; i++) {
if (!res.includes(arr[i])) res.push(arr[i]);
}
return res;
}
// 5.利用filter
const unique = arr => {
return arr.filter((item, index) => {
return arr.indexOf(item) === index;
});
}
// 6.利用Map
const unique = arr => {
const map = new Map();
const res = [];
for (let i = 0; i < arr.length; i++) {
if (!map.has(arr[i])) {
map.set(arr[i], true);
res.push(arr[i]);
}
}
return res;
}
数组求和
// 1.利用for循环
function getSum(arr){
let sum = 0;
for(var i = 0;i<arr.length;i++){
sum += arr[i];
}
return sum ;
}
// 2.利用reduce
const sum = arr.reduce(function (prev, cur) {
return prev + cur;
}, 0);
// 3.利用forEach
function getSum(arr) {
let sum = 0;
arr.forEach((val) => {
sum += val;
});
return sum;
};
// 4.使用递归实现数组求和
function sum(arr) {
if (arr.length === 0) {
return 0;
}
return arr[0] + sum(arr.slice(1));
}
xxx
xxx
参考:
https://mp.weixin.qq.com/s/xtyrAj1qtkUFLdb75dShtw
https://mp.weixin.qq.com/s/39zJtMHUFSTAfhPXEXDvIA
https://mp.weixin.qq.com/s/x2xjF_6_O399EBxxwYqtQw
https://mp.weixin.qq.com/s/nkUw6qPJGFRHQuYLmsXKWQ
https://mp.weixin.qq.com/s/rLuGgS-HL2s3GUeJOk_F9w