资讯专栏INFORMATION COLUMN

[前端工坊]浅谈Web编程中的异步调用的发展演变

qpwoeiru96 / 3466人阅读

摘要:三即生成器,它是生成器函数返回的一个对象,是中提供的一种异步编程解决方案而生成器函数有两个特征,一是函数名前带星号,二是内部执行语句前有关键字调用一个生成器函数并不会马上执行它里面的语句,而是返回一个这个生成器的迭代器对象。

文章来自微信公众号:前端工坊(fe_workshop),不定期更新有趣、好玩的前端相关原创技术文章。 如果喜欢,请关注公众号:前端工坊
版权归微信公众号所有,转载请注明出处。
作者:京东金融-移动研发部-前端开发工程师 张恒

作为Web工程师,相信大家在开发项目的过程中,都存在与服务器端的通信,如登录验证、获取用户信息、获取应用数据等都需要通过调用后端的API来进行操作,而实现这一操作的正是异步调用;
这篇文章旨在通过一些异步调用的概念和相应的代码演示,尽量详细地介绍异步调用的实现、各种异步编程的使用方式和区别,以及他们的发展演变;

一、AJAX

在Web应用的开发过程中,为了实现良好的交互体验,我们都会使用 ajax 的方式与后端通信,实现无刷新数据提取和快速展现,极大地提升了用户体验;
ajax 的全称是Asynchronous JavaScript and XML,Asynchronous 即异步,它有别于传统web开发中采用的同步的方式。
ajax 的原理简单来说就是通过 XmlHttpRequest 对象来向服务器发异步请求,从服务器获得数据,然后用JavaScript来操作DOM而更新页面,这其中 XMLHttpRequestajax 的核心机制,
通过这种异步技术,JavaScript可以及时向服务器提出请求和处理响应,而不阻塞用户,从而达到无刷新页面的效果。

相信广大的Web工程师们对此已经耳熟能详,我就不在这里细讲了,如果你是刚入行前端并且不了解此概念,可以移步ajax;
但是必须提到的是XmlHttpRequest 对象有一个属性 onreadystatechange 用于当异步请求状态改变时触发事件执行后续动作,
这也就是本文要讲的异步调与回调处理;对于单个的异步请求及其回调结果处理实际上没太大问题,但当碰到某些复杂场景,需要多次异步调用接口,并且后一个的调用需要前一个异步调用的返回结果作为参数时,
由于是异步形式,不能像同步编程那样编写代码,咱们就不得不嵌套编写,而当嵌套层过多就会出现难以阅读和维护代码。

举个例子,在一个Web App中,需要获取用户的某篇博客的所有跟帖,这时我们就需要有如下的APIs;

1、获取用户会话的token(也可能是一开始进入博客通过登录返回的)

{ 
    status:"success",  
    data:{
        token: "******"
    }
}

2、通过token获取用户详细信息

{
    status:"success",  
    data:{
        userInfo: {
            id: 10001,
            name:"test",
            email:"test@test.com"
        }
    }
}

3、通过userId获取用户文章列表

{
  status:"success",  
  data:[
    {
        id: 1,
        title:"my first article",
        content:"a long content will be here...",
        date:"2018-02-28"
    },
    {
        id: 2,
        title:"my second article",
        content:"a long content will be here...",
        date:"2018-02-28"
    },
  ]
}

4、通过博客id获取所有用户评论

{
 status:"success",  
 data:[
   {
       id: 1,
       userId:,10005,
       comment:"it"s an great article...",
       date:"2018-02-28"
   },
   {
       id: 2,
       userId:,10008,
       comment:"it"s very useful article for me, thanks blogger...",
       date:"2018-03-01"
   },
 ]
}

接下来我们就通过code去实现这样一个逻辑,首定义一个异步调用的公用方法:

function ajaxRequest(url,successHandler){
    var xhr;
    if (window.XmlHttpRequest) {
        xhr = new XmlHttpRequest();
    }else if (window.ActiveXObject) {
        try {
          xhr = new ActiveXObject("Microsoft.XMLHTTP");
        }
        catch (e) {
          try {
              xhr = new ActiveXObject("msxml2.XMLHTTP");
          }
          catch (ex) { }
        }
    }
    
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
            if (xhr.status == 200) {
                successHandler(xhr.responseText);
            }
        }
    }
        
    xhr.open("GET", url);
    xhr.send();
}

使用ajax实现获取用户评论的逻辑则如下所示:

/*获取评论*/
ajaxRequest("your-host/api/get-token",function(res1){
    var token = res1.data.token;
    ajaxRequest("your-host/api/get-user?token="+token,function(res2){
        var userId = res2.data.userInfo.id;
        ajaxRequest("your-host/api/get-article?userId="+userId,function(res3){
            var artcleId=res3.data[0].id;
            ajaxRequest("your-host/api/get-comments?artcleId="+artcleId,function(res4){
                var comments = res4.data;
                console.log(comments);
            });
        })
    })
})

OK,上面的代码是不是让人头晕,如果碰到某些更复杂的逻辑,就会出现更多的嵌套回调,这即称为"回调地狱(callback hell)";
我们可以稍加重构以提高可阅读性:

function getToken(callback){
    ajaxRequest("your-host/api/get-token",function(res1){
        callback(res1.data.token);
    });
}

function getUserByToken(token,callback){
    ajaxRequest("your-host/api/get-user?token="+token,function(res2){
            callback(res2.data.userInfo.id);
    });
}

function getArticlesByUserId(userId,callback){
    ajaxRequest("your-host/api/get-article?userId="+userId,function(res3){
            callback(res3.data[0].id);
    });
}

function getCommentsByArtcleId(artcleId,callback){
    ajaxRequest("your-host/api/get-comments?artcleId="+artcleId,function(res4){
            callback(res4.data);
    });
}

/*获取评论*/
getToken(function(token){
    getUserByToken(token,function(userId){
        getArticlesByUserId(userId,function(artcleId){
            getCommentsByArtcleId(artcleId,function(comments){
                console.log(comments);
            });
        });
    });
});

上面的代码看着是不是稍微清晰了一些,不过函数里面调函数的方式仍然丑陋,下面我们将介绍另一种异步调用方式Promise。

二、Promise

Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。
它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,
但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象,如果你不了解Promise,
可以移步Promise查看详细说明。

一个 Promise对象有且仅有三种状态:

  * pending:初始状态,既不是成功,也不是失败状态
  * fulfilled:意味着操作成功完成
  * rejected:意味着操作失败
  

pending状态的 Promise 对象可能触发fulfilled 状态并传递一个值给相应的状态处理方法,也可能触发失败状态(rejected)并传递失败信息。
当其中任一种情况出现时,Promise 对象的 then 方法绑定的处理方法(handlers )就会被调用(then方法包含两个参数:onfulfilled 和 onrejected,
它们都是Function类型。当Promise状态为fulfilled时,调用 then 的 onfulfilled 方法,当Promise状态为rejected时,调用 then 的 onrejected 方法,
所以在异步操作的完成和绑定处理方法之间不存在竞争),限于样例代码限制,在上面的例子中我并没有对请求异常做处理,在实际项目中读者朋友可以自行加上处理。
因为 Promise.prototype.then和Promise.prototype.catch方法返回promise 对象, 所以它们可以被链式调用。

还是以上的场景为例子来看Promise实现的异步调用的代码片段,如下所示:

function getToken(){
   return new Promise(function(resolve,reject){
       ajaxRequest("your-host/api/get-token",function(res1){
           resolve(res1.data.token);
       });
   }); 
}

function getUserByToken(token){
    return new Promise(function(resolve,reject){
        ajaxRequest("your-host/api/get-user?token="+token,function(res2){
            resolve(res2.data.userInfo.id);
        });
    });
}

function getArticlesByUserId(userId){
    return new Promise(function(resolve,reject){
        ajaxRequest("your-host/api/get-article?userId="+userId,function(res3){
            resolve(res3.data[0].id);
        });
    });
}

function getCommentsByArtcleId(artcleId){
    return new Promise(function(resolve,reject){
        ajaxRequest("your-host/api/get-comments?artcleId="+artcleId,function(res4){
            resolve(res4.data);
        });
    });
}

/*获取评论*/
getToken().then(function(token){
    return getUserByToken(token);
}).then(function(userId){
    return getArticlesByUserId(userId);
}).then(function(artcleId){
    return getCommentsByArtcleId(artcleId);
}).then(function(comments){
    console.log(comments);
});

从上面获取comments的代码可以看出,后一个方法的调用总是在前一个异步调用完成后,通过前一个结果作为参数去执行下一个请求,
一步一步往后执行直到所有异步请求都执行完成,这个过程不仅代码结构上清晰了许多,而且从编程风格上看也能看出些类同步编码的影子。
下面介绍一个更接近同步编程的风格的异步编码方式生成器函数Generator。

三、Generator

Generator即生成器,它是生成器函数(Function*)返回的一个对象,是ES2015中提供的一种异步编程解决方案;
而生成器函数有两个特征,一是函数名前带星号,二是内部执行语句前有关键字 yield,调用一个生成器函数并不会马上执行它里面的语句,而是返回一个这个生成器的迭代器对象。当这个迭代器的 next() 方法被首次调用时,
其内的语句会执行到第一个出现yield的位置为止,yield 后紧跟迭代器要返回的值。或者如果用的是 yield*(多了个星号),则表示将执行权移交给另一个生成器函数(当前生成器暂停执行)。
next() 方法返回一个对象,这个对象包含两个属性:valuedonevalue 属性表示本次 yield 表达式的返回值,done 属性为布尔类型,表示生成器后续是否还有 yield 语句,即生成器函数是否已经执行完毕并返回。
调用 next() 方法时,如果传入了参数,那么这个参数会作为上一条执行的 yield 语句的返回值。看一个简单的例子:

function* genFun(){
    yield "initial";
    var anotherVal=yield "Hello";
    yield anotherVal;
}

var gObj=genFun();
console.log(gObj.next());// 执行 yield "initial";,返回 "initial",{value:"initial",done:false}
console.log(gObj.next());// 执行 yield "Hello",返回 "Hello",{value:"Hello",done:false
console.log(gObj.next("World"));// 将"World"赋给上一条 yield "Hello"的左值anotherVal,即执行 anotherVal="World",返回"World",{value:"World",done:false}
console.log(gObj.next());// 执行完毕,{value:undefined,done:true}

在上面的例子中,如果第三个 next() 的调用是在给anotherVal赋值,这样执行之后返回的 value 即为传入的参数,如果不传参数,则返回的 value 为undefined,且此时的 done 还是 false,这里需要注意。
当在生成器函数中显式 return 时,会导致生成器立即变为完成状态,即调用 next() 方法返回的对象的 donetrue。如果 return 后面跟了一个值,那么这个值会作为当前调用 next() 方法返回的 value 值。请看如下代码:

function* yieldAndReturn() {
  yield "Y";
  return "R";//显式返回处,可以观察到 done 也立即变为了 true
  yield "unreachable";// 不会被执行了
}

var gen = yieldAndReturn()
console.log(gen.next()); // { value: "Y", done: false }
console.log(gen.next()); // { value: "R", done: true }
console.log(gen.next()); // { value: undefined, done: true }

了解了Generator的简单概念之后,那它到底与本文核心内容有什么关联呢?OK,咱们还是以上面的场景来使用Generator方式实现(异步调用api的几个方法公用上面的),代码片段如下:

function* myGen(){
  var token = yield getToken();
  var userId = yield getUserByToken(token);
  var articleId = yield getArticlesByUserId(userId);
  var comments = yield getCommentsByArtcleId(articleId);
  console.log(comments);  
}

var gen = myGen();
gen.next().value.then(function(res1){
  gen.next(res1).value.then(function(res2){
      gen.next(res2).value.then(function(res3){
        gen.next(res3).value.then(function(res4){
            gen.next(res4);
            console.log("executing done");
        });
      });
  });
});

从上面的代码中,咱们可以看到生成器函数 myGen里面的语句就跟平时写同步代码一样类似,只是多了关键字 yield,这即是Generator的关键之处,用同步的编码方式,处理异步逻辑。
但同时我们也看到后半部分代码的执行跟之前的Promise几乎一样,一连串的 then 语句看起来还是不怎么美观,咱们可以对它进行再一次封装:

function genRunner(){
    var gen = myGen();
    
    function run(result){
        if(result.done) {
            return;
        }
        result.value.then(function(res){
            run(gen.next(res));
        });
    }
    run(gen.next());
}

genRunner();

通过封装一个函数执行器,通过在函数内部循环调用自身来执行Generator函数内部的所有yield 语句,这样的代码阅读起来就更加清晰且优雅了!

四、async/await

async 是ES2017引入的一种函数形式,可以使用它加在 function 前来声明定义异步函数,使用它能给异步编程带来极大的便利,从code形式上看就跟编写同步代码一样。当一个async 函数被调用时,它返回一个 Promise 对象。
async 函数返回一个值时,Promise 将用返回的值 resolved。 当async 函数抛出异常或某个值时,Promise将被抛出的值 rejected
async 函数可以包含 await 表达式,带有 await 的语句会暂停async 函数的执行并等待传递的Promise的解析,然后再恢复async 函数的执行并返回解析后的值。
async/await 函数的目的是简化同步使用 Promise 的行为,并对一组 Promise 执行某些行为,就像 Promises 类似于结构化回调一样,async/await 相当于 GeneratorPromise 的集合体。

先来看一个简单的例子:

function fakeRequest() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("second output");
    }, 500);
  });
}

async function asyncCall() {
  console.log("first output");
  var result = await fakeRequest();
  console.log(result);
  console.log("last output");
}

asyncCall();

/*执行后的输出*/
//first output
//second output
//last output

从上面的代码写法以及async 函数内部的执行结果可以看出,这简直就是同步调用的同步编程风格和执行顺序,有没有?其实如上所述,await 的语句会暂停async 函数的执行并等待传递的Promise的解析,
因此才会有console.log("last output");再最后输出,如果在async 函数体外面在写一个执行代码,则会先于await 结果输出;

咱们还是以最初的场景为例,使用async/await 的方式来实现一遍,看看代码风格上的差异:

async function getComments(){
  var token = await getToken();
  var userId = await getUserByToken(token);
  var articleId = await getArticlesByUserId(userId);
  var comments = await getCommentsByArtcleId(articleId);
  console.log(comments);  
}

从代码风格上看是不是跟Generator函数基本一样,只是把星号去掉,前面加了async ,函数体内语句中把 yield 换成来 await;但是调用执行函数时则完全不一样了,
Generator函数需要额外定义执行函数器,通过不断调用 next() 来完成调用获取结果,而async 函数自带来执行函数器,只要调用函数即会执行,因此使用上也方便来许多。

## 总结
咱们再回顾一下文章内容,首先通过最传统的 ajax 方式异步调用和回调函数处理;然后加入Promise对象,通过链式调用使代码编写更加有条理性;
之后又引入了新的异步编程解决方案 Generator ,其函数内部的编码方式与同步写法及其类似,只是 Generator 的执行权交由了另外一个函数,其执行方式仍然需要不断的调用 next() 而略显繁琐;
最后引入了ES2017新标准中收录的新函数 async,通过与await 相结合,使其异步调用的编码实现基本跟同步编码相差无几,且非常易于理解和提高了代码的维护行。
好了,到这里也该是文章结束的时候了,虽然篇幅不长,并且描述文字也不多,但还是希望阅读之后的朋友们能有所收获;由于写作仓促,文中难免出现错误或描述不清的地方,希望朋友们能谅解,并欢迎指正。

注:文中所有的代码都没对异常进行处理,如果你在实际项目中使用,请记得加上异常和错误处理逻辑!

参考资源

AJAX

Promise

Generator

Function*

async/await

阮一峰老师的ECMAScript 6 入门

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93223.html

相关文章

  • 《从零构建前后分离web项目》:开篇 - 纵观WEB历史演变

    摘要:更详细的内容下一章开篇深入聊聊前后分离讲述关于我目前在写从零构建前后分离项目系列,修正和补充以此为准不断更新的项目实践地址彩蛋提前预览下一章传送门 开篇 : 纵观WEB历史演变 在校学习和几年工作工作中不知不觉经历了一半的 WEB 历史演变、对近几年的发展比较了解,结合经验聊聊 WEB 发展历史。 演变不易,但也是必然,因为为人始终要进步。 WEB 的发展史 一、开山鼻祖 - 石器时代...

    tracy 评论0 收藏0
  • 《从零构建前后分离web项目》:开篇 - 纵观WEB历史演变

    摘要:更详细的内容下一章开篇深入聊聊前后分离讲述关于我目前在写从零构建前后分离项目系列,修正和补充以此为准不断更新的项目实践地址彩蛋提前预览下一章传送门 开篇 : 纵观WEB历史演变 在校学习和几年工作工作中不知不觉经历了一半的 WEB 历史演变、对近几年的发展比较了解,结合经验聊聊 WEB 发展历史。 演变不易,但也是必然,因为为人始终要进步。 WEB 的发展史 一、开山鼻祖 - 石器时代...

    songjz 评论0 收藏0
  • 浅谈微服务与接口网关

    摘要:微服务如何演变而来网关在微服务中如何发挥作用本文将以此作为话题,聊聊网关如何影响企业技术架构的演变。微服务之间相互独立,使用者无需配置环境,直接调用即可完成开发。 互联网技术日新月异,项目架构不断升级优化。随着企业微服务的兴起和第三方API的发展,API网关这一作为微服务核心组件的产品也逐渐被越来越多的人认识。微服务如何演变而来?网关在微服务中如何发挥作用?本文将以此作为话题,聊聊AP...

    Aceyclee 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<