Sushi JavaScript
碎碎念
手撕题经常在面筋中出现,寿司|面筋还蛮搭配,虽然说现在面试造火箭的行为还是很多,不过写一些 Javascript 的手撕题还是蛮有帮助的:
- 它能让你更熟悉 Javascript
- 面试
- 产生疑惑,比如:为什么存在这种方法,它是为了解决什么问题
- 更全面的考虑问题,需要考虑一些特殊情况边界情况
这儿就放一些手撕题吧,希望能多多更新多多回顾,多多深究"Why"的问题而不是"How"的问题。
call/apply/bind
这三个函数都是用来改变 this
指向性的,故而在我们知道怎么实现这三个函数之前需要了解 this
指向性的问题。
Call
call 接收一个对象和参数列表,调用它的则是一个需要把 this 指向到 thisArg 的函数。需要注意的是 call 会立即调用一次
fn.call(thisArg, arg1, arg2, ...)
所以我们需要做的就是:
- 获取需要执行的函数(就是 this,因为 call 的调用都是 fn.call(),fn 是一个函数)
- 对 thisArg 进行转换成对象类型(防止传入的是一个非对象类型),这个 thisArg 就是需要绑定的 this
- 调用需要被执行的函数(通过给 thisArg 增加方法属性)
- 删除添加到对象上的函数
- 返回函数的执行结果
// 在 Function 原型上设置 myCall 方法,这样所有函数都可以调用该方法
Function.prototype.myCall = function (thisArg, ...args) {
// 这里的 this 指向调用 myCall 方法的函数,将其赋值给变量 fn 以便后续使用
let fn = this;
// 对传入的 thisArg 进行处理
// 如果 thisArg 为 null 或 undefined,将其设置为全局对象 window(在浏览器环境中)
// 否则,使用 Object() 函数将其转换为对象,确保 thisArg 是一个对象
thisArg =
thisArg !== null && thisArg !== undefined ? Object(thisArg) : window;
// 将调用 myCall 的函数添加到 thisArg 对象上,属性名为 fn
// 这样在调用 thisArg.fn 时,函数内部的 this 就会指向 thisArg
thisArg.fn = fn;
// 调用添加到 thisArg 上的函数,并将参数 args 传递给它
// 使用扩展运算符 ... 将参数展开
const result = thisArg.fn(...args);
// 调用完函数后,删除之前添加到 thisArg 上的临时属性 fn
// 避免污染 thisArg 对象
delete thisArg.fn;
// 返回函数的执行结果
return result;
};
// 测试代码
function greet(message) {
console.log(`${message}, ${this.name}`);
}
const person = { name: "Alice" };
greet.myCall(person, "Hello"); // 输出: Hello, Alice
我们还可以使用 Symbol 来使得这个函数更简洁一些:
Function.prototype.customCall = function (context = window, ...args) {
// 这里的 this 指向调用 customCall 的函数
const fnSymbol = Symbol("fn");
context[fnSymbol] = this;
const result = context[fnSymbol](...args);
delete context[fnSymbol];
return result;
};
// 测试代码
function greet(message) {
console.log(`${message}, ${this.name}`);
}
const person = { name: "Alice" };
greet.customCall(person, "Hello");
apply
apply 和 call 功能完全一致,唯一的区别就是 call 接收的是参数列表,而 apply 接受的是含有参数的一个数组
所以相较于 call 的操作,我们只需要多做一步操作:对传入的参数数组进行处理即可:
Function.prototype.customApply = function (context = window, args = []) {
// 这里的 this 指向调用 customApply 的函数
const fnSymbol = Symbol("fn");
context[fnSymbol] = this;
let result;
if (args.length === 0) {
result = context[fnSymbol]();
} else {
result = context[fnSymbol](...args);
}
delete context[fnSymbol];
return result;
};
// 测试代码
function greet(message) {
console.log(`${message}, ${this.name}`);
}
const person = { name: "Bob" };
greet.customApply(person, ["Hi"]);
为什么要进行条件判断
有些函数可能会根据传入参数的数量来执行不同的逻辑,也就是函数重载。如果直接使用扩展运算符展开一个空数组,函数可能会接收到一个空参数列表,这可能会导致函数执行不符合预期的逻辑。
function exampleFunction() {
if (arguments.length === 0) {
console.log("No arguments were passed.");
} else {
console.log("Arguments were passed.");
}
}
// 直接展开空数组
exampleFunction(...[]); // 输出 'Arguments were passed.',不符合预期
// 使用判断
if (arguments.length === 0) {
exampleFunction(); // 输出 'No arguments were passed.',符合预期
} else {
exampleFunction(...arguments);
}
例如上面的例子,不过这是一种特殊情况了。
bind
fn.bind(thisArg,arg1,arg2...)
bind 方法会创建一个新的函数
,在调用时 this 值会被绑定到指定的对象上,并且可以预设
参数
何时 bind
- 固定函数的 this 指向,防止丢失(例如在 setTimeOut 中调用某个对象方法)
- 预设函数参数,从而创建一个新的函数。
Function.prototype.customBind = function (context, ...args) {
// 这里的 this 指向调用 customBind 的函数
const self = this;
return function (...newArgs) {
// 合并预设参数和新传入的参数
const allArgs = [...args, ...newArgs];
// 使用 call 方法调用原函数,并绑定 this 值
return self.call(context, ...allArgs);
};
};
// 测试代码
function greet(message) {
console.log(`${message}, ${this.name}`);
}
const person = { name: "Charlie" };
const greetPerson = greet.customBind(person, "Hey");
greetPerson();
Curry(柯里化)
Why Curry?
函数柯里化是把一个多参数函数转换为一系列单参数函数的技术,不受语言限制。为什么要用函数柯里化:
- 参数复用
- 允许你分步传入参数,在需要结果时再进行计算
- 柯里化后的函数可以根据不同的需求灵活组合参数,并且代码结构更加清晰,便于维护
给出两个代码例子:
function multiply(a, b) {
return a * b;
}
const double = curry(multiply)(2);
console.log(double(5));
console.log(double(10));
const curriedAdd = curry(add);
const step1 = curriedAdd(1);
const step2 = step1(2);
console.log(step2(3));
function curry(func) {
// 返回一个名为 curried 的函数,该函数用于处理参数并决定是否调用原函数
return function curried(...args) {
// 检查当前传入的参数数量是否大于或等于原函数所需的参数数量
if (args.length >= func.length) {
// 如果满足条件,则使用 apply 方法调用原函数,并将当前的 this 值和参数传递给它
// 这里的 this 会保持调用时的上下文
return func.apply(this, args);
} else {
// 如果传入的参数数量不足,返回一个新的函数
return function (...newArgs) {
// 这个新函数会将之前传入的参数和新传入的参数合并
// 然后递归调用 curried 函数,继续处理参数
return curried.apply(this, [...args, ...newArgs]);
};
}
};
}
// 测试代码
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3));
console.log(curriedAdd(1, 2)(3));
console.log(curriedAdd(1)(2, 3));
防抖与节流
防抖(Debounce)
防抖是指在一定时间内,只有最后一次触发事件才会执行相应的处理函数。如果在这个时间内又触发了事件,那么计时将重新开始。
- 减少不必要的计算和请求:在一些高频触发的场景中,如搜索框输入提示、窗口大小改变等,如果每次触发事件都执行相应的处理函数,会导致大量的计算和请求,影响性能。使用防抖可以确保只有在用户停止操作一段时间后才执行处理函数,减少不必要的计算和请求。
- 提高用户体验:避免因为用户的频繁操作而导致页面卡顿或出现异常,提高用户体验。
所以我们要做的就是:
- 定义一个函数,这个函数会传入一个会被执行的方法,和防抖的时长
- 返回一个函数,这个函数就是处理好的防抖函数,可以传入被执行的函数所需要的参数
- 这边可以通过闭包的技术,在外部先定义一个 timer,然后执行防抖函数时先判断 timer 函数存在不存在,存在就清除定时器。
- 然后创建一个定时器,并赋值给 timer 变量,这是为了避免 this 指向被改变,所以我们需要使用 apply 来制定 this
function debounce(func, delay) {
let timer = null;
return function (...args) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// 测试
function search() {
console.log("执行搜索操作");
}
const debouncedSearch = debounce(search, 300);
// 模拟多次触发搜索事件
for (let i = 0; i < 5; i++) {
debouncedSearch();
}
立即执行一次防抖
我们可以传入一个 Boolean 参数来表示是否开启立即执行:
function debounce(func, delay, immediate = false) {
let timer = null;
return function (...args) {
const callNow = immediate && !timer;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
timer = null;
if (!immediate) {
func.apply(this, args);
}
}, delay);
if (callNow) {
func.apply(this, args);
}
};
}
// 省略测试函数
节流(Throttle)
节流是指在一定时间内,只执行一次处理函数。如果在这个时间内多次触发事件,只有第一次会执行处理函数,后续的触发会被忽略,直到时间间隔结束后,再次触发事件才会执行处理函数。
- 控制函数执行频率:在一些需要频繁触发的场景中,如滚动加载、按钮点击等,如果不加以控制,会导致函数执行过于频繁,影响性能。使用节流可以限制函数的执行频率,确保在一定时间内只执行一次处理函数,提高性能。
- 保护服务器:在一些需要向服务器发送请求的场景中,如滚动加载数据,如果不加以控制,会导致服务器压力过大。使用节流可以限制请求的频率,保护服务器。
function throttle(func, delay) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= delay) {
func.apply(this, args);
lastTime = now;
}
};
}
// 测试节流函数
function scrollHandler() {
console.log("处理滚动事件");
}
const throttledScroll = throttle(scrollHandler, 500);
// 模拟多次触发滚动事件
window.addEventListener("scroll", throttledScroll);
深拷贝
深拷贝是创建一个新对象,新对象的属性值和原对象相同,但它们在内存中是独立的,修改新对象不会影响原对象。
浅拷贝创建一个新对象,但只复制原始对象第一层属性
的值。如果属性是基本类型,则复制其值;如果属性是引用类型,则复制其引用(地址)。
实现浅拷贝
- 遍历属性
- Object.assign()
- Spread Operator (
...
)
浅拷贝陷阱
对于嵌套对象或数组,修改副本中的嵌套结构会影响原始对象。这是因为嵌套对象的引用在原始对象和副本之间是共享的。
实现深拷贝
深拷贝创建一个新对象,并递归地复制原始对象的所有嵌套对象,确保副本与原始对象完全独立。
JSON 序列化/反序列化
最简单(但有局限)的深拷贝方法,这是通过将对象转换成字符串,然后再转换成对象实现的深拷贝:
const original = { name: "John", details: { age: 30 } };
const deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.details.age = 25;
console.log(original.details.age); // 输出: 30,原对象不受影响
但我们有必要说明它的缺陷,这也是我们提出更好的方法时需要考虑的问题:
WARNING
- 无法处理函数和正则表达式
JSON.stringify()
会忽略对象中的函数和正则表达式属性
,因为 JSON 格式不支持这两种数据类型。
- 无法处理
Symbol
类型的属性JSON.stringify()
会忽略对象中键为Symbol
类型的属性。
- 无法处理循环引用
- 如果对象存在循环引用,
JSON.stringify()
会抛出错误。
- 如果对象存在循环引用,
- 丢失
Date
对象的时间信息JSON.stringify()
会把Date
对象转换为字符串,在使用JSON.parse()
进行反序列化时,这些字符串不会再转换回Date
对象,而是以字符串形式存在。
- 丢失对象的原型链
- 使用
JSON.stringify()
和JSON.parse()
进行深拷贝时,会丢失对象的原型链信息。 :::
- 使用
递归
- 首先需要判断传入的对象是否是一个对象类型,如果不是对象类型直接返回即可。
- 如果是对象类型,那么就需要去递归调用 deepCopy 函数。
- 如果他的 key 是一个 Symbol, 就需要 Object.getOwnPropertySymbols 获取到 symbol 的 key,然后去递归调用。
- 然后对传入的数据进行一系列的判断,进行对应的处理。
- 如果是循环引用,就需要用到他的第二个参数 map,初始化是一个 WeakMap 数据,我们每次遍历的时候都会在 map 中将当前这个对象作为 WeakMap 的 key 值存起来,如果发现有一样的存在,那就说明存在递归调用,直接 return 对应的值。
// 判断传入的值是否为对象或函数
function isObject(value) {
return (
value !== null && (typeof value === "object" || typeof value === "function")
);
}
// 实现深拷贝的函数
function deepCopy(value, map = new WeakMap()) {
// 若不是对象或函数,直接返回该值
if (!isObject(value)) return value;
// 处理循环引用,若已存在于map中则直接返回对应拷贝对象
if (map.has(value)) return map.get(value);
let clone;
// 处理Set类型
if (value instanceof Set) {
clone = new Set([...value]);
}
// 处理Map类型
else if (value instanceof Map) {
clone = new Map([...value]);
}
// 处理Symbol类型
else if (typeof value === "symbol") {
clone = Symbol(value.description);
}
// 处理函数类型
else if (typeof value === "function") {
clone = value;
}
// 处理数组和普通对象
else {
clone = Array.isArray(value) ? [] : {};
}
// 记录已拷贝对象,避免循环引用
map.set(value, clone);
// 遍历可枚举属性
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
clone[key] = deepCopy(value[key], map);
}
}
// 处理以Symbol为键的属性
const symbolKeys = Object.getOwnPropertySymbols(value);
for (const symbolKey of symbolKeys) {
clone[symbolKey] = deepCopy(value[symbolKey], map);
}
return clone;
}
结构化克隆算法
structuredClone() 是一个相对较新的全局方法,它实现了结构化克隆算法,可以创建深层次的副本.
优缺点
优点
- 原生 API,无需依赖外部库
- 可以处理大多数 JavaScript 内置类型
- 支持循环引用
- 性能通常较好
缺点
- 不能克隆函数
- 不能克隆 DOM 节点
- 不会保留对象的原型链
// 定义一个包含复杂数据结构的对象
const original = {
num: 123,
str: "hello",
arr: [1, 2, 3],
obj: { key: "value" },
date: new Date(),
reg: /abc/g,
};
// 使用 structuredClone 方法进行深拷贝
const cloned = structuredClone(original);
// 修改克隆对象的属性
cloned.arr.push(4);
cloned.obj.newKey = "newValue";
// 验证原对象未受影响
console.log(original.arr); // 输出: [1, 2, 3]
console.log(original.obj); // 输出: { key: 'value' }
实际上
在实际场景中,我们常常做的是混合克隆:浅克隆不能保证源对象不会被更改;深克隆常常会克隆出我们不需要的深层数据。啊有时间写一篇关于克隆的应用吧
Promise
Promise 是 JavaScript 中用于处理异步操作的一种对象,它是异步编程的一种解决方案,能避免回调地狱,让异步代码的编写和维护更加容易。
Why Promise
什么是回调
回调是一种常见的编程机制,它允许函数将另一个函数作为参数传递,并在特定的事件发生或特定的条件满足时调用这个传入的函数。
回调函数就是作为参数传递给其他函数的函数,这个被传递的函数会在外部函数的特定位置被调用,以实现某种特定的逻辑。
回调的作用有二:
解耦代码:通过回调,我们可以将不同的功能模块解耦。比如,一个数据获取模块和一个数据处理模块,数据获取模块获取到数据后,通过回调通知数据处理模块来处理数据,这样两个模块可以独立开发和维护,提高了代码的可维护性和可扩展性。
实现异步操作:在异步编程中,回调函数非常重要。例如,在进行网络请求、文件读取等耗时操作时,程序不会阻塞等待操作完成,而是继续执行后续代码。当这些异步操作完成后,通过回调函数来通知程序执行相应的处理逻辑。
我们知道 JavaScript 是单线程的,一次只能执行一个任务,会阻塞其他任务。因此,所有的网络任务、游览器事件等都是异步的,我们可以使用异步回调函数来进行异步操作。由于回调很多,函数作为参数层层嵌套,就陷入了回调地狱。这种情况下,就像是金字塔一样的代码非常不利于阅读。
Promise 对象的主要⽤途是通过链式调⽤的结构,将原本回调嵌套的异步处理流程,转化成“对象.then().then()...”
的链式结构,这样虽然仍离不开回调函数,但是将原本的回调嵌套结构,转化成了连续调⽤的结构,这样就可以在阅读上编程上下左右结构的异步执⾏流程了。
剖析 Promise
Promise 有 3 个状态:
pending
待定,初始状态fulfilled
兑现,已完成,通常代表成功执行了某一任务。初始化函数中的 resolve()执行时,状态就会变味 fulfilled,而且.then 函数注册的回调会开始执行,resolve 中传递的参数会进入回调函数成为形参。rejected
拒绝,通常代表执行一次任务失败,调用 reject()时,catch 注册的函数就会触发,并且 reject 中传递的内容会变成回调函数的形参。
三种状态之间的关系:
当对象创建之后同⼀个 Promise 对象只能从 pending 状态变更为 fulfilled 或 rejected 中的其中⼀种,并且状态⼀旦变更就不会再改变,此时 Promise 对象的流程执⾏完成并且 finally 函数执⾏。
catch/then/finally
new Promise(function (resolve, reject) {
resolve();
reject();
})
.then(function () {
console.log("then执⾏");
})
.catch(function () {
console.log("catch执⾏");
})
.finally(function () {
console.log("finally执⾏");
});
执行后依次打印 then执行 -> finally执行
,发现.catch 的回调没有执行。
再看如下代码:
new Promise(function (resolve, reject) {
reject();
resolve();
})
.then(function () {
console.log("then执⾏");
})
.catch(function () {
console.log("catch执⾏");
})
.finally(function () {
console.log("finally执⾏");
});
这个串代码和之前的代码唯一的不同 在于 Promise 中的回调先执行了resolve()
还是先执行了reject()
,打印结果是catch执行 -> finally执行
,发现.then
的回调没有执行。
注意:
Promise.prototype.catch()
其实是一个语法糖,相当于是调用 Promise.prototype.then(null, onRejected)
。.then
中其实是可以传入 2 个回调函数,第一个回调函数是resolve()
后执行,第二个回调函数是reject()
后执行,2 个是互斥的。
这是因为 Promise 的异步回调部分如何执行,取决于我们在初始化函数中的操作,并且初始化函数中⼀旦调⽤了 resolve 后⾯再执⾏ reject 也不会影响 then 执⾏,catch 也不会执⾏,反之同理。 ⽽在初始化回调函数中,如果不执⾏任何操作,那么 promise 的状态就仍然是 pending,所有注册的回调函数都不会执⾏。
是不是可以把 resolve 或者 reject 的调用设定在异步函数内去调用,这样是不是就能解决回调地狱的问题了?
new Promise(function (resolve, reject) {
setTimeout(() => {
console.log(111);
resolve();
}, 2000);
})
.then(function () {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log(222);
resolve();
}, 2000);
});
})
.then(function () {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log(333);
resolve();
}, 2000);
});
})
.catch(function () {
console.log("catch执⾏");
})
.finally(function () {
console.log("finally执⾏");
});
上面代码每隔 2s 依次打印111 -> 222 -> 333 finally执行
。333 执行后立马执行 finally。
中断链式调用的方式
中断的⽅式可以使⽤抛出⼀个异常或返回⼀个 rejected 状态的 Promise 对象
链式调用的基本形式
只要有 then()并且触发了 resolve,整个链条就会执⾏到结尾,这个过程中的第⼀个回调函数的参数是 resolve 传⼊的值
后续每个函数都可以使⽤ return 返回⼀个结果,如果没有返回结果的话下⼀个 then 中回调函数的参数就是 undefined
返回结果如果是普通变量,那么这个值就是下⼀个 then 中回调函数的参数
如果返回的是⼀个 Promise 对象,那么这个 Promise 对象 resolve 的结果会变成下⼀次 then 中回调的函数的参数
如果 then 中传⼊的不是函数或者未传值,Promise 链条并不会中断 then 的链式调⽤,并且在这之前最后⼀次的返回结果,会直接进⼊离它最近的正确的 then 中的回调函数作为参数
// tip 5
const p2 = new Promise((resolve, reject) => {
console.log(1);
resolve();
});
p2.then(() => {
console.log(2);
return 123;
})
.then()
.then("456")
.then((res) => {
console.log(res);
});
// 1
// 2
// 123
resolve 和 reject
至于 resolve 和 reject,我们通过上面已经知道了 resolve 和 reject 能够更改 Promise 的状态,而 Promise 的状态是不可逆的,且是私有的。所以我们必须在 Promise 内部调用 resolve 或者 reject。
当然,resolve 和 reject 也能够传入参数,而传入的参数,会变为.then 或.catch 的回调函数中的参数。
Promise 常用 API
all()
假如我们有一个需求,一个页面需要请求 3 个接口才能渲染,并且要求 3 个接口必须全部返回。如果我们通过链式调用的方式,接口 1 请求了再去请求接口 2 然后去请求接口 3,全都成功了再去渲染页面。这种就很耗时,所以就有了一个 all 的方法来解决。
Promise.all([promise对象,promise对象,...]).then(回调函数)
Promise.all()的参数是一个 Promise 数组,只有数组中所有的 Promise 的状态变成了 fulfilled 之后才会执行.then 回调的第一个回调函数,并且将每个 Promise 结果的数组变为回调函数的参数。如果 Promise 中有一个 rejected,那么就会触发.catch()的回调。
race()
race()方法与 all()方法的使用格式相同,不同的是,回调函数的参数是 promise 数组中最快执行完毕的 promise 的返回值,它的状态可能是 fulfilled 也有可能是 rejected,但是是最快返回的。 根据 race 这个单词就能理解,相当于一群 promise 进行比赛
allSettled()
该方法需要传入所有不在 pendding 状态的 promise 数组,然后通过该方法可以知道数组中的 promise 的当前状态。
当有多个彼此不依赖的异步任务成功完成时,或者总是想知道每个 promise 的结果时,通常使用它。
any()
接受一个 promise 数组,只要有一个 promise 的状态变成了 fulfilled,那么这个方法就会返回这个 promise;
如果所有的 promise 的状态都是 rejected,那么就返回失败的 promise,并且把单一的错误集合在一起。
Sushi Promise
手写一个 Promise 已经是一个常见的手写功能了,虽然实际工作上可能并不会用到。但是在面试时还是会经常被提起的。
总体实现
首先我们可以按照 Promise 的特点来实现一个简单的总体结构。
- 它是一个构造函数,每个创建的 promise 都有各自状态和值,且初始状态为 pending,初始值为 undefined。
- 创建实例的时候需要传入一个函数,而这个函数可以接受 2 个函数作为参数,这 2 个函数都有一个参数,且都可以更改实例的状态和值。
- 根据 Promise 的结构,我们可以发现 then()、catch()方法在它的原型上。
function MyPromise(fn) {
this.PromiseState = "pending";
this.PromiseResult = undefined;
function resolve(data) {}
function reject(error) {}
}
MyPromise.prototype.then = function (thenCallback) {};
MyPromise.prototype.catch = function (catchCallback) {};
上述代码很简单,就定义了一个 Promise 的构造函数,有 2 个属性(PromiseState、PromiseResult)和 2 个方法(resolve、reject)。
原型上增加了 then 方法和 catch 方法,且这 2 个方法都接收一个函数。
而每次通过 new 来创建 Promise 的函数时,传入的函数会执行,因此我们直接调用 fn 函数,并传入 resolve 和 reject 这 2 个函数。
初步实现 resolve/reject
resolve 的调用会更改 promise 的状态和值,且状态是不可逆的。也就是说,只能从 pending 变成 fulfilled 或者 rejected。而 resolve 函数的参数值会变成 promise 实例的值,reject 同理。
function MyPromise(fn) {
this.PromiseState = "pending";
this.PromiseResult = undefined;
// 保存实例对象的this的值
const self = this;
function resolve(data) {
// 如果不使用self,这里内部的this会指向window
// 如果当前的promise实例不是pending的状态就退出了,否则就更改当前的promise实例的状态和值
if (self.PromiseState !== "pending") {
return;
}
// 1.修改对象的状态([[promiseState]])
// 2.设置对象结果值([[promiseResult]])
self.PromiseState = "fulfilled";
self.PromiseResult = data;
}
function reject(error) {
if (self.PromiseState !== "pending") {
return;
}
self.PromiseState = "rejected";
self.PromiseResult = error;
}
fn(resolve, reject);
}
then 的实现
then 方法中我们知道它有 2 个参数,且这 2 个参数都是函数,第一个参数会在 promise 执行 resolve 时调用,第二个参数会在 promise 执行 reject 时调用。而.catch 只不过是调用.then 第二个参数的语法糖而已。
.then 的执行会返回一个新的 Promise 实例。而决定调用.then 的第几个参数,则是根据调用 then 的那个 promise 实例的状态决定。
MyPromise.prototype.then = function (thenCallback, catchCallback) {
return new Promise((resolve, reject) => {
// 调用回调函数,要根据当前的promise实例来调用
if (this.PromiseState === "fulfilled") {
const result = thenCallback(this.PromiseResult);
resolve(result);
}
if (this.PromiseState === "rejected") {
const result = catchCallback(this.PromiseResult);
resolve(result);
}
});
};
异步控制并发数
前端异步控制请求并发数是一种优化技术,它能限制在同一时间发起的异步请求数量,避免因过多请求同时进行而对服务器和网络造成过大压力,防止请求超时或失败,保证页面性能和流畅度,适用于批量文件上传、图片懒加载、数据批量拉取等多种场景。
为了实现它,我们的设想是:
- 首先我们定义一个函数,这个函数会有 2 个参数,分别是请求的数组,以及最大并发数
- 这个函数会返回一个 Promise
- 我们会定义一个 len,len 就是请求 url 数组的长度;还有 1 个 count 变量,初始值为 0;如果后续 len 的长度和 count 的值一样就表示请求完了
- 然后我们需要定义一个 start 函数,用来去执行请求。
- 这个函数会从数组中拿出第一个 url 去请求,然后无论成功失败,都会进行一次判断 count 是不是和长度-1 相同,因为 count 是从 0 开始加的,如果相同就表示数组中的请求都请求完了,然后 promise 变成 fullfiled 的状态,否则 count 自增,继续执行 start 去取 url 请求。
- 这时我们需要使用 while 启动 limit 数量的任务,每个任务内部会自调用 start 函数
function limitRequest(urls = [], limit = 3) {
return new Promise((resolve, reject) => {
const len = urls.length;
let count = 0;
// 同时启动limit个任务
while (limit > 0) {
start();
limit -= 1;
}
function start() {
const url = urls.shift(); // 从数组中拿取第一个任务
if (url) {
axios
.post(url)
.then((res) => {
// todo
})
.catch((err) => {
// todo
})
.finally(() => {
if (count == len - 1) {
// 最后一个任务完成
resolve();
} else {
// 完成之后,启动下一个任务
count++;
start();
}
});
}
}
});
}
AI写的:
function asyncControl(tasks, concurrency) {
let index = 0;
let running = 0;
const results = [];
return new Promise((resolve, reject) => {
function runNext() {
// 当所有任务都完成且没有正在运行的任务时,返回结果
if (index === tasks.length && running === 0) {
resolve(results);
return;
}
// 在并发数限制内,持续取出任务执行
while (running < concurrency && index < tasks.length) {
const currentIndex = index++;
running++;
const task = tasks[currentIndex];
task()
.then((result) => {
results[currentIndex] = result;
})
.catch((error) => {
results[currentIndex] = error;
})
.finally(() => {
running--;
runNext();
});
}
}
runNext();
});
}
// 示例使用
// 模拟异步请求
function createAsyncTask(id) {
return () =>
new Promise((resolve) => {
setTimeout(() => {
console.log(`Task ${id} completed`);
resolve(id);
}, Math.random() * 1000);
});
}
const tasks = Array.from({ length: 10 }, (_, i) => createAsyncTask(i + 1));
const concurrency = 3;
asyncControl(tasks, concurrency).then((results) => {
console.log('All tasks completed:', results);
});