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

JavaScript函数属性的并发处理

2024-01-144.3k 阅读

JavaScript 函数属性概述

在 JavaScript 中,函数不仅仅是可执行的代码块,它们还具有属性。这些属性为函数增添了额外的信息和功能。例如,name属性返回函数的名称,这在调试和日志记录中非常有用。

function greet() {
    console.log('Hello!');
}
console.log(greet.name); 

上述代码通过greet.name获取了函数greet的名称并打印出来。

函数还有length属性,它表示函数定义中形式参数的个数。

function add(a, b) {
    return a + b;
}
console.log(add.length); 

这里add.length返回2,因为add函数定义了两个参数。

并发处理基础概念

并发是指在同一时间段内处理多个任务。在 JavaScript 中,由于其单线程的特性,并发处理通常通过事件循环和异步操作来实现。

事件循环机制

JavaScript 的事件循环是其实现异步的核心机制。它持续检查调用栈是否为空,如果为空,就从任务队列中取出任务放入调用栈执行。任务队列中存放的是异步操作完成后产生的回调函数。

例如,setTimeout函数会将其回调函数放入任务队列。

console.log('Start');
setTimeout(() => {
    console.log('Timeout callback');
}, 1000);
console.log('End');

在上述代码中,首先打印Start,然后设置一个定时器,接着打印End。一秒后,定时器的回调函数被放入任务队列,当调用栈为空时,该回调函数被执行,打印Timeout callback

异步操作类型

  1. 回调函数:这是 JavaScript 中最基本的异步处理方式。例如,文件读取操作通常以回调函数的形式提供结果。
const fs = require('fs');
fs.readFile('example.txt', 'utf8', (err, data) => {
    if (err) {
        console.error(err);
    } else {
        console.log(data);
    }
});

这里fs.readFile是一个异步操作,它接受一个回调函数,在文件读取完成后调用该回调函数。

  1. Promise:Promise 是一种更强大的异步处理方式,它解决了回调地狱的问题。Promise 有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
function delay(ms) {
    return new Promise((resolve) => {
        setTimeout(resolve, ms);
    });
}
delay(2000).then(() => {
    console.log('Delayed for 2 seconds');
});

上述代码中,delay函数返回一个 Promise,setTimeout完成后会 resolve 该 Promise,then方法中的回调函数会在 Promise 被 resolve 时执行。

  1. async/await:这是基于 Promise 的语法糖,使异步代码看起来更像同步代码。
async function main() {
    await delay(1000);
    console.log('Waited for 1 second');
}
main();

main函数中,await关键字暂停函数的执行,直到 Promise 被 resolve。

函数属性与并发处理的结合

利用函数属性管理并发任务

  1. 自定义属性用于任务跟踪:可以为函数添加自定义属性来跟踪并发任务的状态。例如,假设有多个图片加载任务,我们可以为每个加载函数添加一个属性来标记任务是否完成。
function loadImage(url) {
    const img = new Image();
    img.src = url;
    img.onload = () => {
        loadImage.isCompleted = true;
        console.log('Image loaded successfully');
    };
    img.onerror = () => {
        loadImage.isCompleted = true;
        console.log('Image load failed');
    };
    return img;
}

const img1 = loadImage('image1.jpg');
console.log(loadImage.isCompleted); 

在上述代码中,loadImage函数添加了isCompleted属性来跟踪图片加载任务的状态。

  1. 利用name属性区分任务:在处理多个并发任务时,函数的name属性可以用于区分不同类型的任务。
function task1() {
    // 任务1的逻辑
    console.log('Task 1 is running');
}

function task2() {
    // 任务2的逻辑
    console.log('Task 2 is running');
}

const tasks = [task1, task2];
tasks.forEach(task => {
    console.log(`Starting task: ${task.name}`);
    task();
});

这里通过task.name可以清楚地知道正在执行的任务名称。

并发执行多个函数及其属性处理

  1. Promise.all 与函数属性Promise.all可以并发执行多个 Promise,并在所有 Promise 都 resolve 后返回结果。我们可以结合函数属性来管理这些并发任务。
function fetchData1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Data from fetch1');
        }, 1000);
    });
}

function fetchData2() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Data from fetch2');
        }, 1500);
    });
}

fetchData1.taskType = 'data - fetch';
fetchData2.taskType = 'data - fetch';

Promise.all([fetchData1(), fetchData2()]).then((results) => {
    console.log(results); 
    const taskType = fetchData1.taskType;
    console.log(`Tasks are of type: ${taskType}`);
});

在上述代码中,为fetchData1fetchData2函数添加了taskType属性,Promise.all执行完成后可以通过该属性获取任务类型。

  1. async/await 与函数属性:在使用async/await时,同样可以利用函数属性。
async function processData1() {
    await delay(1000);
    return 'Processed data 1';
}

async function processData2() {
    await delay(1200);
    return 'Processed data 2';
}

processData1.taskPriority = 'high';
processData2.taskPriority = 'low';

async function mainProcess() {
    const result1 = await processData1();
    const result2 = await processData2();
    console.log(result1, result2); 
    const priority1 = processData1.taskPriority;
    const priority2 = processData2.taskPriority;
    console.log(`Priority of processData1: ${priority1}, Priority of processData2: ${priority2}`);
}

mainProcess();

这里为processData1processData2函数添加了taskPriority属性,在mainProcess函数中可以获取并使用这些属性。

处理函数属性并发冲突

并发访问同一函数属性的冲突

  1. 问题表现:当多个并发任务同时访问和修改同一个函数属性时,可能会出现数据不一致的问题。
function counter() {
    if (!counter.value) {
        counter.value = 0;
    }
    counter.value++;
    return counter.value;
}

const promises = [];
for (let i = 0; i < 10; i++) {
    promises.push(new Promise((resolve) => {
        setTimeout(() => {
            const result = counter();
            resolve(result);
        }, i * 100);
    }));
}

Promise.all(promises).then((results) => {
    console.log(results); 
    console.log(counter.value); 
});

在上述代码中,多个定时器并发调用counter函数,由于 JavaScript 的单线程特性,虽然看起来是并发执行,但实际上是依次执行。然而,如果在多线程环境下,counter.value的自增操作可能会出现数据不一致,因为读取和自增操作不是原子性的。

  1. 解决方案:可以使用锁机制来解决这个问题。在 JavaScript 单线程环境中,可以通过利用 Promise 的特性模拟锁。
let lock = false;
function counter() {
    return new Promise((resolve) => {
        while (lock) {
            // 等待锁释放
        }
        lock = true;
        if (!counter.value) {
            counter.value = 0;
        }
        counter.value++;
        const result = counter.value;
        lock = false;
        resolve(result);
    });
}

const promises = [];
for (let i = 0; i < 10; i++) {
    promises.push(counter());
}

Promise.all(promises).then((results) => {
    console.log(results); 
    console.log(counter.value); 
});

这里通过lock变量模拟锁,保证在同一时间只有一个任务可以修改counter.value

不同函数属性在并发中的相互影响

  1. 问题分析:当不同函数的属性在并发任务中相互关联时,可能会出现复杂的问题。例如,一个函数的属性依赖于另一个函数的执行结果,而这些函数在并发执行。
function setup() {
    setup.config = {
        serverUrl: 'http://example.com'
    };
}

function fetchData() {
    if (!setup.config) {
        throw new Error('Setup not done');
    }
    return `Fetching data from ${setup.config.serverUrl}`;
}

const setupPromise = new Promise((resolve) => {
    setTimeout(() => {
        setup();
        resolve();
    }, 1000);
});

const fetchPromise = new Promise((resolve) => {
    setTimeout(() => {
        try {
            const data = fetchData();
            resolve(data);
        } catch (error) {
            resolve(error.message);
        }
    }, 500);
});

Promise.all([setupPromise, fetchPromise]).then((results) => {
    console.log(results); 
});

在上述代码中,fetchData函数依赖于setup函数设置的config属性。由于fetchData可能在setup完成之前执行,会导致错误。

  1. 解决策略:可以通过控制任务的执行顺序来解决。例如,使用async/await确保setup函数先执行。
async function setup() {
    setup.config = {
        serverUrl: 'http://example.com'
    };
}

function fetchData() {
    if (!setup.config) {
        throw new Error('Setup not done');
    }
    return `Fetching data from ${setup.config.serverUrl}`;
}

async function main() {
    await setup();
    const data = fetchData();
    console.log(data); 
}

main();

这里通过await setup()确保setup函数执行完成后再执行fetchData函数。

性能优化与函数属性并发处理

优化并发任务数量

  1. 限制并发任务数的原因:过多的并发任务可能会导致资源耗尽,影响性能。例如,同时发起过多的网络请求可能会使网络带宽饱和,导致所有请求变慢。

  2. 实现方式:可以使用队列来管理并发任务,控制同时执行的任务数量。

function taskExecutor(taskQueue, maxConcurrent) {
    let activeCount = 0;
    function executeNext() {
        while (activeCount < maxConcurrent && taskQueue.length > 0) {
            const task = taskQueue.shift();
            activeCount++;
            task().then(() => {
                activeCount--;
                executeNext();
            });
        }
    }
    executeNext();
}

function task1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('Task 1 completed');
            resolve();
        }, 1000);
    });
}

function task2() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('Task 2 completed');
            resolve();
        }, 1500);
    });
}

const taskQueue = [task1, task2, task1, task2, task1];
taskExecutor(taskQueue, 2);

在上述代码中,taskExecutor函数接受一个任务队列和最大并发数,通过控制activeCount来确保同时执行的任务不超过最大并发数。

函数属性对性能的影响

  1. 属性访问开销:频繁访问和修改函数属性可能会带来一定的性能开销。例如,每次读取函数的自定义属性时,都需要查找对象的属性,这在大量并发任务中可能会累积成明显的性能问题。
function expensiveTask() {
    for (let i = 0; i < 1000000; i++) {
        // 模拟一些计算
        const result = i * i;
        expensiveTask.counter++;
    }
    return expensiveTask.counter;
}

expensiveTask.counter = 0;
const startTime = Date.now();
for (let j = 0; j < 100; j++) {
    expensiveTask();
}
const endTime = Date.now();
console.log(`Execution time: ${endTime - startTime} ms`);

在上述代码中,每次在expensiveTask函数内部访问和修改counter属性,会增加一定的性能开销。

  1. 优化建议:尽量减少不必要的属性访问和修改。如果可能,将一些计算结果缓存起来,避免重复计算和属性访问。
function optimizedTask() {
    let localCounter = 0;
    for (let i = 0; i < 1000000; i++) {
        const result = i * i;
        localCounter++;
    }
    optimizedTask.counter = localCounter;
    return optimizedTask.counter;
}

optimizedTask.counter = 0;
const startTime = Date.now();
for (let j = 0; j < 100; j++) {
    optimizedTask();
}
const endTime = Date.now();
console.log(`Execution time: ${endTime - startTime} ms`);

这里通过使用局部变量localCounter减少了对函数属性counter的频繁访问和修改,提高了性能。

错误处理与函数属性并发处理

并发任务中的错误传播

  1. Promise 的错误处理:在使用Promise.all执行并发任务时,如果其中一个 Promise 被 reject,整个Promise.all会被 reject,错误会传播。
function successTask() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Success');
        }, 1000);
    });
}

function errorTask() {
    return new Promise((_, reject) => {
        setTimeout(() => {
            reject(new Error('Task failed'));
        }, 1500);
    });
}

Promise.all([successTask(), errorTask()]).catch((error) => {
    console.error(error.message); 
});

在上述代码中,errorTask被 reject 后,Promise.all会捕获该错误并执行catch块。

  1. async/await 的错误处理:在async函数中使用await时,如果 Promise 被 reject,会抛出错误,可以使用try...catch块捕获。
async function main() {
    try {
        await successTask();
        await errorTask();
    } catch (error) {
        console.error(error.message); 
    }
}

main();

这里通过try...catch捕获了errorTask抛出的错误。

函数属性与错误关联

  1. 利用属性标记错误:可以为函数添加属性来标记任务是否发生错误,以及错误的具体信息。
function divide(a, b) {
    try {
        const result = a / b;
        divide.error = null;
        return result;
    } catch (error) {
        divide.error = error;
        return null;
    }
}

const result1 = divide(10, 2);
const result2 = divide(10, 0);
console.log(result1); 
console.log(result2); 
if (divide.error) {
    console.error(divide.error.message); 
}

在上述代码中,divide函数添加了error属性,根据除法操作是否成功设置该属性,以便后续检查错误。

  1. 错误处理与属性清理:在处理错误后,需要清理相关的函数属性,以避免影响后续任务。
function complexTask() {
    complexTask.tempData = 'Some temporary data';
    try {
        // 复杂任务逻辑
        throw new Error('Task failed');
    } catch (error) {
        console.error(error.message); 
        delete complexTask.tempData;
    }
}

complexTask();
console.log(complexTask.tempData); 

这里在捕获错误后,通过delete操作删除了complexTask函数的tempData属性,避免其对后续操作产生影响。

实践案例:Web 应用中的并发与函数属性

图片预加载

  1. 需求分析:在 Web 应用中,为了提高用户体验,常常需要预加载图片。我们可以并发预加载多个图片,并利用函数属性管理加载状态。

  2. 代码实现

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Image Pre - load</title>
</head>

<body>
    <script>
        function preloadImage(url) {
            const img = new Image();
            img.src = url;
            img.onload = () => {
                preloadImage.isLoaded = true;
                console.log(`Image ${url} loaded`);
            };
            img.onerror = () => {
                preloadImage.isLoaded = false;
                console.log(`Image ${url} load failed`);
            };
            return img;
        }

        const imageUrls = ['image1.jpg', 'image2.jpg', 'image3.jpg'];
        const promises = [];
        imageUrls.forEach(url => {
            promises.push(new Promise((resolve) => {
                const img = preloadImage(url);
                img.onload = () => {
                    resolve();
                };
                img.onerror = () => {
                    resolve();
                };
            }));
        });

        Promise.all(promises).then(() => {
            console.log('All images pre - loaded');
        });
    </script>
</body>

</html>

在上述代码中,preloadImage函数为每个图片加载任务添加了isLoaded属性,通过Promise.all并发预加载多个图片,并在所有图片加载完成后执行相应操作。

数据批量获取与处理

  1. 业务场景:在 Web 应用中,可能需要从多个 API 端点获取数据,并对这些数据进行统一处理。

  2. 具体实现

function fetchData(url) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', url, true);
        xhr.onreadystatechange = () => {
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                    resolve(JSON.parse(xhr.responseText));
                } else {
                    reject(new Error('Request failed'));
                }
            }
        };
        xhr.send();
    });
}

function processData(data) {
    // 数据处理逻辑
    return data.map(item => item * 2);
}

const urls = ['http://api1.com/data', 'http://api2.com/data', 'http://api3.com/data'];
const promises = urls.map(url => fetchData(url));

Promise.all(promises).then((results) => {
    const allData = [].concat(...results);
    const processedData = processData(allData);
    console.log(processedData); 
}).catch((error) => {
    console.error(error.message); 
});

这里通过fetchData函数并发从多个 URL 获取数据,利用Promise.all等待所有数据获取完成后,使用processData函数进行统一处理。同时,可以为fetchData函数添加属性来记录请求的状态、次数等信息,以便进行更细致的管理和调试。例如:

function fetchData(url) {
    fetchData.requestCount = (fetchData.requestCount || 0) + 1;
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', url, true);
        xhr.onreadystatechange = () => {
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                    resolve(JSON.parse(xhr.responseText));
                } else {
                    reject(new Error('Request failed'));
                }
            }
        };
        xhr.send();
    });
}

这样可以通过fetchData.requestCount获取总的请求次数,有助于性能分析和调试。

与其他技术结合的函数属性并发处理

与 Web Workers 的结合

  1. Web Workers 简介:Web Workers 允许在后台线程中运行脚本,从而实现真正的并行处理,突破 JavaScript 单线程的限制。

  2. 结合方式:在 Web Workers 中,可以利用函数属性来传递和管理任务相关的信息。

// main.js
const worker = new Worker('worker.js');
function taskWithProperty() {
    taskWithProperty.taskId = 'task1';
    return 'Some data for worker';
}
const data = taskWithProperty();
worker.postMessage({ data, taskId: taskWithProperty.taskId });

worker.onmessage = (event) => {
    console.log(`Received from worker: ${event.data}`);
};

// worker.js
self.onmessage = (event) => {
    const { data, taskId } = event.data;
    console.log(`Received task with id: ${taskId}`);
    const result = data.toUpperCase();
    self.postMessage(result);
};

在上述代码中,主线程通过函数taskWithProperty添加taskId属性,并将数据和属性值传递给 Web Worker,Web Worker 根据taskId识别任务并进行处理。

与 Node.js 模块系统的结合

  1. Node.js 模块特性:Node.js 的模块系统允许将代码组织成多个模块,每个模块可以导出函数。这些导出的函数属性在并发处理中可以发挥重要作用。

  2. 实际应用:假设我们有一个模块用于处理文件操作,模块中的函数可以添加属性来管理文件操作任务。

// fileUtils.js
const fs = require('fs');

function readFileAsync(path) {
    readFileAsync.taskStatus = 'in - progress';
    return new Promise((resolve, reject) => {
        fs.readFile(path, 'utf8', (err, data) => {
            if (err) {
                readFileAsync.taskStatus = 'failed';
                reject(err);
            } else {
                readFileAsync.taskStatus = 'completed';
                resolve(data);
            }
        });
    });
}

function writeFileAsync(path, data) {
    writeFileAsync.taskStatus = 'in - progress';
    return new Promise((resolve, reject) => {
        fs.writeFile(path, data, (err) => {
            if (err) {
                writeFileAsync.taskStatus = 'failed';
                reject(err);
            } else {
                writeFileAsync.taskStatus = 'completed';
                resolve();
            }
        });
    });
}

module.exports = {
    readFileAsync,
    writeFileAsync
};
// main.js
const fileUtils = require('./fileUtils');

Promise.all([
    fileUtils.readFileAsync('input.txt'),
    fileUtils.writeFileAsync('output.txt', 'Some data')
]).then(() => {
    console.log('All file operations completed');
    console.log(`Read file task status: ${fileUtils.readFileAsync.taskStatus}`);
    console.log(`Write file task status: ${fileUtils.writeFileAsync.taskStatus}`);
}).catch((error) => {
    console.error(error.message);
});

在上述代码中,fileUtils模块中的readFileAsyncwriteFileAsync函数添加了taskStatus属性,在主程序中通过并发执行这些函数,并获取其属性值来了解任务状态。