JavaScript中避免回调地狱方法

在使用JavaScript时,为了实现某些逻辑经常会写出层层嵌套回调函数,如果嵌套过多,会极大影响代码可读性和逻辑,这种情况也被成为回调地狱(callback hell)。我们不能一直容忍这种code存在在代码中。

1 什么是回调地狱(callback hell)

sample:

var sayhello = function(callback){
    setTimeout(function(){
        console.log("hello");
        return callback(null);
    },1000);
}
sayhello(function(err){
    console.log("xiaomi");
});
console.log("mobile phone");

这段code执行结果是:

mobile phone
hello
Xiaomi

这样看起来代码并没有什么问题。但是如果我现在利用上面的code 依次打印出xiaomi apple huawei 写出来就是这样的:

var sayhello = function(name, callback){
    setTimeout(function(){
        console.log("hello");
        console.log(name);
        return callback(null);
    },1000);
}
sayhello("xiaomi", function(err){
    sayhello("apple", function(err){
        sayhello("huawei", function(err){
            console.log("end");
        });
    });
});
console.log("mobile phone");

从上面这段code就可以看出来如果回调层层嵌套会带来的问题。看起来太蠢了。

2 如何避免

2.1 解决回调嵌套问题(ES6 promise)

Promise 对象就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。

promise只有两个状态resolve和reject,当它触发任何一个状态后,它会将当前的值缓存起来,并在有回调函数添加进来的时候尝试调用回调函数,如果这个时候还没有触发resolve或者reject,那么回调函数会被缓存,等待调用,如果已经有了状态(resolve或者reject),则立刻调用回调函数。并且所有回调函数在执行后都立即被销毁。

New Promise( function(resolve, reject) {...} /* executor */  );

executor是带有 resolve 和 reject 两个参数的函数 。Promise构造函数执行时立即调用executor 函数, resolve 和 reject 两个函数作为参数传递给executor(executor 函数在Promise构造函数返回新建对象前被调用)。resolve 和 reject 函数被调用时,分别将promise的状态改为fulfilled(完成)或rejected(失败)。executor 内部通常会执行一些异步操作,一旦完成,可以调用resolve函数来将promise状态改成fulfilled,或者在发生错误时将它的状态改为rejected。

var sayhello = function(name){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log("hello");
            console.log(name);
            resolve();
        },1000);
    });
}

sayhello("xiaomi").then(function(){
    console.log('frist');
}).then(function(){
    return sayhello("huawei");
    console.log('second');
}).then(function(){
    console.log('second');
}).then(function(){
    return sayhello("apple");
}).then(function(){
    console.log('end');
}).catch(function(err){
    console.log(err);
})
console.log("mobile phone");

Promise 提供then方法加载回调函数,catch方法捕捉执行过程中抛出的错误。
虽然这样将回调函数的执行和结果比较清晰的分开了,但看着那一堆then,其实并没有将嵌套回调的问题根本上的解决,只是换了一种写法而已。

2.2 ES6 co/yield方案

yield: Generator 函数是协程在 ES6 的实现,而yield是 Generator关键字, 异步操作需要暂停的地方,都用yield语句注明。
co: co 模块是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于 Generator 函数的自动执行。

关于 co/yield详细原理可参考:
http://es6.ruanyifeng.com/#docs/generator-async

2.2.1 什么是Generator 函数?

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即主动交出执行权,暂停执行)。
整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。Generator 函数的执行方法如下。

function* gen() {
     yield console.log("test 1");
     yield console.log("test 2");
     yield console.log("test 3");
    return y;
}

var g = gen();
g.next()//“test 1”
g.next()//“test 2”

Generator 函数不同于普通函数的另一个地方,即执行它不会返回结果,返回的是指针对象。调用指针g的next方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的yield语句,打印”test 1”并暂停,再次调用next会打印”test 2”暂停住,但不会打印”test 3”

Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因

2.2.2 co模块

Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权(即调用next)。
两种方法可以做到这一点。
(1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
(2)Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权,因为resolve后会执行then方法。

co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的yield命令后面,只能是 Thunk 函数或 Promise 对象。如果数组或对象的成员,全部都是 Promise 对象,也可以使用 co。co本身就是Promise 对象。

实质co流程可以理解为:

  • co 检测 传入函数 是否为Generator 。(是,则可以在yield处暂停执行)
  • co得到Generator函数返回指针,并使用next方法开始执行 Generator 。
  • 第一个异步函数(Promise对象)有了结果,调用了resolve,co使用Promise 对象then的方法接管执行权接着调用next 执行Generator剩余部分,循环调用。

2.2.3 使用

Generator 函数前面要加星号(*)
根据语法规范,yield* 的作用是代理 yield 表达式,将需要函数本身产生(yield)的值委托出去。yield* 后面跟一个生成器函数、或其他可迭代的对象(如一个数组、字符串、arguments对象)。yield* 表达式的返回值,就是其后面可迭代对象迭代完毕时的返回值。

var sayhello = function(name, ms){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log("hello");
            console.log(name);
            // if (error) return reject(error);
            resolve("helloworld");
        },ms);
    });
}

var gen = function *(){
    yield sayhello("xiaomi", 2000);
    console.log('frist');
    yield sayhello("huawei", 1000);
    console.log('second');
    yield sayhello("apple", 500);
    console.log('end');
}

console.log("mobile phone");
co(gen());
console.log("mobile end");

2.3 ES7 async/await 方案

async/await是es7的新标准,并且在node7.0中已经得到支持。
它就是 Generator 函数的语法糖,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。可以理解官方对co和Generator 封装方案。

2.3.1 定义

async函数定义如下

async function fn(){
    await sayhello();
    return 0;
}

关键字:async
async关键字用于修饰function,async函数的特征在于调用return返回的并不是一个普通的值,而是一个Promise对象,如果正常return了,则返回Promise.resolve(返回值),如果throw一个异常了,则返回Promise.reject(异常)。

关键字:await
await关键字只能在async函数中才能使用,也就是说你不能在任意地方使用await。await关键字后跟一个promise对象(你想要执行的异步操作),函数执行到await后会退出该函数,直到事件轮询检查到Promise有了状态resolve或reject 才重新执行这个函数后面的内容。

它与直接使用Generator,具有以下优点:

  1. 内置执行器,所以不再需要co模块了
  2. async函数的await命令后面,可以是 Promise对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)
  3. async函数的返回值是 Promise 对象

2.3.2 使用

var sayhello = function(name, ms){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log("hello");
            console.log(name);
         if(name === "huawei")
                return reject(name);
             else
           return resolve("helloworld");

        }, ms);
    });
}

async function test() {
    try {
        console.log('frist');
        await sayhello("xiaomi", 2000);
        console.log('second');
        await sayhello("huawei", 1000);
        console.log('end');
        await sayhello("apple", 500);
    }
    catch (err) {
        console.log('err:'+err);
    }
};

test();

2.3.3 对错误的处理

await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到,而后续的await函数就不会继续执行了。我们应该在将await语句放在try catch代码块中,如果有多个await命令,可以统一放在try…catch结构中,我们通过这种方式对发生的异步错误进行处理。

3 参考链接

https://www.cnblogs.com/kazetotori/p/6043983.html
https://blog.csdn.net/guiyecheng/article/details/71724152
阮一峰ECMAScript 6 入门:
http://es6.ruanyifeng.com/#docs/generator-async
http://es6.ruanyifeng.com/#docs/async

相关推荐
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页