MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript闭包的并发实现

2021-11-151.9k 阅读

什么是闭包

在JavaScript中,闭包是一种特殊的函数作用域现象。简单来说,当一个函数内部定义了另一个函数,并且内部函数可以访问外部函数的变量时,就形成了闭包。这使得内部函数即使在外部函数执行完毕后,依然能够访问和操作外部函数作用域中的变量。

例如:

function outer() {
    let outerVar = 10;
    function inner() {
        console.log(outerVar);
    }
    return inner;
}

let closureFunction = outer();
closureFunction(); 

在上述代码中,outer函数返回了inner函数。inner函数形成了闭包,因为它可以访问outer函数作用域中的outerVar变量。即使outer函数已经执行完毕,closureFunction仍然能够访问并打印outerVar的值。

闭包的本质在于函数作用域链的特性。当函数执行时,会创建一个执行上下文,其中包含了函数的作用域链。作用域链是一个对象列表,它从当前函数的活动对象开始,一直延伸到全局对象。在闭包的情况下,内部函数的作用域链不仅包含自身的活动对象,还包含了外部函数的活动对象,这就使得内部函数能够访问外部函数的变量。

并发编程基础

在深入探讨JavaScript闭包的并发实现之前,我们先来了解一下并发编程的基本概念。

并发是指在同一时间段内执行多个任务,这些任务不一定是同时执行(同时执行通常指在多核CPU上的并行执行),而是通过在不同任务之间快速切换,给人一种同时执行的错觉。在JavaScript中,由于其单线程的特性,并发主要通过事件循环(Event Loop)机制来实现。

事件循环

JavaScript的事件循环是其实现并发的核心机制。事件循环会不断地检查调用栈(Call Stack)是否为空,如果为空,就从任务队列(Task Queue)中取出一个任务放入调用栈执行。任务队列中存放的是各种异步操作(如定时器、网络请求等)完成后产生的回调函数。

例如,当我们使用setTimeout函数时:

console.log('start');
setTimeout(() => {
    console.log('timeout');
}, 1000);
console.log('end');

上述代码中,console.log('start')首先被放入调用栈执行并打印。然后setTimeout函数被调用,它并不会立即执行回调函数,而是将这个回调函数放入任务队列中。接着console.log('end')被放入调用栈执行并打印。当调用栈为空时,事件循环会从任务队列中取出setTimeout的回调函数放入调用栈执行,从而打印timeout

异步操作

JavaScript中的异步操作主要通过回调函数、Promise和async/await来实现。

回调函数 回调函数是最基本的异步处理方式。例如在处理文件读取操作时:

const fs = require('fs');
fs.readFile('example.txt', 'utf8', (err, data) => {
    if (err) {
        console.error(err);
    } else {
        console.log(data);
    }
});

这里的fs.readFile是一个异步操作,它接受一个回调函数作为参数。当文件读取完成后,会调用这个回调函数并传入读取的结果或错误信息。

Promise Promise是一种更优雅的异步处理方式,它解决了回调地狱(多个回调函数嵌套导致代码难以阅读和维护)的问题。

const fs = require('fs');
const util = require('util');

const readFilePromise = util.promisify(fs.readFile);

readFilePromise('example.txt', 'utf8')
   .then(data => {
        console.log(data);
    })
   .catch(err => {
        console.error(err);
    });

util.promisify将基于回调的fs.readFile函数转换为返回Promise的函数。通过.then方法可以处理成功的结果,通过.catch方法可以处理错误。

async/await async/await是基于Promise的语法糖,使得异步代码看起来更像同步代码。

const fs = require('fs');
const util = require('util');

const readFilePromise = util.promisify(fs.readFile);

async function readFileAsync() {
    try {
        const data = await readFilePromise('example.txt', 'utf8');
        console.log(data);
    } catch (err) {
        console.error(err);
    }
}

readFileAsync();

async函数内部,await关键字只能用于Promise对象,它会暂停函数的执行,直到Promise被解决(resolved或rejected)。

闭包与并发的联系

闭包在JavaScript的并发编程中有着重要的作用。由于闭包可以保持对外部函数变量的引用,这使得我们可以在异步操作中利用闭包来维护状态。

例如,考虑一个需要多次执行异步操作并记录每次操作结果的场景:

function asyncOperationFactory() {
    let results = [];
    return function asyncOperation(value) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                results.push(value);
                console.log(`Operation with value ${value} completed. Results so far:`, results);
                resolve(results);
            }, 1000);
        });
    };
}

let asyncOperation = asyncOperationFactory();
asyncOperation(1).then(results => console.log('Final results:', results));
asyncOperation(2).then(results => console.log('Final results:', results));

在上述代码中,asyncOperationFactory返回了一个闭包函数asyncOperation。这个闭包函数可以访问并修改asyncOperationFactory作用域中的results数组。每次调用asyncOperation时,都会在异步操作(setTimeout模拟)完成后将新的值添加到results数组中,并且能够持续维护这个状态。

利用闭包实现并发控制

在实际应用中,我们经常需要控制并发任务的数量,避免同时执行过多任务导致资源耗尽或性能问题。通过闭包,我们可以实现一种简单而有效的并发控制机制。

控制并发任务数量

假设我们有一个任务列表,每个任务都是一个异步操作,我们希望同时最多执行3个任务。

function asyncTaskFactory(taskId) {
    return function asyncTask() {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log(`Task ${taskId} completed`);
                resolve();
            }, Math.floor(Math.random() * 2000));
        });
    };
}

function runTasksConcurrently(taskList, maxConcurrent) {
    let runningCount = 0;
    let taskIndex = 0;
    const results = [];

    function runNextTask() {
        while (runningCount < maxConcurrent && taskIndex < taskList.length) {
            runningCount++;
            const task = taskList[taskIndex++];
            task()
               .then(result => {
                    results.push(result);
                    runningCount--;
                    runNextTask();
                })
               .catch(err => {
                    console.error(`Task failed:`, err);
                    runningCount--;
                    runNextTask();
                });
        }
    }

    runNextTask();

    return new Promise((resolve, reject) => {
        const interval = setInterval(() => {
            if (runningCount === 0 && taskIndex === taskList.length) {
                clearInterval(interval);
                resolve(results);
            }
        }, 100);
    });
}

let taskList = Array.from({ length: 10 }, (_, i) => asyncTaskFactory(i + 1));
runTasksConcurrently(taskList, 3).then(results => console.log('All tasks completed:', results));

在上述代码中,asyncTaskFactory创建了单个异步任务的工厂函数。runTasksConcurrently函数实现了并发控制。它通过闭包维护了runningCount(当前正在运行的任务数量)和taskIndex(下一个要执行的任务索引)等状态。runNextTask函数负责在有空闲槽位时启动新的任务,并在任务完成或失败时更新状态并尝试启动下一个任务。

任务队列管理

除了控制并发任务的数量,我们还可以通过闭包来管理任务队列,实现任务的优先级、延迟执行等功能。

例如,我们可以实现一个带有优先级的任务队列:

function priorityTaskFactory(taskId, priority) {
    return function priorityTask() {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log(`Task ${taskId} (Priority ${priority}) completed`);
                resolve();
            }, Math.floor(Math.random() * 2000));
        });
    };
}

function runPriorityTasks(taskList) {
    let runningCount = 0;
    let taskIndex = 0;
    const results = [];

    function runNextTask() {
        while (runningCount < 3 && taskIndex < taskList.length) {
            const nextTask = taskList.slice().sort((a, b) => b[1] - a[1])[taskIndex];
            runningCount++;
            const task = nextTask[0];
            taskIndex++;
            task()
               .then(result => {
                    results.push(result);
                    runningCount--;
                    runNextTask();
                })
               .catch(err => {
                    console.error(`Task failed:`, err);
                    runningCount--;
                    runNextTask();
                });
        }
    }

    runNextTask();

    return new Promise((resolve, reject) => {
        const interval = setInterval(() => {
            if (runningCount === 0 && taskIndex === taskList.length) {
                clearInterval(interval);
                resolve(results);
            }
        }, 100);
    });
}

let priorityTaskList = [
    [priorityTaskFactory(1, 2), 2],
    [priorityTaskFactory(2, 1), 1],
    [priorityTaskFactory(3, 3), 3],
    [priorityTaskFactory(4, 2), 2],
    [priorityTaskFactory(5, 1), 1]
];

runPriorityTasks(priorityTaskList).then(results => console.log('All tasks completed:', results));

在上述代码中,priorityTaskFactory创建了带有优先级的任务。runPriorityTasks函数通过闭包管理任务队列,在每次选择下一个任务时,会优先选择优先级高的任务执行。

闭包在并发数据共享中的应用

在并发编程中,数据共享是一个常见的需求,同时也是一个容易引发问题的地方。闭包可以在一定程度上帮助我们安全地共享数据。

共享状态管理

假设我们有多个异步任务需要共享一个计数器,并且在每次任务完成时更新这个计数器。

function counterTaskFactory(counter) {
    return function counterTask() {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                counter.value++;
                console.log(`Task completed. Counter value:`, counter.value);
                resolve();
            }, Math.floor(Math.random() * 1000));
        });
    };
}

function runCounterTasks(taskList) {
    let runningCount = 0;
    let taskIndex = 0;
    const results = [];
    const counter = { value: 0 };

    function runNextTask() {
        while (runningCount < 2 && taskIndex < taskList.length) {
            runningCount++;
            const task = taskList[taskIndex++];
            task()
               .then(result => {
                    results.push(result);
                    runningCount--;
                    runNextTask();
                })
               .catch(err => {
                    console.error(`Task failed:`, err);
                    runningCount--;
                    runNextTask();
                });
        }
    }

    runNextTask();

    return new Promise((resolve, reject) => {
        const interval = setInterval(() => {
            if (runningCount === 0 && taskIndex === taskList.length) {
                clearInterval(interval);
                resolve(results);
            }
        }, 100);
    });
}

let counterTaskList = Array.from({ length: 5 }, () => counterTaskFactory({ value: 0 }));
runCounterTasks(counterTaskList).then(results => console.log('All tasks completed:', results));

在上述代码中,counterTaskFactory创建的任务共享了counter对象。通过闭包,每个任务都可以访问并更新这个共享的计数器,同时runCounterTasks函数通过并发控制来管理任务的执行。

避免数据竞争

虽然JavaScript是单线程的,但在异步操作中,如果不注意数据共享的管理,仍然可能出现类似数据竞争的问题。闭包可以帮助我们封装数据和操作,避免意外的数据修改。

例如,我们有一个共享的数组,多个任务需要向这个数组中添加数据:

function arrayTaskFactory(dataArray) {
    return function arrayTask(value) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                dataArray.push(value);
                console.log(`Task added value ${value} to array. Array now:`, dataArray);
                resolve();
            }, Math.floor(Math.random() * 1000));
        });
    };
}

function runArrayTasks(taskList) {
    let runningCount = 0;
    let taskIndex = 0;
    const results = [];
    const dataArray = [];

    function runNextTask() {
        while (runningCount < 3 && taskIndex < taskList.length) {
            runningCount++;
            const task = taskList[taskIndex++];
            task()
               .then(result => {
                    results.push(result);
                    runningCount--;
                    runNextTask();
                })
               .catch(err => {
                    console.error(`Task failed:`, err);
                    runningCount--;
                    runNextTask();
                });
        }
    }

    runNextTask();

    return new Promise((resolve, reject) => {
        const interval = setInterval(() => {
            if (runningCount === 0 && taskIndex === taskList.length) {
                clearInterval(interval);
                resolve(results);
            }
        }, 100);
    });
}

let arrayTaskList = [
    arrayTaskFactory([])(1),
    arrayTaskFactory([])(2),
    arrayTaskFactory([])(3),
    arrayTaskFactory([])(4),
    arrayTaskFactory([])(5)
];

runArrayTasks(arrayTaskList).then(results => console.log('All tasks completed:', results));

在上述代码中,通过闭包将dataArray封装在arrayTaskFactory内部,每个任务只能通过闭包提供的方式来操作这个数组,从而避免了外部对数组的意外修改,降低了数据竞争的风险。

闭包在并发错误处理中的应用

在并发编程中,错误处理是至关重要的。闭包可以帮助我们更好地组织和管理并发任务中的错误处理逻辑。

集中式错误处理

当有多个并发任务时,我们可以通过闭包实现集中式的错误处理,使得错误处理代码更加统一和易于维护。

function taskFactory(taskId) {
    return function task() {
        return new Promise((resolve, reject) => {
            const shouldFail = Math.random() < 0.3;
            setTimeout(() => {
                if (shouldFail) {
                    reject(new Error(`Task ${taskId} failed`));
                } else {
                    console.log(`Task ${taskId} completed`);
                    resolve();
                }
            }, Math.floor(Math.random() * 1000));
        });
    };
}

function runTasksWithErrorHandling(taskList) {
    let runningCount = 0;
    let taskIndex = 0;
    const results = [];
    let hasError = false;

    function runNextTask() {
        while (runningCount < 4 && taskIndex < taskList.length) {
            runningCount++;
            const task = taskList[taskIndex++];
            task()
               .then(result => {
                    results.push(result);
                    runningCount--;
                    runNextTask();
                })
               .catch(err => {
                    if (!hasError) {
                        console.error(`First error caught:`, err);
                        hasError = true;
                    }
                    runningCount--;
                    runNextTask();
                });
        }
    }

    runNextTask();

    return new Promise((resolve, reject) => {
        const interval = setInterval(() => {
            if (runningCount === 0 && taskIndex === taskList.length) {
                clearInterval(interval);
                if (hasError) {
                    reject(new Error('One or more tasks failed'));
                } else {
                    resolve(results);
                }
            }
        }, 100);
    });
}

let taskListWithError = Array.from({ length: 8 }, (_, i) => taskFactory(i + 1));
runTasksWithErrorHandling(taskListWithError)
   .then(results => console.log('All tasks completed successfully:', results))
   .catch(err => console.error('Overall task failure:', err));

在上述代码中,runTasksWithErrorHandling函数通过闭包维护了hasError状态,用于记录是否已经捕获到错误。当某个任务出错时,会在闭包内进行统一的错误处理,并在所有任务执行完毕后根据hasError状态决定是成功还是失败。

错误隔离与恢复

有时候,我们希望在某个任务出错时,其他任务仍然能够继续执行,同时能够对出错的任务进行隔离和尝试恢复。闭包可以帮助我们实现这样的功能。

function recoverableTaskFactory(taskId) {
    return function recoverableTask() {
        let attempts = 0;
        return new Promise((resolve, reject) => {
            function executeTask() {
                const shouldFail = Math.random() < 0.5;
                setTimeout(() => {
                    if (shouldFail && attempts < 3) {
                        attempts++;
                        console.log(`Task ${taskId} failed. Retrying (attempt ${attempts})...`);
                        executeTask();
                    } else if (shouldFail) {
                        reject(new Error(`Task ${taskId} failed after 3 attempts`));
                    } else {
                        console.log(`Task ${taskId} completed`);
                        resolve();
                    }
                }, Math.floor(Math.random() * 1000));
            }
            executeTask();
        });
    };
}

function runRecoverableTasks(taskList) {
    let runningCount = 0;
    let taskIndex = 0;
    const results = [];

    function runNextTask() {
        while (runningCount < 3 && taskIndex < taskList.length) {
            runningCount++;
            const task = taskList[taskIndex++];
            task()
               .then(result => {
                    results.push(result);
                    runningCount--;
                    runNextTask();
                })
               .catch(err => {
                    console.error(`Task failed:`, err);
                    runningCount--;
                    runNextTask();
                });
        }
    }

    runNextTask();

    return new Promise((resolve, reject) => {
        const interval = setInterval(() => {
            if (runningCount === 0 && taskIndex === taskList.length) {
                clearInterval(interval);
                resolve(results);
            }
        }, 100);
    });
}

let recoverableTaskList = Array.from({ length: 5 }, (_, i) => recoverableTaskFactory(i + 1));
runRecoverableTasks(recoverableTaskList).then(results => console.log('All tasks completed:', results));

在上述代码中,recoverableTaskFactory创建的任务在出错时会尝试自动恢复,最多尝试3次。runRecoverableTasks函数通过闭包管理任务的执行,使得每个任务的错误不会影响其他任务的继续执行。

实际应用场景

网络请求并发控制

在前端开发中,经常需要同时发起多个网络请求。例如,在一个电商应用中,可能需要同时获取商品列表、用户信息、推荐商品等。通过闭包实现的并发控制可以避免同时发起过多请求导致网络拥塞或浏览器性能问题。

function fetchDataFactory(url) {
    return function fetchData() {
        return new Promise((resolve, reject) => {
            fetch(url)
               .then(response => response.json())
               .then(data => {
                    console.log(`Fetched data from ${url}`);
                    resolve(data);
                })
               .catch(err => {
                    console.error(`Error fetching from ${url}:`, err);
                    reject(err);
                });
        });
    };
}

function runFetchTasksConcurrently(taskList, maxConcurrent) {
    let runningCount = 0;
    let taskIndex = 0;
    const results = [];

    function runNextTask() {
        while (runningCount < maxConcurrent && taskIndex < taskList.length) {
            runningCount++;
            const task = taskList[taskIndex++];
            task()
               .then(result => {
                    results.push(result);
                    runningCount--;
                    runNextTask();
                })
               .catch(err => {
                    console.error(`Task failed:`, err);
                    runningCount--;
                    runNextTask();
                });
        }
    }

    runNextTask();

    return new Promise((resolve, reject) => {
        const interval = setInterval(() => {
            if (runningCount === 0 && taskIndex === taskList.length) {
                clearInterval(interval);
                resolve(results);
            }
        }, 100);
    });
}

let fetchTaskList = [
    fetchDataFactory('https://example.com/api/products'),
    fetchDataFactory('https://example.com/api/user'),
    fetchDataFactory('https://example.com/api/recommendations'),
    fetchDataFactory('https://example.com/api/categories')
];

runFetchTasksConcurrently(fetchTaskList, 2).then(results => console.log('All data fetched:', results));

在上述代码中,fetchDataFactory创建了单个网络请求任务。runFetchTasksConcurrently函数通过闭包实现了并发控制,确保同时最多有2个网络请求在执行。

任务调度系统

在后端开发中,任务调度系统是常见的应用场景。例如,一个定时任务系统可能需要在不同时间执行不同的任务,并且需要控制并发任务的数量。闭包可以帮助我们实现灵活的任务调度逻辑。

function scheduledTaskFactory(taskId, delay) {
    return function scheduledTask() {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log(`Task ${taskId} (scheduled) completed`);
                resolve();
            }, delay);
        });
    };
}

function runScheduledTasks(taskList, maxConcurrent) {
    let runningCount = 0;
    let taskIndex = 0;
    const results = [];

    function runNextTask() {
        while (runningCount < maxConcurrent && taskIndex < taskList.length) {
            runningCount++;
            const task = taskList[taskIndex++];
            task()
               .then(result => {
                    results.push(result);
                    runningCount--;
                    runNextTask();
                })
               .catch(err => {
                    console.error(`Task failed:`, err);
                    runningCount--;
                    runNextTask();
                });
        }
    }

    runNextTask();

    return new Promise((resolve, reject) => {
        const interval = setInterval(() => {
            if (runningCount === 0 && taskIndex === taskList.length) {
                clearInterval(interval);
                resolve(results);
            }
        }, 100);
    });
}

let scheduledTaskList = [
    [scheduledTaskFactory(1, 1000), 1000],
    [scheduledTaskFactory(2, 2000), 2000],
    [scheduledTaskFactory(3, 1500), 1500],
    [scheduledTaskFactory(4, 3000), 3000]
];

runScheduledTasks(scheduledTaskList, 2).then(results => console.log('All scheduled tasks completed:', results));

在上述代码中,scheduledTaskFactory创建了带有延迟执行的任务。runScheduledTasks函数通过闭包实现了任务调度和并发控制,确保在规定的并发数量内执行任务。

总结闭包在并发实现中的优势与注意事项

闭包在JavaScript的并发实现中具有诸多优势。首先,它能够有效地封装状态,使得异步任务可以安全地共享和修改数据,同时避免了全局变量带来的命名冲突和数据混乱问题。其次,闭包可以帮助我们实现复杂的并发控制逻辑,如任务队列管理、并发任务数量控制等,使得代码更加模块化和易于维护。此外,在错误处理方面,闭包能够实现集中式的错误处理以及错误隔离与恢复,提高了程序的健壮性。

然而,使用闭包进行并发编程也需要注意一些问题。由于闭包会保持对外部变量的引用,可能会导致内存泄漏。例如,如果一个闭包函数长时间存在并且引用了大量的外部数据,而这些数据不再被其他地方使用,就可能导致内存无法释放。因此,在使用闭包时,要确保及时释放不再需要的引用。另外,虽然闭包可以帮助管理并发任务,但过多的闭包嵌套可能会导致代码可读性下降,形成类似于回调地狱的问题。在编写代码时,要注意合理地组织闭包结构,尽量保持代码的清晰和简洁。

通过深入理解闭包的原理并合理运用其特性,我们可以在JavaScript的并发编程中实现高效、健壮且易于维护的代码,满足各种复杂的业务需求。无论是前端的网络请求管理,还是后端的任务调度系统,闭包都为我们提供了强大的工具来构建优秀的并发应用。