当您迭代用户/引擎定义的函数时,实现 for 循环“中断”功能模拟的最佳方法是什么?
foreach([0,1,2,3,4],function(n){
console.log(n);
if (n==2)
break;});
我曾想过以一种当函数返回“false”时会中断的方式实现 foreach - 但我想听听关于通常如何完成的想法。
return
ing false
是最常见的方法。这就是 jQuery 迭代器函数 .each()
的作用:
我们可以通过使 回调函数返回false。返回非 false 与 a 相同 for 循环中的 continue 语句;它会立即跳到下一个 迭代。
及其非常简化的实现:
each: function( object, callback ) {
var i = 0, length = object.length,
for ( var value = object[0];
i < length && callback.call( value, i, value ) !== false; // break if false is returned by the callback
value = object[++i] ) {}
return object;
}
由于 OP 明确要求 “在 [a]
break
[循环]”内模拟 [the] forEach
[行为]”,并且由于语言核心现在比 11.5 年前拥有更多功能,实际上可以很容易地实现一种原型数组方法,它不仅可以启用 break
,还可以启用 continue
命令,类似于 break
和 continue
这两个语句。
AbortController
及其相关的AbortSignal
的基本思想。
因此,人们可以实施例如一个
PausedStateSignal
...
class PausedStateSignal extends EventTarget {
// shared protected/private state.
#state;
constructor(connect) {
super();
this.#state = {
isPaused: false,
};
connect(this, this.#state);
}
get paused() {
return this.#state.isPaused;
}
}
...将由其
BreakAndContinueController
... 使用
class BreakAndContinueController {
#signal;
#signalState;
constructor() {
new PausedStateSignal((signal, signalState) => {
this.#signal = signal;
this.#signalState = signalState;
});
this.#signalState.isPaused = false;
}
get signal() {
return this.#signal;
}
break() {
const isPaused = this.#signalState.isPaused;
if (!isPaused) {
this.#signalState.isPaused = true;
}
this.#signal.dispatchEvent(
new CustomEvent('break', { detail: { pausedBefore: isPaused } })
);
return !isPaused;
}
continue() {
const isPaused = this.#signalState.isPaused;
if (isPaused) {
this.#signalState.isPaused = false;
}
this.#signal.dispatchEvent(
new CustomEvent('continue', { detail: { pausedBefore: isPaused } })
);
return isPaused;
}
}
...其中
PausedStateSignal
必须扩展 EventTarget
才能通过 dispatchEvent
发出状态变化信号,并且 BreakAndContinueController
具有两个主要方法 break
和 continue
。
get
语法以及私有、受保护的 state
对象,该对象通过在控制器和信号实例之间的引用共享。后者通过在信号实例化时传递的 connect
ing 回调函数来实现。
介绍完该部分后,我们可以继续实际实现数组方法,除了标准
forEach
功能之外,该方法还能够完成三件事......
break
,暂停/停止回调函数的执行
continue
允许...
该实现可以命名为,例如
forEachAsyncBreakAndContinue
,利用上述信号和控制器抽象,可能如下所示...
function forEachAsyncBreakAndContinue(callback, context = null) {
const { promise, reject, resolve } = Promise.withResolvers();
const controller = new BreakAndContinueController;
const { signal } = controller;
const arr = this;
const { length } = arr;
let idx = -1;
function continueLooping() {
while(++idx < length) {
if (signal.paused) {
--idx;
break;
}
try {
callback.call(context, arr.at(idx), idx, arr, controller);
} catch (exception) {
reject(exception.message ?? String(exception));
}
}
if (idx >= length) {
resolve({ success: true });
}
}
signal.addEventListener('continue', ({ detail: { pausedBefore } }) => {
if (pausedBefore) {
// - continue after already having
// encountered a break-command before.
continueLooping();
} else {
// - continue-command while already running which
// is equal to skipping the next occurring cycle.
++idx;
}
});
continueLooping();
return promise;
}
Reflect.defineProperty
分配用于演示目的,如 forEachAsyncBC
到 Array.prototype
...
Reflect.defineProperty(Array.prototype, 'forEachAsyncBC', {
value: forEachAsyncBreakAndContinue,
});
现在的原型
forEachAsyncBC
方法总是会返回一个承诺。这个承诺要么拒绝,要么解决;前者是指提供的回调函数在调用时确实会引发错误,后者是指迭代周期已完全完成的情况。
感谢所有抽象,可以像这样简单地编写测试所有提到的功能的可执行示例代码......
(async () => {
const result = await [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
.forEachAsyncBC((value, idx, arr, controller) => {
console.log({ value, idx });
if (value === 9 || value === 3) {
console.log(`... skip over next value => ${ arr[idx + 1] } ...`);
// skip over.
controller.continue();
} else if (value === 4 || value === 6) {
console.log(`... break at value ${ value } ... continue after 5 seconds ...`);
setTimeout(controller.continue.bind(controller), 5000);
// break loop.
controller.break();
}
});
console.log({ result });
})();
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
class PausedStateSignal extends EventTarget {
// shared protected/private state.
#state;
constructor(connect) {
super();
this.#state = {
isPaused: false,
};
connect(this, this.#state);
}
get paused() {
return this.#state.isPaused;
}
}
class BreakAndContinueController {
#signal;
#signalState;
constructor() {
new PausedStateSignal((signal, signalState) => {
this.#signal = signal;
this.#signalState = signalState;
});
this.#signalState.isPaused = false;
}
get signal() {
return this.#signal;
}
break() {
const isPaused = this.#signalState.isPaused;
if (!isPaused) {
this.#signalState.isPaused = true;
}
this.#signal.dispatchEvent(
new CustomEvent('break', { detail: { pausedBefore: isPaused } })
);
return !isPaused;
}
continue() {
const isPaused = this.#signalState.isPaused;
if (isPaused) {
this.#signalState.isPaused = false;
}
this.#signal.dispatchEvent(
new CustomEvent('continue', { detail: { pausedBefore: isPaused } })
);
return isPaused;
}
}
// - asynchronously implemented `forEach` array method which
// provides a `BreakAndContinueController` instance as 4th
// parameter to its callback function, where the latter's
// two methods `break` and `continue` enable the following ...
//
// - pause a `forEach` loop by invoking `break`.
// - by invoking `continue` ...
// - either continuing a paused `forEach` loop.
// - or skip the `forEach` loop's next iteration step.
//
function forEachAsyncBreakAndContinue(callback, context = null) {
const { promise, reject, resolve } = Promise.withResolvers();
const controller = new BreakAndContinueController;
const { signal } = controller;
const arr = this;
const { length } = arr;
let idx = -1;
function continueLooping() {
while(++idx < length) {
if (signal.paused) {
--idx;
break;
}
try {
callback.call(context, arr.at(idx), idx, arr, controller);
} catch (exception) {
reject(exception.message ?? String(exception));
}
}
if (idx >= length) {
resolve({ success: true });
}
}
signal.addEventListener('continue', ({ detail: { pausedBefore } }) => {
if (pausedBefore) {
// - continue after already having
// encountered a break-command before.
continueLooping();
} else {
// - continue-command while already running which
// is equal to skipping the next occurring cycle.
++idx;
}
});
continueLooping();
return promise;
}
Reflect.defineProperty(Array.prototype, 'forEachAsyncBC', {
value: forEachAsyncBreakAndContinue,
});
</script>