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

JavaScript函数作为值的并发操作

2022-08-045.5k 阅读

JavaScript 函数作为值的并发操作

JavaScript 中的函数本质

在 JavaScript 中,函数是一等公民。这意味着函数可以像其他基本数据类型(如数字、字符串)一样被对待。它可以被赋值给变量,作为参数传递给其他函数,甚至可以作为其他函数的返回值。这种特性为 JavaScript 在处理复杂逻辑,尤其是并发操作时,提供了极大的灵活性。

例如,简单地定义一个函数并将其赋值给变量:

function add(a, b) {
    return a + b;
}
let myFunction = add;
console.log(myFunction(2, 3)); 

这里,add 函数被赋值给了 myFunction 变量,并且可以像调用 add 一样调用 myFunction

并发操作基础

在计算机编程中,并发操作是指在同一时间段内处理多个任务。在 JavaScript 中,由于其单线程的特性,并发操作并不是真正意义上的多线程并行执行。而是通过事件循环(Event Loop)机制,在不同的时间片段内轮流执行不同的任务,从而给用户一种并发执行的错觉。

回调函数与并发

回调函数是 JavaScript 实现并发操作的一种基础方式。当一个异步操作(如读取文件、网络请求)完成时,会调用事先定义好的回调函数。

以下是一个简单的使用 setTimeout 模拟异步操作并使用回调函数的例子:

function asyncTask(callback) {
    setTimeout(() => {
        console.log('异步任务完成');
        callback();
    }, 2000);
}

function afterTask() {
    console.log('回调函数被调用');
}

asyncTask(afterTask);

在这个例子中,asyncTask 函数模拟了一个异步任务,setTimeout 会在 2 秒后执行回调函数 afterTask

函数作为值在并发中的应用

用函数作为参数实现并发控制

当处理多个异步任务时,我们常常需要对它们进行协调和控制。通过将函数作为参数传递,可以方便地实现这一点。

假设我们有两个异步任务,并且希望在第一个任务完成后再执行第二个任务:

function task1(callback) {
    setTimeout(() => {
        console.log('任务 1 完成');
        callback();
    }, 1000);
}

function task2() {
    console.log('任务 2 开始');
    setTimeout(() => {
        console.log('任务 2 完成');
    }, 1000);
}

task1(task2);

这里,task1 函数接受一个回调函数 callback,在任务 1 完成后调用这个回调函数,而我们将 task2 函数作为参数传递给 task1,从而实现了顺序执行两个异步任务。

函数作为返回值与并发

函数不仅可以作为参数传递,还可以作为返回值。这种特性在处理复杂的并发逻辑时非常有用。

考虑一个场景,我们需要创建一个函数,该函数可以根据不同的条件执行不同的异步任务。

function createTask(condition) {
    if (condition) {
        return function() {
            setTimeout(() => {
                console.log('条件为真时的任务完成');
            }, 1500);
        };
    } else {
        return function() {
            setTimeout(() => {
                console.log('条件为假时的任务完成');
            }, 1500);
        };
    }
}

let task = createTask(true);
task();

在这个例子中,createTask 函数根据传入的 condition 返回不同的函数,这些返回的函数都是异步任务。通过这种方式,我们可以根据运行时的条件动态地创建和执行异步任务。

并发操作中的 Promise

Promise 简介

Promise 是 JavaScript 中用于处理异步操作的一种更强大的方式。它代表一个异步操作的最终完成(或失败)及其结果值。Promise 有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。

使用 Promise 进行并发操作

假设我们有两个异步任务,每个任务都返回一个 Promise:

function asyncOperation1() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('操作 1 完成');
        }, 1000);
    });
}

function asyncOperation2() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('操作 2 完成');
        }, 1500);
    });
}

如果我们希望在两个操作都完成后执行一些操作,可以使用 Promise.all

Promise.all([asyncOperation1(), asyncOperation2()])
   .then((results) => {
        console.log(results); 
    })
   .catch((error) => {
        console.error('有任务失败:', error);
    });

Promise.all 接受一个 Promise 数组作为参数,当所有的 Promise 都变为 fulfilled 状态时,它返回的 Promise 才会 resolve,并将所有 Promise 的 resolve 值组成一个数组作为结果。如果其中任何一个 Promise 被 rejectedPromise.all 返回的 Promise 就会立即 rejected

函数作为 Promise 的参数

Promise 的 then 方法接受一个函数作为参数,这个函数会在 Promise resolve 时被调用。同样,catch 方法也接受一个函数作为参数,在 Promise rejected 时被调用。

asyncOperation1()
   .then((result) => {
        console.log('操作 1 的结果:', result);
        return asyncOperation2(); 
    })
   .then((result2) => {
        console.log('操作 2 的结果:', result2);
    })
   .catch((error) => {
        console.error('操作失败:', error);
    });

在这个例子中,asyncOperation1 成功 resolve 后,then 中的函数会被调用,并且这个函数返回了 asyncOperation2 的 Promise,从而实现了链式调用异步操作。

并发操作中的 Async/Await

Async/Await 基础

async/await 是 JavaScript 中基于 Promise 的一种异步函数语法糖。async 关键字用于定义一个异步函数,该函数始终返回一个 Promise。await 关键字只能在 async 函数内部使用,它用于暂停异步函数的执行,直到 Promise 被 resolverejected

使用 Async/Await 实现并发

我们可以使用 async/await 更简洁地实现多个异步操作的并发。例如,重写之前使用 Promise.all 的例子:

async function concurrentOperations() {
    try {
        let result1 = await asyncOperation1();
        let result2 = await asyncOperation2();
        console.log([result1, result2]); 
    } catch (error) {
        console.error('有任务失败:', error);
    }
}

concurrentOperations();

这里,await 使得代码看起来像是同步执行的,但实际上 asyncOperation1asyncOperation2 是异步执行的。await 会等待 Promise 完成,然后将 resolve 的值赋给相应的变量。

函数作为 Async/Await 中的值

async 函数内部,可以定义和使用函数作为值来处理复杂的逻辑。例如:

async function main() {
    function processResult(result) {
        return result.toUpperCase();
    }
    let result = await asyncOperation1();
    let processedResult = processResult(result);
    console.log(processedResult);
}

main();

在这个例子中,processResult 函数在 async 函数 main 内部定义,用于处理 asyncOperation1 的结果。通过这种方式,可以在异步操作的过程中灵活地处理数据。

高级并发模式

并发限制

在处理大量异步任务时,有时我们需要限制同时执行的任务数量,以避免资源耗尽。可以通过队列和 Promise 来实现这一点。

function asyncTaskWithDelay(delay, value) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log(`任务 ${value} 完成`);
            resolve(value);
        }, delay);
    });
}

function limitConcurrentTasks(tasks, maxConcurrent) {
    return new Promise((resolve, reject) => {
        let results = [];
        let completedCount = 0;
        let runningCount = 0;

        function runNext() {
            while (runningCount < maxConcurrent && tasks.length > 0) {
                let task = tasks.shift();
                runningCount++;
                task()
                   .then((result) => {
                        results.push(result);
                        completedCount++;
                        runningCount--;
                        if (completedCount === tasks.length + maxConcurrent) {
                            resolve(results);
                        } else {
                            runNext();
                        }
                    })
                   .catch((error) => {
                        reject(error);
                    });
            }
        }

        runNext();
    });
}

let tasks = Array.from({ length: 10 }, (_, i) => () => asyncTaskWithDelay((i + 1) * 100, i + 1));
limitConcurrentTasks(tasks, 3)
   .then((results) => {
        console.log('所有任务完成,结果:', results);
    })
   .catch((error) => {
        console.error('任务失败:', error);
    });

在这个例子中,limitConcurrentTasks 函数接受一个任务数组和最大并发数 maxConcurrent。它通过一个队列和计数器来控制同时执行的任务数量,确保不会超过最大并发数。

超时处理

在异步操作中,有时我们需要设置一个超时时间,以防止任务长时间阻塞。可以通过 Promise.race 来实现超时处理。

function asyncTask() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('任务完成');
        }, 2000);
    });
}

function timeout(duration) {
    return new Promise((_, reject) => {
        setTimeout(() => {
            reject('操作超时');
        }, duration);
    });
}

Promise.race([asyncTask(), timeout(1500)])
   .then((result) => {
        console.log(result);
    })
   .catch((error) => {
        console.error(error);
    });

在这个例子中,Promise.race 接受两个 Promise,asyncTasktimeout。如果 asyncTask 在 1500 毫秒内没有完成,timeout 的 Promise 会 rejected,从而触发 catch 块。

实际应用场景

网络请求并发

在前端开发中,经常需要同时发起多个网络请求,例如获取用户信息、用户设置和用户权限等。通过并发操作,可以显著提高应用的性能。

假设我们使用 fetch 进行网络请求:

function fetchUserInfo() {
    return fetch('/api/user/info');
}

function fetchUserSettings() {
    return fetch('/api/user/settings');
}

function fetchUserPermissions() {
    return fetch('/api/user/permissions');
}

async function getUserData() {
    try {
        let [infoResponse, settingsResponse, permissionsResponse] = await Promise.all([
            fetchUserInfo(),
            fetchUserSettings(),
            fetchUserPermissions()
        ]);
        let info = await infoResponse.json();
        let settings = await settingsResponse.json();
        let permissions = await permissionsResponse.json();
        return { info, settings, permissions };
    } catch (error) {
        console.error('网络请求失败:', error);
    }
}

getUserData().then((data) => {
    console.log('用户数据:', data);
});

在这个例子中,Promise.all 使得三个网络请求并发执行,提高了获取用户数据的效率。

数据处理并发

在处理大量数据时,例如对数组中的每个元素进行异步处理,可以使用并发操作来加速处理过程。

function processData(item) {
    return new Promise((resolve) => {
        setTimeout(() => {
            let processedItem = item * 2;
            console.log(`处理 ${item} 完成,结果: ${processedItem}`);
            resolve(processedItem);
        }, 500);
    });
}

async function processArray(arr) {
    let results = await Promise.all(arr.map(processData));
    return results;
}

let dataArray = [1, 2, 3, 4, 5];
processArray(dataArray).then((results) => {
    console.log('所有数据处理完成,结果:', results);
});

这里,map 方法将 processData 函数应用到数组的每个元素上,Promise.all 确保所有异步处理并发执行,并在所有处理完成后返回结果。

错误处理与并发

在并发操作中,错误处理至关重要。不同的并发方式有不同的错误处理机制。

Promise 的错误处理

在使用 Promise 时,可以通过 catch 方法捕获错误。如前面的例子所示,Promise.all 中只要有一个 Promise 被 rejected,整个 Promise.all 返回的 Promise 就会被 rejected,并进入 catch 块。

Async/Await 的错误处理

async/await 中,可以使用 try...catch 块来捕获错误。

async function asyncWithError() {
    try {
        let result = await asyncTaskThatFails();
        console.log(result);
    } catch (error) {
        console.error('捕获到错误:', error);
    }
}

function asyncTaskThatFails() {
    return new Promise((_, reject) => {
        setTimeout(() => {
            reject('任务失败');
        }, 1000);
    });
}

asyncWithError();

通过这种方式,可以优雅地处理异步操作中可能出现的错误,保证程序的稳定性。

性能考量

在进行并发操作时,性能是一个重要的考量因素。虽然并发可以提高效率,但过多的并发任务可能会导致资源消耗过大,反而降低性能。

并发任务数量的影响

并发任务数量过多可能会导致 CPU 和内存资源的过度使用。例如,在进行大量网络请求并发时,可能会占用过多的网络带宽,导致请求响应变慢。因此,需要根据实际情况合理设置并发任务的数量,例如在前面提到的并发限制的例子中,通过设置合适的最大并发数,可以优化性能。

任务执行时间的影响

任务本身的执行时间也会影响并发操作的性能。如果任务执行时间较长,过多的并发可能不会带来明显的性能提升,甚至可能因为上下文切换等开销而降低性能。对于这种情况,可以考虑将长任务分解为多个短任务,或者采用其他优化策略,如使用 Web Workers 在后台线程执行计算密集型任务。

与其他技术的结合

与 Web Workers 的结合

Web Workers 允许在后台线程中执行脚本,从而避免阻塞主线程。在处理计算密集型的并发任务时,可以结合 Web Workers 和 JavaScript 的并发操作。

例如,假设我们有一个复杂的计算任务:

// main.js
function startWorker() {
    let worker = new Worker('worker.js');
    worker.onmessage = function(event) {
        console.log('从 Worker 接收到结果:', event.data);
    };
    worker.postMessage({ data: [1, 2, 3, 4, 5] });
}

startWorker();

// worker.js
self.onmessage = function(event) {
    let data = event.data;
    let result = data.reduce((acc, val) => acc + val, 0);
    self.postMessage(result);
};

在这个例子中,主线程通过 postMessage 向 Web Worker 发送数据,Web Worker 进行计算后再通过 postMessage 返回结果,这样主线程在等待计算结果时不会被阻塞,可以继续处理其他任务。

与 Node.js 的结合

在 Node.js 环境中,JavaScript 的并发操作可以与文件系统、网络服务等结合。例如,在处理文件上传时,可以并发读取和处理多个文件。

const fs = require('fs');
const path = require('path');
const { promisify } = require('util');

async function readFiles(files) {
    let readFilePromises = files.map((file) => {
        let filePath = path.join(__dirname, file);
        return promisify(fs.readFile)(filePath, 'utf8');
    });
    let results = await Promise.all(readFilePromises);
    return results;
}

let files = ['file1.txt', 'file2.txt', 'file3.txt'];
readFiles(files).then((contents) => {
    console.log('文件内容:', contents);
});

这里,Promise.all 结合 fs.readFile 的 Promise 化版本,实现了并发读取多个文件的功能。

通过深入理解 JavaScript 函数作为值在并发操作中的应用,我们可以更好地编写高效、灵活且健壮的代码,以满足各种复杂的业务需求。无论是在前端开发还是后端开发中,这些技术都能帮助我们提升应用的性能和用户体验。