9102,Promise 和 Async 你会真的会用了吗?

2,250 阅读6分钟

没什么,只是希望你取的人是我!     --- 时间旅行者的妻子

前言

遥想当年刚接触 JavaScript 时被回调地狱支配的恐惧,😂😂😂!

解救之道,就在其中啊 --- Promise 和 Async/await

推荐 vscode 插件 Code Runner,可以运行代码片段。学习利器啊。不过是基于 Node 运行环境

Promise

一个 Promise有以下几种状态: 状态一旦改变,便不能更改。

pending: 初始状态,既不是成功,也不是失败状态。等待状态转变

fulfilled: 意味着操作成功完成。then 处理逻辑

rejected: 意味着操作失败。可以在 catch 处理逻辑

每一个 then 都会返回一个新的 Promise 实例

Api

// 创建一个 fulfilled 状态的 Promise 实例。
Promise.resolve();
// 创建一个 rejected 状态的 Promise 实例。
Promise.reject();
// then 的第一个参数 resolve {Function} 处理 fulfilled 状态,
// 第二参数rejected 处理 rejected 状态。
// 如果 then 处理了 rejected  状态,那么返回的 Promise 实例为 
// fulfilled,否则会返回的实例为 rejected。
Promise.prototype.then(resolve,rejected);
// 拦截 rejected  状态执行,并返回一个 fulfilled 状态的 Promise 实例
Promise.prototype.catch();
// 不管什么状态最后都会执行
Promise.prototype.finally();
// 执行多个 promise 都执行玩了并将结果返回 Array [promiseRet,promiseRet]
Promise.all([promise,promise]);
// 返回昀行最快的那个 promise 的结果 
Promise.race([promise,promise]);

开胃小菜

  • demo1
const demo1 = function _demo1(num) {
    return new Promise((resolve, reject) => {
        if (num % 2 === 0) {
            // pending 状态 到 fulfilled
            resolve(num);
            return;
        }
        // pending 状态 到 rejected 
        reject(num);
    });
};
demo1(2)
    .then(
        // 处理 fulfilled 状态
        resolveData => {
            console.log('resolve-', resolveData);
        },
        // 处理 rejected 状态,之后的 catch 不会被触发。
        rejectData => {
            console.log('reject', rejectData);
        }
    )
    .then(data => console.log('依然可以调用哦,只是 data undefined', data));
    
    // 2 是偶数,状态转变 fulfilled 执行对应回调。
    // resolve- 2
    // 依然可以调用哦,只是 data undefined undefined
  • demo2
const demo1 = function _demo1(num) {
    return new Promise((resolve, reject) => {
        if (num % 2 === 0) {
            resolve(num);
            return;
        }
        reject(num);
    });
};
demo1(1)
    .then(
        resolveData => {
            console.log('resolve-', resolveData);
        },
        rejectData => {
            console.log('reject', rejectData);
        }
    )
    .then(data => console.log('依然可以调用哦,只是 data undefined-', data))
    .catch(error => console.log('catch:', error));
// 运行结果 1 为偶数,then 处理了对应 rejected,状态不会传递下去,不会执行 catch
// reject 1
//依然可以调用哦,只是 data undefined- undefined
  • demo3
const demo1 = function _demo1(num) {
    return new Promise((resolve, reject) => {
        if (num % 2 === 0) {
            resolve(num);
            return;
        }
        reject(num);
    });
};
demo1(3)
    .then(resolveData => {
        console.log('resolve-', resolveData);
    })
    .then(data => console.log('依然可以调用哦,只是 data undefined-', data))
    .catch(error => {
        console.log('catch-', error);
        return '我处理 rejected 了';
    })
    // 验证 catch 拦截了 rejected ,并返回了 fulfilled 实例
    .then(data => console.log('catch 处理之后的 then-', data));
    // 3 为奇数,状态转变为 rejected ,然后 then 都没有处理其状态, rejected  一直被传递到 catch  处理。
    // catch- 3
    // catch 处理之后的 then- 我处理 rejected 了

推荐 then 只处理 fulfilled 状态,catch 拦截 rejected 状态处理逻辑

promise.then(resolve=>{}).then(resolve=>{}).then(resolve=>{}).catch(error=>{});

Promise 封装 JQuery-Ajax

class AjaxUtil {
    static get(url, otherSetting = {}) {
        return new Promise((resolve, reject) => {
            const ajaxGetSetting = {
                url: url,
                type: 'GET',
                dataType: 'json',
                success: data => {
                    resolve(data);
                },
                error: error => {
                    reject(error);
                }
            };
            $.ajax(Object.assign(otherSetting, ajaxGetSetting));
        });
    }
}
// 不用再传 callback ,爽歪歪了吧。
AjaxUtil.get('/api/videos').then(data => {
    document.getElementById('div').innerHTML = JSON.stringify(data);
});

Promise.all()

/**
 * const promise=Promise.all([promise,promise...]);
 * @description: Promise.all 返回 Promise 实例,数组里面有一个状态是 reject 
 * 返回的实例的状态为 reject
 */
  • Demo
const sleep = function _sleep(ms) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve();
        }, ms);
    });
};
console.log('start');
const sleep1 = sleep(1000).then(() => {
    console.log('休息一秒');
    return '跟他説抱歉,我要去找个女孩';
});
const sleep2 = sleep(2000).then(() => {
    console.log('休息两秒');
    return '忘记应该忘记的,面对未来一切可以出现的。';
});
const sleep3 = sleep(3000).then(() => {
    console.log('休息三秒');
    return '孩子,生活不是靠书本和想当然来的,是靠心去体会的。';
});
const sleep4 = sleep(4000).then(() => {
    console.log('休息四秒');
    return '总有人有不好的时候,但这也会让你回忆起从前不曾在意的美好。';
});
const ret2 = Promise.all([sleep1, sleep2, sleep3, sleep4]);
ret2.then(data => {
    // data 为 所有 promise resolve 的 结果组成的数组
    console.log(data);
});
console.log('end');
// 执行结果为
start
end
休息一秒
休息两秒
休息三秒
休息四秒
[
  '跟他説抱歉,我要去找个女孩',
  '忘记应该忘记的,面对未来一切可以出现的。',
  '孩子,生活不是靠书本和想当然来的,是靠心去体会的。',
  '总有人有不好的时候,但这也会让你回忆起从前不曾在意的美好。'
]

Promise.all 可以在想拿到多个异步事件执行结果的然后进行后续逻辑处理很方便。但是 Promise.all 不会阻塞后续代码的执行。结合 async/await 用同步编码的思维编写代码很舒服。

Demo2

验证 Promise.all 参数中的 promise 有一个状态是 rejected 返回的 promise 是 rejected

const sleep = function _sleep(ms) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve();
        }, ms);
    });
};
const sleep1 = sleep(1000).then(() => {
    console.log('休息一秒');
    return '跟他説抱歉,我要去找个女孩';
});
const sleep2 = sleep(2000).then(() => {
    console.log('休息两秒');
    return '忘记应该忘记的,面对未来一切可以出现的。';
});
const sleep3 = sleep(3000).then(() => {
    console.log('休息三秒');
    return '孩子,生活不是靠书本和想当然来的,是靠心去体会的。';
});
const sleep4 = sleep(4000).then(() => {
    console.log('休息四秒');
    return '总有人有不好的时候,但这也会让你回忆起从前不曾在意的美好。';
});
const sleep5 = sleep(5000).then(() => {
    console.log('休息五秒');
    throw new Error('模拟测试失败情况');
});
const ret2 = Promise.all([sleep1, sleep2, sleep3, sleep4, sleep5]);
ret2.then(data => {
    console.log(data);
    // 执行 catch
}).catch(error => {
    console.log(error.message);
});
休息一秒
休息两秒
休息三秒
休息四秒
休息五秒
模拟测试失败情况

Promise.race([promise,promise])

最先改变状态,就返回那个 promise

const sleep = function _sleep(ms) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve();
        }, ms);
    });
};
const sleep2 = sleep(2000).then(() => {
    console.log('休息两秒');
    return '你可以了解世间万物,但追根溯源的唯一途径便是亲身尝试。';
});
const sleep3 = sleep(3000).then(() => {
    console.log('休息三秒');
    return '人终究是一个孤独的个体,纵使你已经拥有了他人的怀抱,这其中,也许人与人之间唯一不同的,只是你把孤独藏在哪里。';
});
const sleep4 = sleep(4000).then(() => {
    console.log('休息四秒');
    return '躲避和不信任,是因为曾经被应该爱我的人遗弃。';
});
const ret = Promise.race([sleep2, sleep3, sleep4]);
ret.then(data => {
    console.log(data);
});
休息两秒
你可以了解世间万物,但追根溯源的唯一途径便是亲身尝试。
休息三秒
休息四秒

Async/await

async/await ,一直用一直爽。

async 函数返回一个 Promise实例。遇到 await 会代码添加到微任务队列,然后从函数中返回继续执行后续代码。

async/await 用于 Function 上。并且 await 不能单独出现,需要和 async 一起。

const async = async function _async() {
    // 执行遇到 await 将任务添加到微任务队列,然后返回。继续执行后续代码
    const awaitRet = await Promise.resolve(1).then(data => {
        console.log('resolve-then');
        return data;
    });
    console.log(awaitRet);
    console.log('await 后面代码');
};
console.log('start');
async();
console.log('end');
start
end
resolve-then
1
await 后面代码
    const awaitRet = await Promise.resolve(1).then(data => {
        console.log('resolve-then');
        return data;
    });
    console.log(awaitRet);
    console.log('await 后面代码');
    // 上面代码可以简单理解为

Promise.resolve()
    .then(() => {
        const awaitRet = await Promise.resolve(1).then(data => {
        console.log('resolve-then');
        return data;
        });
    })
    // await 后面代码添加到微任务队列
    .then(() => {
        console.log(awaitRet);
        console.log('await 后面代码');
    });

Demo1

const awaitFunc = function _awaitFunc() {
    return Promise.resolve('awaitFunc').then(data => {
        console.log(data);
        return 'awaitFunc-then-return-data';
    });
};
const async = async function _async() {
    // 等待 await 执行完之后,再将后续的代码放入微任务队列
    await awaitFunc().then(data => {
        console.log(data);
    });
    console.log('awaitFunc 执行完在打印');
};
async();
awaitFunc
awaitFunc-then-return-data
awaitFunc 执行完在打印

Demo2

await 阻塞的后续代码放入微任务队列?真的是这样吗?来个 demo 验证我的想法。

const awaitFunc = function _awaitFunc() {
    return Promise.resolve('awaitFunc').then(data => {
        console.log(data);
        return 'awaitFunc-then-return-data';
    });
};
const async = async function _async() {
    // 等待 await 执行完之后,再将后续的代码放入微任务队列
    setTimeout(() => {
        console.log('验证加入了微任务队列---1');
    }, 0);
    await awaitFunc().then(data => {
        console.log(data);
        setTimeout(() => {
            console.log('验证加入了微任务队列---2');
        }, 0);
    });
    console.log('awaitFunc 执行完在打印');
};
async();

awaitFunc
awaitFunc-then-return-data
awaitFunc 执行完在打印
验证加入了微任务队列---1
验证加入了微任务队列---2

若 console.log('awaitFunc 执行完在打印') 被放入了宏任务队列,那么这段代码不会比 setTimeout 先执行。因此 await 阻塞的后续代码,放入到了当前事件循环的微任务队列。所有的微任务也会在本次事件循环被执行完毕。

Promise&async/await 使用场景

用同步的思维编写异步的代码,确实比较符合我们的思维习惯。

const sleep = function _sleep(ms, data) {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(`休息了 ${ms} 秒`);
            resolve(data);
        }, ms);
    });
};
console.log('start');

const data = [
    '跟他説抱歉,我要去找个女孩',
    '忘记应该忘记的,面对未来一切可以出现的。',
    '孩子,生活不是靠书本和想当然来的,是靠心去体会的。',
    '总有人有不好的时候,但这也会让你回忆起从前不曾在意的美好。'
];
const asyncFunc = function _async(data) {
    console.log('asyncFunc-start');
    const promises = [];
    for (let index = 0; index < data.length; index++) {
        promises.push(sleep(index, data[index]));
    }
    const ret = Promise.all(promises);
    console.log('处理完在开始');
    console.log('asyncFunc-end');
    return ret;
};
const retPromise = asyncFunc(data);
retPromise.then(data => {
    console.log(data);
});
console.log('end');

start
asyncFunc-start
处理完在开始
asyncFunc-end
end
休息了 0 秒
休息了 1 秒
休息了 2 秒
休息了 3 秒
[
  '跟他説抱歉,我要去找个女孩',
  '忘记应该忘记的,面对未来一切可以出现的。',
  '孩子,生活不是靠书本和想当然来的,是靠心去体会的。',
  '总有人有不好的时候,但这也会让你回忆起从前不曾在意的美好。'
]

上述代码呢我想让 asyncFunc 里面顺序执行,先处理 Promise.all ,再继续 await 后面的代码。

  • 使用 async/await 改写。
const sleep = function _sleep(ms, data) {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(`休息了 ${ms} 秒`);
            resolve(data);
        }, ms);
    });
};
console.log('start');

const data = [
    '跟他説抱歉,我要去找个女孩',
    '忘记应该忘记的,面对未来一切可以出现的。',
    '孩子,生活不是靠书本和想当然来的,是靠心去体会的。',
    '总有人有不好的时候,但这也会让你回忆起从前不曾在意的美好。'
];
const asyncFunc = async function _async(data) {
    console.log('asyncFunc-start');
    const promises = [];
    for (let index = 0; index < data.length; index++) {
        promises.push(sleep(index, data[index]));
    }
    const ret = await Promise.all(promises);
    console.log('处理完在开始');
    console.log('asyncFunc-end');
    return ret;
};
const retPromise = asyncFunc(data);
retPromise.then(data => {
    console.log(data);
});
console.log('end');

start
asyncFunc-start
end
休息了 0 秒
休息了 1 秒
休息了 2 秒
休息了 3 秒
处理完在开始
asyncFunc-end
[
  '跟他説抱歉,我要去找个女孩',
  '忘记应该忘记的,面对未来一切可以出现的。',
  '孩子,生活不是靠书本和想当然来的,是靠心去体会的。',
  '总有人有不好的时候,但这也会让你回忆起从前不曾在意的美好。'
]

可以看到 await 后面的代码需要等到 Promise.all 微任务全部执行完毕才可以开始。注意那个 end 打印时机,其实 async/await 也是基于事件循环,并不是阻塞代码执行。不然那个 end 应该最后打印才是。

在循环中使用 async/await

forof,forin,fori 循环可以感知 await

const data = [1, 2, 3];
const async = async function _async(data) {
    for (const index in data) {
        const item = data[index];
        const ret = await Promise.resolve(item)
            .then(data => {
                console.log('第一个 then');
                return data;
            })
            .then(data => {
                console.log('第二个 then');
                return data;
            });
        console.log('await 返回值', ret);
        console.log('await 之后的代码');
    }
};
async(data);

// fori
const data = [1, 2, 3];
const async = async function _async(data) {
    for (let index = 0; index < data.length; index++) {
        const item = data[index];
        await Promise.resolve(item)
            .then(data => {
                console.log('第一个 then');
                return data;
            })
            .then(data => {
                console.log('第二个 then');
                return data;
            });
        console.log('await 之后的代码');
    }
};
async(data);

// forof
const data = [1, 2, 3];
const async = async function _async(data) {
    for (const item of data) {
        await Promise.resolve(item)
            .then(data => {
                console.log('第一个 then');
                return data;
            })
            .then(data => {
                console.log('第二个 then');
                return data;
            });
        console.log('await 之后的代码');
    }
};
async(data);
第一个 then
第二个 then
await 之后的代码
第一个 then
第二个 then
await 之后的代码
第一个 then
第二个 then
await 之后的代码

forin,forof,fori 都能感知 await

forEach 不能感知 await

const data = [1, 2, 3];
const async = async function _async(data) {
    data.forEach(async item => {
        const ret = await Promise.resolve(item)
            .then(data => {
                console.log('第一个 then');
                return data;
            })
            .then(data => {
                console.log('第二个 then');
                return data;
            });
        console.log('await 返回值', ret);
        console.log('await 之后的代码');
    });
};
async(data);
第一个 then
第一个 then
第一个 then
第二个 then
第二个 then
第二个 then
await 返回值 1
await 之后的代码
await 返回值 2
await 之后的代码
await 返回值 3
await 之后的代码