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

JavaScript中的Promise与异步编程语法

2024-11-053.6k 阅读

异步编程的背景

在JavaScript的世界里,理解异步编程至关重要。JavaScript是单线程语言,这意味着它在同一时间只能执行一个任务。在浏览器环境中,这一特性确保了诸如DOM操作等任务不会被其他任务打断,提供了流畅的用户体验。然而,这种单线程特性也带来了挑战。

想象一下,如果JavaScript代码中存在一个长时间运行的任务,比如读取一个非常大的文件或者进行复杂的计算,在这个任务执行期间,所有其他代码,包括用户交互相关的代码,都必须等待。这会导致界面卡顿,用户体验严重下降。例如:

function longRunningTask() {
    let sum = 0;
    for (let i = 0; i < 1000000000; i++) {
        sum += i;
    }
    return sum;
}
console.log('开始任务');
const result = longRunningTask();
console.log('任务完成,结果是:', result);

在上述代码中,longRunningTask函数执行了一个非常耗时的循环计算。在这个函数执行期间,后续的代码无法执行,浏览器的界面也会处于假死状态。

为了解决这个问题,JavaScript引入了异步编程的概念。异步操作允许JavaScript在等待某个操作完成(比如网络请求、文件读取等)的同时,继续执行其他代码,从而避免阻塞主线程,保持应用程序的响应性。

Promise的诞生与概念

在Promise出现之前,JavaScript中处理异步操作主要依赖于回调函数。例如,使用setTimeout模拟一个异步任务:

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

这里,setTimeout接受一个回调函数作为参数,在指定的延迟时间(1000毫秒)后执行这个回调函数。然而,当有多个异步操作相互依赖时,回调函数会变得非常复杂,形成所谓的“回调地狱”。

比如,假设有三个异步任务,每个任务都依赖前一个任务的结果:

function asyncTask1(callback) {
    setTimeout(() => {
        const result1 = '任务1的结果';
        callback(result1);
    }, 1000);
}
function asyncTask2(result1, callback) {
    setTimeout(() => {
        const result2 = result1 + ',经过任务2处理';
        callback(result2);
    }, 1000);
}
function asyncTask3(result2, callback) {
    setTimeout(() => {
        const result3 = result2 + ',经过任务3处理';
        callback(result3);
    }, 1000);
}
asyncTask1((result1) => {
    asyncTask2(result1, (result2) => {
        asyncTask3(result2, (result3) => {
            console.log(result3);
        });
    });
});

这种层层嵌套的回调函数不仅代码可读性差,而且维护和调试都非常困难。

Promise的出现就是为了解决“回调地狱”的问题。Promise是一个代表异步操作最终完成(或失败)及其结果值的对象。它有三种状态:

  1. Pending(进行中):初始状态,既不是成功,也不是失败状态。
  2. Fulfilled(已成功):意味着操作成功完成,Promise对象会携带一个成功的值。
  3. Rejected(已失败):意味着操作失败,Promise对象会携带一个失败的原因。

一旦Promise的状态从Pending转变为FulfilledRejected,就称为已敲定(settled),状态就不会再改变。

创建Promise对象

创建Promise对象非常简单,使用new Promise()构造函数,它接受一个执行器函数作为参数。执行器函数有两个参数,分别是resolverejectresolve用于将Promise状态变为Fulfilledreject用于将Promise状态变为Rejected。例如:

const myPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        const success = true;
        if (success) {
            resolve('Promise成功');
        } else {
            reject('Promise失败');
        }
    }, 1000);
});

在上述代码中,myPromise就是一个Promise对象。通过setTimeout模拟一个异步操作,1秒后根据success的值决定是调用resolve还是reject

处理Promise的结果

Promise对象提供了.then()方法来处理Promise成功或失败的结果。.then()方法接受两个回调函数作为参数,第一个回调函数处理成功的情况,第二个回调函数(可选)处理失败的情况。例如:

const myPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        const success = true;
        if (success) {
            resolve('Promise成功');
        } else {
            reject('Promise失败');
        }
    }, 1000);
});
myPromise.then((result) => {
    console.log(result); // 输出:Promise成功
}, (error) => {
    console.error(error);
});

如果Promise成功,then方法中的第一个回调函数会被调用,传入成功的值;如果Promise失败,then方法中的第二个回调函数(如果提供)会被调用,传入失败的原因。

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

const myPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        const success = false;
        if (success) {
            resolve('Promise成功');
        } else {
            reject('Promise失败');
        }
    }, 1000);
});
myPromise.then((result) => {
    console.log(result);
}).catch((error) => {
    console.error(error); // 输出:Promise失败
});

使用.catch()可以使代码更加简洁,尤其是在处理多个Promise操作组成的链时。

Promise链式调用

Promise的强大之处在于可以进行链式调用。当.then()方法被调用并返回一个新的Promise对象时,就可以继续在这个新的Promise对象上调用.then()方法,形成链式调用。这使得处理多个相互依赖的异步操作变得更加清晰和简洁。

例如,重新实现前面提到的三个相互依赖的异步任务:

function asyncTask1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            const result1 = '任务1的结果';
            resolve(result1);
        }, 1000);
    });
}
function asyncTask2(result1) {
    return new Promise((resolve) => {
        setTimeout(() => {
            const result2 = result1 + ',经过任务2处理';
            resolve(result2);
        }, 1000);
    });
}
function asyncTask3(result2) {
    return new Promise((resolve) => {
        setTimeout(() => {
            const result3 = result2 + ',经过任务3处理';
            resolve(result3);
        }, 1000);
    });
}
asyncTask1()
   .then(asyncTask2)
   .then(asyncTask3)
   .then((result3) => {
        console.log(result3); // 输出:任务1的结果,经过任务2处理,经过任务3处理
    })
   .catch((error) => {
        console.error(error);
    });

在上述代码中,asyncTask1返回一个Promise对象,当这个Promise成功时,asyncTask2会被调用,并传入asyncTask1的成功结果。asyncTask2也返回一个Promise对象,以此类推。这种链式调用方式使得代码逻辑更加清晰,避免了回调地狱。

Promise.all()方法

Promise.all()方法用于将多个Promise实例,包装成一个新的Promise实例。新的Promise实例在所有传入的Promise实例都成功时才会成功,只要有一个Promise实例失败,新的Promise实例就会失败。

它接受一个Promise对象数组作为参数,返回一个新的Promise对象。例如:

const promise1 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise1成功');
    }, 1000);
});
const promise2 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise2成功');
    }, 2000);
});
const promise3 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise3成功');
    }, 3000);
});
Promise.all([promise1, promise2, promise3])
   .then((results) => {
        console.log(results); // 输出:['Promise1成功', 'Promise2成功', 'Promise3成功']
    })
   .catch((error) => {
        console.error(error);
    });

在上述代码中,Promise.all([promise1, promise2, promise3])返回的新Promise对象会在promise1promise2promise3都成功时才成功,成功的值是一个包含所有Promise成功值的数组。

如果有一个Promise失败,比如:

const promise1 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise1成功');
    }, 1000);
});
const promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('Promise2失败');
    }, 2000);
});
const promise3 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise3成功');
    }, 3000);
});
Promise.all([promise1, promise2, promise3])
   .then((results) => {
        console.log(results);
    })
   .catch((error) => {
        console.error(error); // 输出:Promise2失败
    });

此时,Promise.all返回的Promise会立即失败,失败原因是promise2的失败原因。

Promise.race()方法

Promise.race()方法同样接受一个Promise对象数组作为参数,返回一个新的Promise对象。但与Promise.all不同的是,Promise.race返回的Promise会在第一个完成(无论是成功还是失败)的Promise完成时就完成,其结果就是第一个完成的Promise的结果。

例如:

const promise1 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise1成功');
    }, 3000);
});
const promise2 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise2成功');
    }, 1000);
});
const promise3 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise3成功');
    }, 2000);
});
Promise.race([promise1, promise2, promise3])
   .then((result) => {
        console.log(result); // 输出:Promise2成功
    })
   .catch((error) => {
        console.error(error);
    });

在上述代码中,promise2是最先完成的Promise,所以Promise.race返回的Promise的结果就是promise2的结果。

如果第一个完成的Promise是失败的,比如:

const promise1 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise1成功');
    }, 3000);
});
const promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('Promise2失败');
    }, 1000);
});
const promise3 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise3成功');
    }, 2000);
});
Promise.race([promise1, promise2, promise3])
   .then((result) => {
        console.log(result);
    })
   .catch((error) => {
        console.error(error); // 输出:Promise2失败
    });

此时,Promise.race返回的Promise会因为promise2的失败而失败。

Promise.resolve()和Promise.reject()方法

Promise.resolve()方法用于将现有值转换为已解决状态(Fulfilled)的Promise对象。它可以接受任何值作为参数,如果参数本身就是一个Promise对象,则直接返回该Promise对象;如果参数不是Promise对象,则返回一个新的已解决状态的Promise对象,其值为传入的参数。

例如:

const value = '普通值';
const resolvedPromise = Promise.resolve(value);
resolvedPromise.then((result) => {
    console.log(result); // 输出:普通值
});

在上述代码中,Promise.resolve(value)返回一个已解决状态的Promise对象,其值为value

Promise.reject()方法则用于创建一个已拒绝状态(Rejected)的Promise对象,它接受一个参数作为拒绝的原因。例如:

const errorPromise = Promise.reject('拒绝原因');
errorPromise.catch((error) => {
    console.error(error); // 输出:拒绝原因
});

这两个方法在处理异步操作的初始化或转换时非常有用。

异步函数(async/await)

虽然Promise已经极大地改善了异步编程体验,但JavaScript在ES2017中又引入了async/await语法,进一步简化了异步代码的书写。async函数是一种异步函数,它返回一个Promise对象。如果async函数的返回值不是Promise对象,JavaScript会自动将其包装成一个已解决状态的Promise对象。

例如:

async function asyncFunction() {
    return '异步函数的返回值';
}
asyncFunction().then((result) => {
    console.log(result); // 输出:异步函数的返回值
});

在上述代码中,asyncFunction是一个async函数,它返回一个已解决状态的Promise对象,值为'异步函数的返回值'

await关键字只能在async函数内部使用,它用于暂停async函数的执行,等待一个Promise对象解决(FulfilledRejected),然后恢复async函数的执行,并返回Promise对象解决的值。

例如,使用async/await重新实现前面的三个相互依赖的异步任务:

function asyncTask1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            const result1 = '任务1的结果';
            resolve(result1);
        }, 1000);
    });
}
function asyncTask2(result1) {
    return new Promise((resolve) => {
        setTimeout(() => {
            const result2 = result1 + ',经过任务2处理';
            resolve(result2);
        }, 1000);
    });
}
function asyncTask3(result2) {
    return new Promise((resolve) => {
        setTimeout(() => {
            const result3 = result2 + ',经过任务3处理';
            resolve(result3);
        }, 1000);
    });
}
async function main() {
    try {
        const result1 = await asyncTask1();
        const result2 = await asyncTask2(result1);
        const result3 = await asyncTask3(result2);
        console.log(result3); // 输出:任务1的结果,经过任务2处理,经过任务3处理
    } catch (error) {
        console.error(error);
    }
}
main();

在上述代码中,main函数是一个async函数,通过await等待每个异步任务完成,并获取其结果。这种方式使得异步代码看起来更像是同步代码,大大提高了代码的可读性。

同时,async/awaittry/catch结合使用,可以方便地捕获异步操作中抛出的错误。如果不使用try/catchasync函数内部抛出的错误会导致未处理的Promise拒绝,可能会导致应用程序出现难以调试的问题。

错误处理与最佳实践

在使用Promise和async/await进行异步编程时,错误处理至关重要。

对于Promise,使用.catch()方法可以捕获Promise链中任何位置抛出的错误。在链式调用中,只要有一个Promise被拒绝,.catch()就会被触发。例如:

function asyncTask1() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('任务1失败');
        }, 1000);
    });
}
function asyncTask2() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('任务2成功');
        }, 1000);
    });
}
asyncTask1()
   .then(asyncTask2)
   .catch((error) => {
        console.error(error); // 输出:任务1失败
    });

在上述代码中,asyncTask1被拒绝,.catch()捕获到错误并输出。

对于async/await,使用try/catch块来捕获错误。例如:

async function main() {
    try {
        const result1 = await asyncTask1();
        const result2 = await asyncTask2(result1);
        console.log(result2);
    } catch (error) {
        console.error(error);
    }
}
main();

这里,try块中执行异步操作,如果任何一个await的Promise被拒绝,catch块会捕获到错误。

另外,在编写异步代码时,遵循以下最佳实践可以提高代码的质量和可维护性:

  1. 保持代码简洁:避免在单个async函数或Promise链中包含过多复杂的逻辑。将复杂的逻辑拆分成多个独立的异步函数或Promise。
  2. 合理命名:为异步函数和Promise命名时,要清晰地表达其功能,这样可以提高代码的可读性。
  3. 处理未处理的Promise拒绝:在Node.js环境中,可以监听unhandledRejection事件来捕获未处理的Promise拒绝,及时发现并处理潜在的问题。例如:
process.on('unhandledRejection', (reason, promise) => {
    console.log('未处理的Promise拒绝:', reason);
    console.log('相关Promise:', promise);
});
  1. 测试异步代码:使用测试框架(如Jest、Mocha等)来编写异步测试用例,确保异步代码的正确性。例如,在Jest中测试一个async函数:
async function asyncFunction() {
    return '异步函数的返回值';
}
test('测试异步函数', async () => {
    const result = await asyncFunction();
    expect(result).toBe('异步函数的返回值');
});

通过深入理解Promise和async/await的原理和使用方法,以及遵循错误处理和最佳实践,开发者可以编写出高效、可靠且易于维护的JavaScript异步代码,提升应用程序的性能和用户体验。无论是前端开发中的网络请求、动画处理,还是后端开发中的数据库操作、文件读取等场景,异步编程都是不可或缺的重要技能。