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

JavaScript期约(Promise)的链式调用

2023-05-055.7k 阅读

JavaScript 期约(Promise)的链式调用

什么是 Promise 链式调用

在 JavaScript 中,Promise 是一种处理异步操作的方式,它代表了一个尚未完成但预计将来会完成的操作结果。Promise 有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。

Promise 的链式调用是指通过 .then() 方法不断地串联多个异步操作。每个 .then() 方法都会返回一个新的 Promise,这样就可以将多个异步操作连接成一个链条,前一个操作的结果可以作为后一个操作的输入。

Promise 链式调用的基本语法

const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Result of promise1');
    }, 1000);
});

promise1
   .then(result1 => {
        console.log(result1);
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(`Processed: ${result1}`);
            }, 1000);
        });
    })
   .then(result2 => {
        console.log(result2);
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(`Final result: ${result2}`);
            }, 1000);
        });
    })
   .then(finalResult => {
        console.log(finalResult);
    })
   .catch(error => {
        console.error('An error occurred:', error);
    });

在上述代码中,promise1 是一个初始的 Promise,它在 1 秒后 resolve 并返回一个字符串。第一个 .then() 方法接收 promise1 的结果,打印出来,并返回一个新的 Promise,这个新 Promise 在 1 秒后 resolve 并处理前一个结果。以此类推,通过链式调用实现了多个异步操作的顺序执行。

链式调用中的返回值

  1. 返回非 Promise 值 当在 .then() 方法中返回一个非 Promise 值时,这个值会被自动包装成一个已 resolved 的 Promise。例如:
const promise = new Promise((resolve, reject) => {
    resolve('Initial value');
});

promise
   .then(result => {
        return 'Returned non - Promise value';
    })
   .then(nextResult => {
        console.log(nextResult);
    });

这里第一个 .then() 返回了一个字符串,第二个 .then() 会接收到这个字符串,因为它被自动包装成了一个已 resolved 的 Promise。

  1. 返回 Promise 如果在 .then() 中返回一个 Promise,后续的 .then() 会等待这个返回的 Promise 完成。例如:
const promise = new Promise((resolve, reject) => {
    resolve('Initial value');
});

promise
   .then(result => {
        return new Promise((innerResolve, innerReject) => {
            setTimeout(() => {
                innerResolve(`Processed: ${result}`);
            }, 1000);
        });
    })
   .then(nextResult => {
        console.log(nextResult);
    });

第一个 .then() 返回了一个新的 Promise,第二个 .then() 会等待这个新 Promise 被 resolve 后才执行。

错误处理在链式调用中的表现

  1. 单个 .catch() 处理整个链条的错误 在 Promise 链式调用中,可以在链条的末尾添加一个 .catch() 方法来捕获整个链条中任何一个 Promise 被 rejected 时的错误。
const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('Error in promise1');
    }, 1000);
});

promise1
   .then(result1 => {
        console.log(result1);
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(`Processed: ${result1}`);
            }, 1000);
        });
    })
   .then(result2 => {
        console.log(result2);
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(`Final result: ${result2}`);
            }, 1000);
        });
    })
   .catch(error => {
        console.error('An error occurred:', error);
    });

在这个例子中,promise1 被 rejected,但是由于在链条末尾有 .catch(),错误被捕获并打印。

  1. 在链条中间处理错误 也可以在链条中间的某个 .then() 后添加 .catch() 来处理特定部分的错误。例如:
const promise1 = new Promise((resolve, reject) => {
    resolve('Initial value');
});

promise1
   .then(result1 => {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                reject('Error in middle promise');
            }, 1000);
        });
    })
   .catch(error => {
        console.error('Error in middle:', error);
        return 'Recovered value';
    })
   .then(result2 => {
        console.log(result2);
    });

这里中间的 Promise 被 rejected,.catch() 捕获了错误并返回了一个新值,后续的 .then() 可以继续执行。

链式调用与并发和并行操作

  1. 并发操作 虽然 Promise 链式调用主要用于顺序执行异步操作,但可以通过 Promise.all() 等方法实现并发操作,然后再将并发操作的结果融入链式调用中。
const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Promise1 result');
    }, 1000);
});

const promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Promise2 result');
    }, 1500);
});

Promise.all([promise1, promise2])
   .then(results => {
        console.log('All promises resolved:', results);
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(`Combined: ${results.join(', ')}`);
            }, 1000);
        });
    })
   .then(finalResult => {
        console.log(finalResult);
    });

Promise.all() 会等待所有传入的 Promise 都 resolved,然后将结果以数组形式传递给 .then(),之后可以继续链式调用。

  1. 并行操作 JavaScript 本身是单线程的,但通过 Web Workers 等技术,可以实现类似并行的效果。在 Promise 链式调用的场景下,可以在每个 .then() 中启动 Web Workers 进行一些 CPU 密集型的并行任务。不过这涉及到更多的跨线程通信和数据传递的细节。例如:
// main.js
const worker = new Worker('worker.js');
const promise = new Promise((resolve, reject) => {
    worker.onmessage = function (event) {
        resolve(event.data);
    };
    worker.onerror = function (error) {
        reject(error);
    };
    worker.postMessage('Start work');
});

promise
   .then(result => {
        console.log('Worker result:', result);
        return new Promise((resolve, reject) => {
            const newWorker = new Worker('anotherWorker.js');
            newWorker.onmessage = function (event) {
                resolve(event.data);
            };
            newWorker.onerror = function (error) {
                reject(error);
            };
            newWorker.postMessage(result);
        });
    })
   .then(finalResult => {
        console.log('Final result from workers:', finalResult);
    });
// worker.js
self.onmessage = function (event) {
    // 模拟一些计算
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
        result += i;
    }
    self.postMessage(result);
};
// anotherWorker.js
self.onmessage = function (event) {
    // 更多计算
    let newResult = event * 2;
    self.postMessage(newResult);
};

在这个例子中,通过 Web Workers 在 Promise 链式调用中实现了类似并行的操作。

链式调用中的微任务与宏任务

  1. 微任务队列 Promise 的 .then() 回调函数会被放入微任务队列。微任务队列会在当前宏任务执行完后,下一个宏任务执行前被执行。例如:
console.log('Start');
const promise = new Promise((resolve, reject) => {
    resolve('Resolved');
});

promise.then(result => {
    console.log('Promise then:', result);
});

console.log('End');

输出结果为:

Start
End
Promise then: Resolved

这里 console.log('End') 先执行,因为它在宏任务中,而 .then() 回调在微任务队列中,在当前宏任务结束后执行。

  1. 宏任务队列setTimeout 这样的操作会将回调放入宏任务队列。当微任务队列为空时,事件循环会从宏任务队列中取出任务执行。例如:
console.log('Start');
const promise = new Promise((resolve, reject) => {
    resolve('Resolved');
});

promise.then(result => {
    console.log('Promise then:', result);
});

setTimeout(() => {
    console.log('Timeout');
}, 0);

console.log('End');

输出结果为:

Start
End
Promise then: Resolved
Timeout

这里 setTimeout 的回调在宏任务队列中,会在微任务队列(.then() 回调所在队列)执行完后才执行。

实际应用场景

  1. 数据获取与处理 在前端开发中,经常需要从服务器获取数据,然后对数据进行处理。例如,先获取用户信息,再根据用户信息获取用户的订单列表,最后处理订单数据。
function getUserInfo() {
    return new Promise((resolve, reject) => {
        // 模拟 API 调用
        setTimeout(() => {
            resolve({ name: 'John', age: 30 });
        }, 1000);
    });
}

function getOrderList(user) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (user) {
                resolve([{ orderId: 1, amount: 100 }, { orderId: 2, amount: 200 }]);
            } else {
                reject('User not found');
            }
        }, 1000);
    });
}

function processOrders(orders) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const totalAmount = orders.reduce((acc, order) => acc + order.amount, 0);
            resolve(`Total amount: ${totalAmount}`);
        }, 1000);
    });
}

getUserInfo()
   .then(user => {
        return getOrderList(user);
    })
   .then(orders => {
        return processOrders(orders);
    })
   .then(finalResult => {
        console.log(finalResult);
    })
   .catch(error => {
        console.error('Error:', error);
    });
  1. 文件读取与处理 在 Node.js 中,可以使用 Promise 链式调用来读取文件内容,然后对文件内容进行解析和处理。
const fs = require('fs');
const path = require('path');
const util = require('util');

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

const filePath = path.join(__dirname, 'example.txt');

readFile(filePath, 'utf8')
   .then(content => {
        console.log('File content:', content);
        const lines = content.split('\n');
        return lines.filter(line => line.trim()!== '');
    })
   .then(filteredLines => {
        const wordCount = filteredLines.reduce((acc, line) => acc + line.split(' ').length, 0);
        return `Total word count: ${wordCount}`;
    })
   .then(finalResult => {
        console.log(finalResult);
    })
   .catch(error => {
        console.error('Error reading file:', error);
    });

在这个例子中,首先使用 readFile 读取文件内容,然后对内容进行处理,统计单词数量。

链式调用的最佳实践

  1. 保持链条简洁 避免在链式调用中添加过多复杂的逻辑,尽量将每个 .then() 方法的功能单一化。这样可以提高代码的可读性和可维护性。例如:
// 不好的实践
const promise = new Promise((resolve, reject) => {
    resolve('Initial');
});

promise
   .then(result => {
        // 多个复杂操作
        let processed1 = result.toUpperCase();
        let processed2 = processed1.split('').reverse().join('');
        return new Promise((innerResolve, innerReject) => {
            setTimeout(() => {
                innerResolve(processed2);
            }, 1000);
        });
    })
   .then(nextResult => {
        console.log(nextResult);
    });

// 好的实践
const promise = new Promise((resolve, reject) => {
    resolve('Initial');
});

function upperCase(result) {
    return result.toUpperCase();
}

function reverseString(result) {
    return result.split('').reverse().join('');
}

promise
   .then(upperCase)
   .then(reverseString)
   .then(result => {
        return new Promise((innerResolve, innerReject) => {
            setTimeout(() => {
                innerResolve(result);
            }, 1000);
        });
    })
   .then(nextResult => {
        console.log(nextResult);
    });
  1. 合理处理错误 在链式调用中,确保在合适的位置处理错误。如果错误处理放在链条末尾,可能会掩盖中间步骤的错误。如果中间某个操作的错误有特定的处理方式,应该在该操作对应的 .then() 后添加 .catch()

  2. 避免回调地狱替代物 虽然 Promise 链式调用解决了回调地狱的问题,但如果链式调用过长且不注意代码结构,也会导致代码难以阅读。可以考虑使用 async/await 语法来进一步优化代码结构,它基于 Promise 并提供了更简洁的异步代码书写方式。

async/await 与 Promise 链式调用的关系

async/await 是 JavaScript 中处理异步操作的另一种方式,它建立在 Promise 的基础之上。async 函数总是返回一个 Promise。await 只能在 async 函数内部使用,它会暂停 async 函数的执行,等待 Promise 被 resolved 或 rejected。

例如,将之前的数据获取与处理的例子用 async/await 改写:

function getUserInfo() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ name: 'John', age: 30 });
        }, 1000);
    });
}

function getOrderList(user) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (user) {
                resolve([{ orderId: 1, amount: 100 }, { orderId: 2, amount: 200 }]);
            } else {
                reject('User not found');
            }
        }, 1000);
    });
}

function processOrders(orders) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const totalAmount = orders.reduce((acc, order) => acc + order.amount, 0);
            resolve(`Total amount: ${totalAmount}`);
        }, 1000);
    });
}

async function main() {
    try {
        const user = await getUserInfo();
        const orders = await getOrderList(user);
        const finalResult = await processOrders(orders);
        console.log(finalResult);
    } catch (error) {
        console.error('Error:', error);
    }
}

main();

这里 async/await 语法使得异步代码看起来更像同步代码,避免了层层嵌套的 .then() 链式调用。但本质上,它还是基于 Promise 进行异步操作的处理,await 等待的就是一个 Promise 的完成。

总结 Promise 链式调用的要点

  1. Promise 链式调用通过 .then() 方法串联多个异步操作,前一个操作的结果可以作为后一个操作的输入。
  2. 正确处理返回值,非 Promise 值会被自动包装,返回 Promise 时后续 .then() 会等待其完成。
  3. 合理进行错误处理,可以在链条末尾或中间合适位置使用 .catch() 捕获错误。
  4. 了解微任务和宏任务队列对 Promise 链式调用执行顺序的影响。
  5. 在实际应用中,如数据获取与处理、文件读取等场景,合理运用 Promise 链式调用。
  6. 遵循最佳实践,保持链条简洁,合理处理错误,避免新的代码混乱。
  7. 认识到 async/await 是基于 Promise 链式调用的更简洁的异步处理方式,可以根据具体情况选择使用。

通过深入理解和掌握 Promise 链式调用,开发者能够更有效地处理 JavaScript 中的异步操作,提高代码的质量和可维护性。无论是在前端开发还是后端开发中,Promise 链式调用都是处理异步流程的重要工具。