[前端工坊]浅谈Web编程中的异步调用的发展演变
文章来自微信公众号:前端工坊(fe_workshop),不定期更新有趣、好玩的前端相关原创技术文章。 如果喜欢,请关注公众号:前端工坊
版权归微信公众号所有,转载请注明出处。
作者:京东金融-移动研发部-前端开发工程师 张恒
作为Web工程师,相信大家在开发项目的过程中,都存在与服务器端的通信,如登录验证、获取用户信息、获取应用数据等都需要通过调用后端的API来进行操作,而实现这一操作的正是异步调用;
这篇文章旨在通过一些异步调用的概念和相应的代码演示,尽量详细地介绍异步调用的实现、各种异步编程的使用方式和区别,以及他们的发展演变;
一、AJAX
在Web应用的开发过程中,为了实现良好的交互体验,我们都会使用 ajax 的方式与后端通信,实现无刷新数据提取和快速展现,极大地提升了用户体验;
ajax 的全称是Asynchronous JavaScript and XML,Asynchronous 即异步,它有别于传统web开发中采用的同步的方式。
ajax 的原理简单来说就是通过 XmlHttpRequest 对象来向服务器发异步请求,从服务器获得数据,然后用JavaScript来操作DOM而更新页面,这其中 XMLHttpRequest 是 ajax 的核心机制,
通过这种异步技术,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:'[email protected]' } } }
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() 方法返回一个对象,这个对象包含两个属性:value 和 done,value 属性表示本次 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() 方法返回的对象的 done 为 true。如果 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 相当于 Generator和 Promise 的集合体。
先来看一个简单的例子:
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 相结合,使其异步调用的编码实现基本跟同步编码相差无几,且非常易于理解和提高了代码的维护行。
好了,到这里也该是文章结束的时候了,虽然篇幅不长,并且描述文字也不多,但还是希望阅读之后的朋友们能有所收获;由于写作仓促,文中难免出现错误或描述不清的地方,希望朋友们能谅解,并欢迎指正。
注:文中所有的代码都没对异常进行处理,如果你在实际项目中使用,请记得加上异常和错误处理逻辑!
参考资源
相关推荐
* 最好使用异步调用,否则可能产生UI阻塞错误。* myTextView.setText; 虽然这里能设定结果,* Called when the activity is first created.