JS 中异步的解决方案

3,690 阅读6分钟
原文链接: webfe.kujiale.com

callback

在 JavaScript 中函数是第一公民,可以做为参数传入函数中执行。所以我们可以把需要异步执行的代码放到回调函数中,然后在异步回调再执行这些代码。

Example

如下定义 delay 函数, callback 参数的类型是一个函数, 该函数会在1秒钟后执行。

function delay(callback) {
    console.log('foo');
    setTimeout(callback, 1000);
}

delay(console.log.bind(console, 'bar')); 

// 控制台立即打印 foo
// 1秒钟后控制台输出 bar

回调函数处理单个异步操作看起来问题不大,但想像这样几个场景。

  • 场景一:假设 delayA, delayB, delayC 都是异步操作, 传入的 callback 参数在完成操作后执行. 操作 done 依赖 delayC 操作的结果, delayC 操作又依赖 delayB, delayB 又依赖 delayA,我们怎么解决?:
function delayA(callback) {
    setTimeout(() => {
        callback(1);
    }, 1000);
}

function delayB(fn, somthingFromA) {
    setTimeout(() => {
        callback(somthingFromA + 1);
    }, 2000);
}

function delayC(fn, somthingFromB) {
    setTimeout(() => {
        callback(somthingFromB + 1);
    }, 3000);
}

function done(result) {
    console.log(result);
}

delayA((feedBackByA) => {
    delayB((feedBackByB) => {
        delayC((feedBackByC) => {
            done(feedBackByC);
        }, feedBackByB);
    }, feedBackByA);
});
// 6 秒后控制台输出 3
  • 场景二:操作 done 依赖 delayA, delayB, delayC 操作的结果, 但 delayA, delayB, delayC 之间没有依赖关系,他们定义如下:
function delayA(callback) {
    setTimeout(() => {
        callback(1);
    }, 1000);
}

function delayB(fn) {
    setTimeout(() => {
        callback(2);
    }, 2000);
}

function delayC(fn) {
    setTimeout(() => {
        callback(3);
    }, 3000);
}

function done(somthingFromA, somthingFromB, somthingFromC) {
    console.log(somthingFromA, somthingFromB, somthingFromC);
}

当然我们可以用场景一同样的解决方案,但多个不相关异步操作同步执行延迟了 done 操作的执行时机。

delayA((feedBackByA) => {
   delayB((feedBackByB) => {
       delayC((feedBackByC) => {
           done(feedBackByA, feedBackByB, feedBackByC);
       });
   });
});
// 6 秒后控制台输出 1, 2, 3

因此需要定义一个辅助函数,这个辅助函数使得多个异步操作同时开始执行,并在最晚回调的异步操作结束时执行回调函数:

/*
* fns: 异步操作函数的数组
* callback: 所有异步操作都完成时的回调
*/
var helper = (fns, callback) => {
    // 定义一个 checkList,标识每个异步操作是否完成
    const checkList = new Array(fns.length).fill(false);
    // 保存每个异步操作返回的参数供 callback 使用
    const parameters = new Array(fns.length);

    // 判断是否所有的异步操作都已完成
    function allCheck() {
        return checkList.reduce((prev, val) => {
            return prev && val;
        }, true);
    }
    
    fns.forEach((fn, index) => {
        fns((feedBack) => {
            parameters[index] = feedBack;
            checkList[index] = true;
            if (allCheck()) {
                callback.apply(null, parameters);
            }
        });
    });
};

helper([delayA, delayB, delayC], done);
// 3 秒后控制台输出 1, 2, 3

再想象场景一和场景二的需求结合起来,代码将会变得嵌套过深不易阅读。综上,过度使用回调函数就会产生传说中的回调地狱,随着异步操作的依赖关系变得复杂,代码变得丑陋而不可维护。

Promise

在 ES6 中引入了 Promise,经常被用来处理异步,我们来看看它是怎么简化我们处理异步的过程的。

对 Promise 不了解的同学可以先看看 developer.mozilla.org/en-US/docs/…

首先我们看看把之前的一个由异步函数转化为一个 Promise:

funcion promisify(fn) {
    return (args) => 
        return new Promise((resolve, reject) => {
            // fn 是一个异步操作,第一参数是回调函数,第二个参数是执行的参数
            fn(resolve, args);
        });
    };
}

那么原来一个使用 callback 作为参数的异步操作如下:

function delay(callback, content) {
    setTimeout(() => callback(content), 1000);
}

就可以被 Promise 化:

const delayPromise = promisify(delay);

delayPromise('foo').then((content) => {
    console.log(content);
});
// 1 秒后控制台输出 foo

看起来虽然和回调函数没有什么区别,但我们再看看上面的场景一,使用 Promise 我们的解决方案会发生什么变化。

promisify(delayA)()
    .then((feedbackFromA) => {
        return promisify(delayB)(feedbackFromA);
    })
    .then((feedbackFromB) => {
        return promisify(delayC)(feedbackFromB);
    })
    .then((feedbackFromC) => {
        done(feedbackFromC);
    });
// 6 秒后控制台输出 3

再看场景二:

Promise.all([
    promisify(delayA)(),
    promisify(delayB)(),
    promisify(delayC)()
]).then(([feedbackFromA, feedbackFromB, feedbackFromC]) => {
    done(feedbackFromA, feedbackFromB, feedbackFromC);
});
// 3 秒后控制台打印 1,2,3

一旦所有异步操作被封装为 Promise 对象,那么显然不会再有回调地狱的问题了。

Generator

Generator 也是 ES6 中引入的概念。对于 Generator 不了解的同学请先查看文档 developer.mozilla.org/en-US/docs/…

首先我们看看一个 Generator 是如何工作的:

function* main() {
    const a = yield 1; // a 为第二次调用 next 时传入的参数
    const b = yield 2 + a; // b 为第三次调用 next 时传入的参数
    yield 3 + b;
    return 4;
}
const gen = main();
gen.next(); // {value: 1, done: false}
gen.next(1); // {value: 3, done: false}
gen.next(5); // {value: 8, done: false}
gen.next(); // {value: 4, done: true}

Generator 又是怎么解决异步的呢?想象我们有一个辅助函数 helper 函数, 可以让下面的 generator 自动执行完毕, 那么以后写异步代码就可以和写同步代码一样方便了。

helper(function* main(args) {
    const feedBackFromA = yield promisify(delayA)(args);
    const feedBackFromB = yield promisify(delayB)(feedBackFromA);
    const feedBackFromC = yield promisify(delayC)(feedBackFromB);
    done(feedBackFromC);
})(realArgs);

这个 helper 函数应满足一下几个条件:

  • 自动执行 generator 直到 generator 的状态为完成
  • yield 关键字后面的 Promise resolve 的值被返回作为下次调用 next 的参数
  • 返回 Promise,使用 generator 实例的返回值作为 resolve 的值

因此,定义 helper 如下:

function helper(genFn) {
    return (...args) => new Promise(resolve, reject) => {
        let gen = genFn(args);
        function next(prev) => {
            const {
                value,
                done
            } = gen.next(prev);
            if(done) {
                return resolve(next.value);
            } else {
                return value.then(next);
            }
        }
        next();
    });
}

那么

helper(main)();
// 6 秒后控制台打印 3

那么场景二的问题可以可以利用这个 helper 这么解决:

 helper(function* main() {
    const [feedBackFromA, feedBackFromB, feedBackFromC] = yield Promise.all([
        promisify(delayA)();
        promisify(delayB)();
        promisify(delayC)();
    ]);
    d(feedBackFromA, feedBackFromB, feedBackFromC);
})();
// 3 秒后控制台打印 1,2,3

感兴趣的人可以自己加工这个 helper ,使得它支持下面这样的语法:

helper(function* main() {
    const [feedBackFromA, feedBackFromB, feedBackFromC] = yield [
        promisify(delayA)();
        promisify(delayB)();
        promisify(delayC)();
    ];
    d(feedBackFromA, feedBackFromB, feedBackFromC);
})();

这里推荐一个好用的三方库 co, 它提供了一个很强大的 helper 实现, 他的 yield 后面可以接受 Array,Promise,Function 等众多类型。

Async Function

ES7 中加入了 async function 的原生支持。

所以写法和上面用 generator 的解决方案很相似,但 await 关键词后面只接受 Promise 对象。
async function 的解决方案:
场景一:

async function main() {
    const feedBackFromA = await promisify(delayA)();
    const feedBackFromB = await promisify(delayB)(feedBackFromA);
    const feedBackFromC = await promisify(delayC)(feedBackFromB);
    d(feedBackFromC);
}

场景二:

async function main() {
    const feedBacks = await Promise.all([
        promisify(delayA)(/* a的参数 */);
        promisify(delayB)(/* b的参数 */);
        promisify(delayC)(/* c的参数 */);
    ]);
    d(feedBacks);
}

错误处理

callback

在各自的回调中处理错误。

a(() => {
    if (err) {
        // handle the error of a
    }
    b(() => {
        if (err) {
            // handle the error of b
        }
        ...
    });
});

Promise

使用 catch 方法获取在整个 Promise 过程的错误统一处理,也可以在 then 方法的第二个参数中各自处理。

promisify(delayA)()
    .then(promisify(delayB), (err) => {
        // handle the error of a
    })
    .catch((err) => {
        // handle all errors
    });

Generator 和 Async Function

写法已经接近同步,可直接使用 try catch 来处理错误。

async function main() {
    try {
        const feedBackFromA = await promisify(delayA)(/* a的参数 */);
        const feedBackFromB = await promisify(delayB)(aRes);
        const feedBackFromC = await promisify(delayC)(feedBackFromB);
        d(feedBackFromC);
    } catch (err) {
        // handle all errors
    }
}
知识共享许可协议
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。