手写
debounce 防抖
function debounce(fn, delay, immediate = false) {
let timer = null;
function debounced(...args){
const context = this;
if(timer) clearTimeout(timer);
if(immediate && !timer) {
fn.apply(context, args);
}
timer = setTimeout(() => {
timer = null;
if(!immediate) {
fn.apply(context, args);
}
}, delay)
}
debounced.cancel = function(){
if(timer) clearTimeout(timer);
timer = null;
}
return debounced;
}
throttle 节流
function throttle(fn) {
let canRun = true;
function throttled (...args){
const context = this;
if(canRun) {
canRun = false;
fn.apply(context, args);
}
}
throttled.enable = function(){
canRun = true;
}
throttled.disable = function(){
canRun = false;
}
return throttled;
}
深度优先搜索
function findPathInArrayTree(arrayTree, target) {
function dfs(nodes, pathSoFar){
for(const node of nodes) {
const newPath = [...pathSoFar, node.value]
if(node.value === target) return newPath;
if(node.children && node.children.length > 0) {
const res = dfs(node.children, newPath);
if(res) return res;
}
}
return null;
}
return dfs(arrayTree, []);
}
const tree = [ { value: 1, children: [] },
{ value: 2, children: [
{ value: 3, children: [] },
{ value: 4, children: [
{ value: 5, children: [] }
] }
] }
];
console.log(findPathInArrayTree(tree, 5));
定时器
function mySetTimeout(fn, delay){
const start = new Date();
function tick(){
if(new Date() - start >= delay) {
fn();
} else {
requestAnimationFrame(tick); // 用动画帧循环检查
}
}
tick();
}
Promise.all
function promiseAll(promises){
return new Promise((resolve, reject) => {
if(!Array.isArray(promises)) {
return reject(new TypeError('Argument must be an array'))
}
const results = [];
let count = 0;
promises.forEach((item,index) => {
Promise.resolve(item).then(value => {
results[index] = value;
count += 1;
if(count === promises.length) {
resolve(results);
}
}).catch(error => {
reject(error);
})
})
if(!promises.length) {
resolve([]);
}
})
}
深拷贝
function deepClone(obj, map = new WeakMap()){
// 处理 null
if(obj === null) return obj;
// 处理非对象类型
if(typeof obj !== 'object') return obj;
// 处理循环引用(array、object可能存在循环引用)
if(map.has(obj)) return map.get(obj);
let clone = null;
// 处理Array
if(Array.isArray(obj)) {
clone = [];
map.set(obj, clone);
for(let i=0; i<obj.length;i++) {
clone[i] = deepClone(obj[i], map);
}
return clone;
}
// 处理正则
if(obj instanceof RegExp ) {
return new RegExp(obj.source, obj.flags);
}
// 处理Date
if(obj instanceof Date ) {
return new Date(obj);
}
// 处理Error
if(obj instanceof Error ) {
return new Error(obj);
}
// 处理Object
if(obj instanceof Object){
// 这里不是{}
clone = Object.create(Object.getPrototypeOf(obj));
map.set(obj, clone);
for(let key of Object.keys(obj)) {
clone[key] = deepClone(obj[key], map);
}
return clone;
}
}
Tree组件
- 调用组件
<MyTree data={mockTreeData} />
2. Tree组件
const MyTree = (props: IMyTree) => {
const { data, onSelect, renderLabel } = props;
// 展开
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set([]));
// 选中项
const [selectedIds, setSelectedIds] = useState<string>();
// 展开折叠
const toggleExpand = useCallback(
(id: string) => {
const currentExpandedIds = new Set(expandedIds);
if (currentExpandedIds.has(id)) {
currentExpandedIds.delete(id);
} else {
currentExpandedIds.add(id);
}
setExpandedIds(currentExpandedIds);
},
[expandedIds]
);
const toggleSelect = useCallback(
(id: string) => {
if (selectedIds === id) {
return;
}
setSelectedIds(id);
onSelect?.(id);
},
[selectedIds, onSelect]
);
return (
<div className="my-tree-wrapper">
{data.length <= 0 ? (
<div>暂无数据</div>
) : (
data.map((node) => (
<MyTreeItem
key={node.id}
node={node}
level={0}
expandedIds={expandedIds}
toggleExpand={toggleExpand}
selectedIds={selectedIds}
toggleSelect={toggleSelect}
renderLabel={renderLabel}
/>
))
)}
</div>
);
};
3. TreeItem组件
const MyTreeItem = (props: IMyTreeItem) => {
const {
node,
level,
expandedIds,
toggleExpand,
selectedIds,
toggleSelect,
renderLabel,
} = props;
// 包含子元素
const hasChildren = useMemo(
() => node.children && node.children.length > 0,
[node]
);
// 展开项
const isExpanded = useMemo(
() => expandedIds.has(node.id),
[expandedIds, node]
);
// 选中项
const isSelected = useMemo(
() => selectedIds === node.id,
[selectedIds, node]
);
const handleClick = useCallback(() => {
if(hasChildren) {
toggleExpand(node.id);
} else {
toggleSelect(node.id);
}
},[hasChildren, toggleExpand, toggleSelect, node]);
return (
<div className={`my-tree-item-wrapper ${isSelected ? "is-selected" : ""}`} style={{ paddingLeft: level * 16 }}>
{/* 当前元素 */}
<div
className={'my-tree-item-content'}
onClick={handleClick}
>
{hasChildren && (
<span className={"my-tree-item-content-expand"}>
{isExpanded ? "-" : "+"}
</span>
)}
<span className="my-tree-item-content-label">
{renderLabel
? renderLabel({ node, isExpanded, level, isSelected })
: node.label}
</span>
</div>
{/* 子元素 */}
{hasChildren && isExpanded && (
<div className="my-tree-item-children-content">
{node.children?.map((child) => (
<MyTreeItem
key={child.id}
node={child}
level={level + 1}
expandedIds={expandedIds}
toggleExpand={toggleExpand}
selectedIds={selectedIds}
toggleSelect={toggleSelect}
renderLabel={renderLabel}
/>
))}
</div>
)}
</div>
);
};
可选中的List
- 调用组件
<MyList data={mockData} />
2. List组件
export default function MyList({ data }: {data: IListData[]}){
const [checkedIds, setCheckedIds] = useState<Set<number>>(new Set([]));
return (
<div>
{data.map(node => (
<MyListItem
key={node.id}
node={node}
checkedIds={checkedIds}
setCheckedIds={setCheckedIds}
/>
))}
</div>
);
}
3. ListItem组件
export default function MyListItem(props: {
node: IListData;
checkedIds: Set<number>;
parentNode?: IListData;
setCheckedIds: (val: Set<number>) => void;
}) {
const { node, checkedIds, setCheckedIds } = props;
const isChecked = useMemo(() => checkedIds.has(node.id), [node, checkedIds]);
const toggleCheck = useCallback(() => {
if (node.children?.length) {
// 父节点
const childIds = new Set(node.children.map((item) => item.id));
if (checkedIds.has(node.id)) {
// 同时消除掉所有的子元素
const newCheckedIds = new Set(
[...checkedIds].filter(
(item) => !childIds.has(item) && item !== node.id
)
);
setCheckedIds(newCheckedIds);
} else {
const newCheckedIds = new Set([...checkedIds, node.id, ...childIds]);
setCheckedIds(newCheckedIds);
}
} else {
// 子节点
const newChecked = new Set([...checkedIds]);
if (checkedIds.has(node.id)) {
// 取消子节点
newChecked.delete(node.id);
} else {
// 选中子节点
newChecked.add(node.id);
}
setCheckedIds(newChecked);
}
}, [checkedIds, node, setCheckedIds]);
return (
<div>
<label>
<input type="checkbox" checked={isChecked} onChange={toggleCheck} />
{node.label}
</label>
{node.children && node.children.length > 0 && (
<div style={{ paddingLeft: "12px" }}>
{node.children.map((child) => (
<MyListItem
key={child.id}
node={child}
parentNode={node}
checkedIds={checkedIds}
setCheckedIds={setCheckedIds}
/>
))}
</div>
)}
</div>
);
}
复杂工具类型
// 1️⃣ DeepPartial
// 把对象所有属性及子属性都变成可选
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
// 2️⃣ DeepReadonly
// 把对象所有属性及子属性都变成只读
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
// 3️⃣ UnionToIntersection
// 把联合类型转成交叉类型
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends
(k: infer I) => void ? I : never;
// 5️⃣ Flatten
// 将嵌套数组展开为一维数组类型
type Flatten<T extends any[]> = T extends [infer First, ...infer Rest]
? First extends any[]
? [...Flatten<First>, ...Flatten<Rest>]
: [First, ...Flatten<Rest>]
: [];
// 8️⃣ ObjectValues
// 获取对象所有值的联合类型
type ObjectValues<T> = T[keyof T];
call、apply、bind
// 替换this指针指向,传递参数
Function.prototype.myCall = function(context, ...args){
context = context ?? globalThis; // context表示this新的指向
fn = Symbol('fn'); // 避免重名
context[fn] = this; // this表示调用mycall的函数, 等于 newObj[fn] = this
const res = context[fn](...args); // newObj(...args)
delete context[fn];
return res;
}
Function.prototype.myApply = function(context, args) {
context = context ?? globalThis;
fn = Symbol('fn');
context[fn] = this;
const res = Array.isArray(args) ? context[fn](...args) : context[fn]();
delete context[fn]
return res;
}
Function.prototype.myBind = function(context, ...args){
const fn = this;
function boundFn(...rest){
if(this instanceof boundFn) {
return new fn(...args, ...rest);
}
return fn.apply(context, [...rest, ...args]);
}
boundFn.prototype = Object.create(fn.prototype);
return boundFn;
}
第一个不重复字符(字符串 + 哈希)
function firstUniqChar(str){
const map = new Map();
for(let s of str) {
let count = map.get(s) ?? 0;
count += 1;
map.set(s, count);
}
for(let i=0;i<str.length;i++) {
if(map.get(str[i]) === 1) {
return i;
}
}
return -1;
}
console.log(firstUniqChar("lleetcode"));
判断字符串是否是一个 只考虑字母数字并忽略大小写的回文字符串
export function isPalindrome(str){
let s = str.replace(/[^a-zA-Z0-9]/g, "");
s = s.toLocaleLowerCase();
if(s.length <= 1) return true;
let leftIdx = 0;
let rightIdx = s.length - 1;
while(leftIdx <= rightIdx) {
if(s[leftIdx] === s[rightIdx]) {
leftIdx++;
rightIdx--;
} else {
return false;
}
}
return true;
}
console.log(isPalindrome("A++++ man, a plan, a canal: Panama"))
有效括号(栈)
function isValid(str){
const stack = [];
const map = {
'(': ')',
'{': '}',
'[': ']'
}
for(let s of str){
// 右半部分,需要出栈对比
if(!map[s]){
const lastS = stack.pop();
if(map[lastS] === s){
continue;
} else {
stack.push(lastS);
}
} else {
stack.push(s);
}
}
return stack.length <= 0;
}
Promise 并发控制
async function limitPromises(funcs, limit){
const result = [];
const running = [];
for(let fn of funcs){
const p = fn().then((res) => {
result.push(res);
running.splice(running.findIndex(p), 1);
})
running.push(p);
if(running.length >= limit) {
await Promise.race(running);
}
}
await Promise.all(running);
return result;
}
长度为 k 的滑动窗口,返回每个窗口最大值
function maxSlidingWindow(nums, k){
const res = [];
for(let i=0;i<=nums.length - k;i++) {
const curArray = nums.slice(i, i+k);
const maxNum = Math.max(...curArray);
res.push(maxNum);
}
return res;
}
console.log(maxSlidingWindow([1,3,-1,-3,5,3,6,7], 3));
移动零
function moveZeroes(nums){
let s=0;
for(let f=1;f<nums.length;f++) {
if(nums[s] === 0 && nums[f] !== 0) {
[nums[s], nums[f]] = [nums[f], nums[s]];
s++;
} else if(nums[s] !== 0) {
s++;
}
}
}
找出数组中两数和为目标值的下标
function twoSum(nums, target){
const map = new Map();
for(let i=0;i<nums.length;i++) {
const otherNum = target - nums[i];
if(map.has(otherNum)) {
return [map.get(otherNum), i];
} else {
map.set(nums[i], i);
}
}
return [];
}
Tree 查找目标值
function findPathInArrayTree(arrayTree, target) {
function dfs(nodes, pathSoFar){
for(const node of nodes) {
const newPath = [...pathSoFar, node.value]
if(node.value === target) return newPath;
if(node.children && node.children.length > 0) {
const res = dfs(node.children, newPath);
if(res) return res;
}
}
return null;
}
return dfs(arrayTree, []);
}
const tree = [ { value: 1, children: [] },
{ value: 2, children: [
{ value: 3, children: [] },
{ value: 4, children: [
{ value: 5, children: [] }
] }
] }
];
console.log(findPathInArrayTree(tree, 5));
Promise + SetTimeout
🥇 01. 并发限制调度器(异步霸榜 No.1)
业务场景:要发 100 个请求,但后端限流,每次只能发 N 个
扩展交互:可以在中途暂停执行,获取执行的结果
🌈实现思路:模拟一个迷你版“浏览器资源调度器”,这个调度器的核心本质,是通过「running 计数」「idx 游标」「runNext 自驱动」三者配合,实现一个动态的任务池。它保证任务源源不断执行,但同时不会超过给定的并发上限。
- 业务调用
export function MyWork() {
// 生成调度器
const scheduler = limitRequests(tasks, 3);
function handleStart() {
scheduler.start().then((res) => {
console.log("所有任务完成!");
console.log("结果:", res);
});
}
function handleEnd() {
console.log("暂停完成执行~");
scheduler.stop();
}
return (
<div>
<button onClick={handleStart}>开始</button>
<button onClick={handleEnd}>暂停</button>
</div>
);
}
2. 创建调度器
export function limitRequests(tasks, limit) {
const res = [] // 存所有任务返回的 Promise,用来最终 Promise.all
let idx = 0 // 当前处理到第几个任务
let running = 0 // 当前正在执行的任务数量(关键的并发控制变量)
let stopped = false // 用于标识是否已停止
// 暴露的停止执行的方法
function stop() {
stopped = true
}
function start() {
return new Promise((resolve, reject) => {
function runNext() {
// 执行队列处理完毕或者已暂停,返回结果
if (running === 0 && stopped) {
return resolve(Promise.all(res))
}
// 正在执行的任务数量不超过单次限制,存在未执行的任务
while (running < limit && idx < tasks.length) {
// 如果停止标志为 true,阻止新的任务加入
if (stopped) {
return
}
// 获取当前任务并执行
const cur = tasks[idx++]()
res.push(cur)
running++
cur.then(() => {
running--
runNext()
}).catch(reject)
}
}
runNext()
})
}
return { stop, start }
}
3. 模拟异步方法、准备数据
// 创建100个任务
export const tasks = Array.from({ length: 100 }, (_, i) => () => fetchData(i))
// 模拟异步请求方法
export function fetchData(id: number) {
return new Promise(resolve => {
const time = Math.random() * 2000
console.log(`开始任务: ${id}`)
setTimeout(() => {
console.log(`完成任务: ${id}`)
resolve(id)
}, time)
})
}
4. 自定义hook实现功能
const useLimitRequests = (tasks: any[], limit: number)=> {
const resultRef = useRef<number[]>([]); // 用 useRef 存储任务结果,避免重新渲染
const isStop = useRef<boolean>(false);
const idx = useRef<number>(0);
const reunning = useRef<number>(0);
const onStop = useCallback(() => {
isStop.current = true;
},[]);
const onStart = useCallback(() => {
return new Promise((resolve, reject) => {
function nextRun(){
if(reunning.current === 0 && isStop.current) {
return resolve(Promise.all(resultRef.current));
}
while(idx.current < tasks.length && reunning.current < limit){
if(isStop.current){
return;
}
const curTaskRes = tasks[idx.current]();
idx.current += 1;
resultRef.current.push(curTaskRes)
reunning.current += 1;
curTaskRes.then(() => {
reunning.current -= 1;
nextRun();
}).catch((error: any) => reject(error))
}
}
nextRun();
})
},[isStop, limit, tasks]);
return { onStop, onStart};
}
🥈 02. 支持指数退避的重试(Backoff Retry)
业务场景:接口偶尔报错,你希望自动重试 3 次,每次等待时间翻倍。
- 创建重试方法
function retry(fn, times = 3, delay = 500) {
return new Promise((resolve, reject) => {
const attempt = (n, d) => {
fn().then(resolve).catch(err => {
if (n === 0) return reject(err)
setTimeout(() => attempt(n - 1, d * 2), d)
})
}
attempt(times, delay)
})
}
2. 业务调用
function handleRetry() {
retry(mockRequest, 3, 500)
.then((result) => console.log(result)) // 如果请求成功,输出结果
.catch((error) => console.log(error)); // 如果重试失败,输出错误
}
🥉 03. 带超时控制的 Promise(Timeout Promise)
业务场景:请求超 3 秒自动失败,不等了
- 自定义函数实现
export function withTimeout(fn, ms){
// 存放定时器
let timer = null;
// 超时函数
const timeOut = () => new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error('超时了')), ms);
});
// Promise.race 会返回一个结果, fn 目标函数
return Promise.race([fn(), timeOut()]).finally(() => {
clearTimeout(timer);
})
}
2. 模拟延迟异步方法
export function slowTask() {
return new Promise((resolve) => {
setTimeout(() => resolve('Task completed'), 2000); // 模拟一个 3 秒的任务
});
}
3. 业务调用
function handleTimeOut() {
withTimeout(slowTask, 1000) // 设置 1 秒超时
.then((result) => console.log(result)) // 如果任务完成,输出结果
.catch((error) => console.log(error)); // 如果超时,输出超时错误
}
🚢 04. 串行任务:一步一步稳扎稳打
业务场景:分片上传、表单分步骤提交
核心逻辑:每个任务会按顺序一个接一个地执行,直到上一个任务完成后,才会开始下一个任务
- 自定义方法
export async function runInSequence(tasks){
const result = [];
for (const task of tasks) {
const res = await task();
result.push(res);
}
return result;
}
2. 模拟异步请求
export const fetchData = (task: any) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`Task ${task} completed`);
resolve(`Result of ${task}`);
}, 1000); // 每个任务延迟 1 秒
});
};
// 定义任务列表
export const tasks = [
() => fetchData('task1'),
() => fetchData('task2'),
() => fetchData('task3')
];
3. 调用
async function handleEquence(){
const result = await runInSequence(tasks);
console.log('All tasks completed', result);
}
⌚️ 05. Promise 版“多方等待 ready”机制
核心逻辑:这个机制用于让多个任务或组件等待某个条件(如
ready()方法被调用)满足后再继续执行
业务场景: 1. 多个任务依赖同一个条件:比如,多个组件在等待某个数据加载完成后再开始执行某个操作。 2. 等待多个异步任务的准备:多个异步任务可能依赖某个资源,只有当该资源准备好时,才能继续执行后续操作。 3. 协调并发任务的开始:不同的任务或组件可以等待一个共同的“开始信号”,一旦信号发送,所有等待的任务就可以同时开始。
- 自定义class
export class Waiter{
queue: any[];
readyFlag: boolean;
constructor(){
this.queue = []; // 所有等待的任务
this.readyFlag = false; // 是否已经准备好
}
wait(){
// 条件已经准备好了,直接返回一个已解决的 Promise
if(this.readyFlag) {
return Promise.resolve();
} else {
// 将该任务的 Promise 放入 queue 队列中,等待
return new Promise((r) => this.queue.push(r));
}
}
ready(){
this.readyFlag = true; // 设置条件已准备好
this.queue.forEach(r => r()); // 遍历队列并触发所有等待的任务
this.queue = []; // 清空队列
}
}
2. 调用
function handleReady() {
const waiter = new Waiter();
// 任务 1:等待条件准备好后执行
waiter.wait().then(() => console.log("Task 1 completed"));
// 任务 2:等待条件准备好后执行
waiter.wait().then(() => console.log("Task 2 completed"));
// 任务 3:等待条件准备好后执行
waiter.wait().then(() => console.log("Task 3 completed"));
// 在 2 秒后,调用 `ready()`,表示条件准备好,所有任务可以执行
setTimeout(() => {
waiter.ready(); // 调用 ready,触发所有等待的任务
}, 2000);
}
🌺06. 可暂停 / 恢复的 setInterval(轮询神器)
核心逻辑:可以启动、暂停和恢复一个定时任务,而无需重启整个定时器
业务场景:页面隐藏暂停轮询,返回恢复
- 自定义class
export class PausableInterval{
delay: number;
fn: any;
timer: any;
running: boolean;
constructor(fn: any, delay: number){
this.fn = fn // 定时任务函数
this.delay = delay // 定时器的间隔时间(单位:毫秒)
this.timer = null // 存储定时器的标识符
this.running = false // 标记定时器是否正在运行
}
start(){
// 如果定时器已经在运行,直接返回,不做重复启动
if(this.running) return;
this.running = true;
const tick = () => {
if (!this.running) return // 如果定时器已暂停,则不再继续执行
this.fn(); // 执行定时任务
this.timer = setTimeout(tick, this.delay) // 使用 setTimeout 模拟 setInterval
}
tick();
}
pause() {
clearTimeout(this.timer);
this.running = false;
}
resume(){
this.start()
}
}
2. 模拟请求
function printMessage() {
console.log("Task is running...");
}
export const pausableInterval = new PausableInterval(printMessage, 1000);
3. 调用
function handleStartInterval() {
pausableInterval.start();
// 停止定时器
setTimeout(() => {
console.log("Pausing the task...");
pausableInterval.pause();
}, 3000); // 3秒后暂停
// 恢复定时器
setTimeout(() => {
console.log("Resuming the task...");
pausableInterval.resume();
}, 5000); // 5秒后恢复
}
🌹07. 带最大等待 maxWait 的防抖(搜索框的神)
业务场景:用于优化那些频繁触发的事件,特别是在搜索框、输入框或滚动等高频率操作中,常常用来减少不必要的计算或请求
- 自定义方法
function debounce(fn, delay, { maxWait = 0 } = {}) {
let timer = null; // 存放定时器
let start = null; // 第一次调用时间
return function (...args) {
const now = Date.now(); // 获取当前时间戳
if (!start) start = now; // 记录第一次调用的时间
clearTimeout(timer); // 清除之前的定时器,避免多次触发
const run = () => {
start = null; // 重置 `start`,表示已经执行过操作
fn.apply(this, args); // 执行函数,并传入当前的 `this` 和参数
};
// 如果到达 `maxWait` 时间,强制执行 `fn`;否则继续延迟执行
if (maxWait && now - start >= maxWait) run();
else timer = setTimeout(run, delay); // 在 `delay` 时间后执行
};
}
2. 模拟短期内多次触发
function searchQuery(query) {
console.log("Searching for:", query);
}
const debouncedSearch = debounce(searchQuery, 500, { maxWait: 2000 });
// 模拟用户输入
debouncedSearch("apple");
debouncedSearch("app");
debouncedSearch("appl");
debouncedSearch("apple pie");