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

JavaScript Promise链式调用与错误处理

2022-11-242.5k 阅读

1. Promise链式调用基础

在JavaScript中,Promise是一种处理异步操作的强大工具。Promise有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。一旦Promise状态改变,就不会再变,并且只有异步操作的结果可以决定当前是成功还是失败状态。

Promise链式调用允许我们将多个异步操作按顺序连接起来,每个操作的输出作为下一个操作的输入。这是通过.then()方法实现的。例如:

const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('第一个Promise的结果');
    }, 1000);
});

const promise2 = promise1.then((result) => {
    console.log(result); // 输出:第一个Promise的结果
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('第二个Promise的结果');
        }, 1000);
    });
});

const promise3 = promise2.then((result) => {
    console.log(result); // 输出:第二个Promise的结果
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('第三个Promise的结果');
        }, 1000);
    });
});

promise3.then((result) => {
    console.log(result); // 输出:第三个Promise的结果
});

在上述代码中,promise1首先被创建并在1秒后成功解决(resolved),其结果传递给promise1.then()的回调函数。在这个回调函数中,又返回了一个新的Promise(promise2),promise2同样在1秒后解决,其结果再传递给promise2.then()的回调函数,依此类推。这种链式调用使得异步操作可以按顺序依次执行,避免了“回调地狱”。

2. 链式调用中的值传递

在Promise链式调用中,每个.then()方法返回的值会成为下一个.then()方法回调函数的参数。如果.then()回调函数返回一个非Promise值,这个值会被包装成一个已解决的Promise传递给下一个.then()。例如:

const promise = new Promise((resolve, reject) => {
    resolve(10);
});

promise
   .then((value) => {
        return value * 2; // 返回一个非Promise值20
    })
   .then((newValue) => {
        console.log(newValue); // 输出20
    });

这里,第一个.then()回调函数返回value * 2(即20),这个值被自动包装成一个已解决的Promise,传递给第二个.then()回调函数,从而输出20。

如果.then()回调函数返回一个Promise,下一个.then()会等待这个Promise解决后再执行。例如:

const promise = new Promise((resolve, reject) => {
    resolve(10);
});

promise
   .then((value) => {
        return new Promise((innerResolve, innerReject) => {
            setTimeout(() => {
                innerResolve(value * 2);
            }, 1000);
        });
    })
   .then((newValue) => {
        console.log(newValue); // 1秒后输出20
    });

在这个例子中,第一个.then()回调函数返回了一个新的Promise,这个Promise在1秒后解决,其解决值value * 2(即20)传递给了第二个.then()回调函数。

3. 链式调用的便捷语法

为了简化Promise链式调用,我们可以在每次.then()调用后直接返回新的操作,而不需要显式创建新的Promise。例如:

const fetchData = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('初始数据');
        }, 1000);
    });
};

fetchData()
   .then(data => {
        return data +'经过处理1'; // 直接返回处理后的值
    })
   .then(newData => {
        return newData +'经过处理2'; // 再次返回处理后的值
    })
   .then(finalData => {
        console.log(finalData); // 输出:初始数据 经过处理1 经过处理2
    });

这种语法使得链式调用更加简洁,代码可读性更高。

4. Promise链式调用中的错误处理

在Promise链式调用中,错误处理至关重要。当Promise被拒绝(rejected)时,我们需要捕获并处理这些错误,以避免程序崩溃。

4.1 使用.catch()捕获错误

.catch()方法用于捕获Promise链中任何位置抛出的错误。例如:

const promise = new Promise((resolve, reject) => {
    reject('发生错误');
});

promise
   .then(result => {
        console.log(result);
    })
   .catch(error => {
        console.log('捕获到错误:', error); // 输出:捕获到错误: 发生错误
    });

在这个例子中,promise被拒绝,错误信息发生错误.catch()捕获并输出。

4.2 链式调用中的错误传播

如果在Promise链中某个.then()回调函数抛出错误,这个错误会被传播到下一个.catch()。例如:

const promise = new Promise((resolve, reject) => {
    resolve('初始值');
});

promise
   .then(result => {
        throw new Error('处理过程中发生错误');
    })
   .then(newResult => {
        console.log(newResult);
    })
   .catch(error => {
        console.log('捕获到错误:', error.message); // 输出:捕获到错误: 处理过程中发生错误
    });

这里,第一个.then()回调函数抛出了一个错误,这个错误跳过了第二个.then()(因为第二个.then()只有在第一个.then()成功时才会执行),被.catch()捕获。

4.3 多个.catch()的情况

在Promise链中可以有多个.catch(),但只要有一个.catch()捕获到错误,后续的.catch()就不会再执行。例如:

const promise = new Promise((resolve, reject) => {
    reject('发生错误');
});

promise
   .catch(error => {
        console.log('第一个catch捕获到错误:', error); // 输出:第一个catch捕获到错误: 发生错误
    })
   .catch(error => {
        console.log('第二个catch捕获到错误:', error); // 不会执行
    });

第一个.catch()捕获到错误后,错误就不会再继续传播到第二个.catch()

4.4 在.then()中处理错误

除了使用.catch(),也可以在.then()回调函数中处理错误。.then()方法实际上接受两个回调函数,第一个是成功时的回调,第二个是失败时的回调。例如:

const promise = new Promise((resolve, reject) => {
    reject('发生错误');
});

promise
   .then(
        result => {
            console.log(result);
        },
        error => {
            console.log('在then中捕获到错误:', error); // 输出:在then中捕获到错误: 发生错误
        }
    );

这种方式虽然也能处理错误,但不如使用.catch()简洁,尤其是在链式调用中,.catch()可以统一捕获整个链中的错误,而在.then()中处理错误需要在每个.then()中都添加错误处理回调。

5. 复杂场景下的Promise链式调用与错误处理

5.1 并行执行多个Promise并处理结果

有时我们需要并行执行多个Promise,并在所有Promise都完成后处理它们的结果。这可以通过Promise.all()方法实现。Promise.all()接受一个Promise数组,返回一个新的Promise,只有当所有输入的Promise都成功解决时,这个新Promise才会成功解决,其解决值是一个包含所有输入Promise解决值的数组。如果有任何一个Promise被拒绝,这个新Promise就会被拒绝,且拒绝原因是第一个被拒绝的Promise的原因。

例如,我们有两个异步任务,获取用户信息和获取用户权限:

const getUserInfo = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('用户信息');
        }, 1000);
    });
};

const getUserPermissions = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('用户权限');
        }, 1500);
    });
};

Promise.all([getUserInfo(), getUserPermissions()])
   .then(([info, permissions]) => {
        console.log('用户信息:', info); // 输出:用户信息: 用户信息
        console.log('用户权限:', permissions); // 输出:用户权限: 用户权限
    })
   .catch(error => {
        console.log('发生错误:', error);
    });

在这个例子中,getUserInfo()getUserPermissions()并行执行,Promise.all()等待它们都完成后,将结果传递给.then()回调函数。

5.2 处理部分Promise失败的情况

在某些场景下,即使有部分Promise失败,我们也希望继续处理其他成功的Promise结果。这可以通过Promise.allSettled()方法实现。Promise.allSettled()接受一个Promise数组,返回一个新的Promise,当所有输入的Promise都已经被解决(无论是成功还是失败)时,这个新Promise才会被解决,其解决值是一个包含每个Promise状态和结果(或错误)的对象数组。

例如:

const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('成功1');
    }, 1000);
});

const promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('失败2');
    }, 1500);
});

const promise3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('成功3');
    }, 2000);
});

Promise.allSettled([promise1, promise2, promise3])
   .then(results => {
        results.forEach((result, index) => {
            if (result.status === 'fulfilled') {
                console.log(`Promise ${index + 1} 成功:`, result.value);
            } else {
                console.log(`Promise ${index + 1} 失败:`, result.reason);
            }
        });
    });

上述代码中,Promise.allSettled()等待所有Promise都完成,无论成功或失败,然后我们可以根据每个结果的status属性分别处理成功和失败的情况。

5.3 竞争多个Promise

Promise.race()方法接受一个Promise数组,返回一个新的Promise,这个新Promise会在第一个输入的Promise被解决(无论是成功还是失败)时就被解决,其解决值或拒绝原因就是第一个被解决的Promise的解决值或拒绝原因。

例如,我们有两个异步任务,一个任务可能很快完成,另一个可能需要较长时间:

const fastTask = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('快速任务完成');
        }, 1000);
    });
};

const slowTask = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('慢速任务完成');
        }, 3000);
    });
};

Promise.race([fastTask(), slowTask()])
   .then(result => {
        console.log('最先完成的任务结果:', result); // 输出:最先完成的任务结果: 快速任务完成
    })
   .catch(error => {
        console.log('发生错误:', error);
    });

在这个例子中,fastTask()更快完成,所以Promise.race()返回的Promise会以fastTask()的解决值被解决。

6. 结合async/await的Promise链式调用与错误处理

ES2017引入的async/await语法是基于Promise的,它提供了一种更简洁、更同步化的方式来处理异步操作。async函数总是返回一个Promise,await只能在async函数内部使用,它会暂停async函数的执行,等待Promise被解决后再继续执行。

6.1 使用async/await进行链式调用

例如,我们将之前的Promise链式调用用async/await重写:

const fetchData = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('初始数据');
        }, 1000);
    });
};

const processData1 = data => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data +'经过处理1');
        }, 1000);
    });
};

const processData2 = data => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data +'经过处理2');
        }, 1000);
    });
};

const main = async () => {
    const data = await fetchData();
    const newData = await processData1(data);
    const finalData = await processData2(newData);
    console.log(finalData); // 输出:初始数据 经过处理1 经过处理2
};

main();

在这个例子中,main函数是一个async函数,通过await依次等待每个异步操作完成,代码看起来更像同步代码,可读性更高。

6.2 async/await中的错误处理

async/await中,可以使用传统的try...catch块来捕获错误。例如:

const fetchData = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('发生错误');
        }, 1000);
    });
};

const main = async () => {
    try {
        const data = await fetchData();
        console.log(data);
    } catch (error) {
        console.log('捕获到错误:', error); // 输出:捕获到错误: 发生错误
    }
};

main();

在这个例子中,fetchData()返回的Promise被拒绝,await暂停执行,错误被try...catch块捕获并处理。

7. 最佳实践与常见问题

7.1 保持链式调用的简洁性

在编写Promise链式调用时,尽量保持每个.then()回调函数的逻辑简洁。如果某个.then()回调函数逻辑过于复杂,可以考虑将其封装成一个独立的函数,这样不仅可以提高代码的可读性,也便于维护。

例如,避免这样复杂的.then()回调:

const promise = new Promise((resolve, reject) => {
    resolve('初始值');
});

promise
   .then(result => {
        // 复杂的处理逻辑
        let processedResult = result +'经过一系列复杂处理';
        // 更多复杂操作
        return processedResult;
    })
   .then(newResult => {
        console.log(newResult);
    });

而是将复杂逻辑封装成函数:

const processData = result => {
    let processedResult = result +'经过一系列复杂处理';
    // 更多复杂操作
    return processedResult;
};

const promise = new Promise((resolve, reject) => {
    resolve('初始值');
});

promise
   .then(processData)
   .then(newResult => {
        console.log(newResult);
    });

7.2 合理使用错误处理

在Promise链式调用中,要确保在合适的位置进行错误处理。不要让错误在链式调用中无限制传播,尽量在可能发生错误的地方附近进行捕获和处理,这样可以更好地定位和解决问题。

同时,要注意错误处理的粒度。如果在一个.catch()中处理了所有可能的错误,可能会掩盖一些特定错误的处理逻辑。例如,在一个涉及网络请求和数据处理的链式调用中,网络请求错误和数据处理错误可能需要不同的处理方式。

7.3 避免内存泄漏

在使用Promise时,尤其是在循环中创建大量Promise时,要注意避免内存泄漏。确保每个Promise都正确地被解决或拒绝,避免未处理的Promise导致内存占用不断增加。

例如,在循环中创建Promise时:

const promises = [];
for (let i = 0; i < 1000; i++) {
    const promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            if (Math.random() > 0.5) {
                resolve(i);
            } else {
                reject(new Error(`错误 ${i}`));
            }
        }, 100);
    });
    promises.push(promise);
}

Promise.all(promises)
   .then(results => {
        console.log(results);
    })
   .catch(error => {
        console.log('发生错误:', error.message);
    });

在这个例子中,每个Promise都在一定时间后被解决或拒绝,避免了潜在的内存泄漏。

7.4 理解Promise的执行顺序

Promise的执行顺序遵循JavaScript的事件循环机制。微任务队列(Promise的.then()回调属于微任务)会在宏任务(如setTimeout的回调)之前执行。这意味着,即使在代码中setTimeout的调用在Promise创建之后,Promise的.then()回调也可能先于setTimeout的回调执行。

例如:

console.log('开始');

const promise = new Promise((resolve, reject) => {
    resolve('Promise已解决');
});

promise.then(result => {
    console.log(result); // 输出:Promise已解决
});

setTimeout(() => {
    console.log('setTimeout回调'); // 输出:setTimeout回调
}, 0);

console.log('结束');

在这个例子中,输出顺序为“开始”、“Promise已解决”、“结束”、“setTimeout回调”。这是因为Promise的.then()回调作为微任务,在当前宏任务(主线程代码执行)结束后,下一个宏任务(setTimeout回调)执行之前被执行。

8. 总结

Promise链式调用和错误处理是JavaScript异步编程中的核心内容。通过合理使用Promise的链式调用,可以将多个异步操作按顺序组织起来,避免回调地狱,提高代码的可读性和可维护性。同时,正确的错误处理机制可以确保程序在面对异步操作失败时能够优雅地处理,避免程序崩溃。

结合async/await语法,使得异步代码看起来更像同步代码,进一步简化了异步编程。在实际开发中,要遵循最佳实践,注意避免常见问题,以编写出高效、健壮的异步JavaScript代码。无论是处理简单的异步任务还是复杂的并发操作,对Promise链式调用和错误处理的深入理解都将是开发者的有力武器。