理解promise前需要搞懂的相关概念
-
回调函数
之前我有写过关于js中回调函数的概念,可以点击这里看一下!
我还是再简单说一下js中回调函数的概念,可以从这几个方面来理解:1、回调函数首先是函数,也就是说它跟你认识的其它js函数没什么不同,其实你可以认为js中所有函数都是回调函数;2、回调函数可能更强调的在未来某个时间点或某次事件触发之后再调用函数这种概念。我们看下面的代码解释,相信你一看就懂:
//普通函数a function a(){ console.log("普通函数"); }; a(); //回调函数b function b(){ console.log("回调函数b"); }; document.body.onclick = b; /* 这里之所以把函数b称为回调函数,只因为它被用于点击事件触发时,其实它跟a函数差不多的, 从某种意义上来说,a函数你可以理解成文档加载完之后才执行的,那么从这种意义上来说,a也可以说成回调函数 所以说回调函数跟普通函数没什么大不同,只是回调函数强调你在某个时间点用它 */
-
同步与异步
先说同步,同步就是说你代码的运行结果跟你书写代码顺序是一样的。如:
console.log('a'); console.log('b'); //这里先打印a,再打印b
而异步,你可以从两个方面来理解它:1、它是相对于同步来说的,怎么理解呢?就是说同步代码还没有执行完成的时候,异步代码运行结果肯定得不到,因为js是单线程的;2、异步是某个事件触发并同步队列代码为空时才执行。
对于理解第一点,可以看下面的代码:
let a,b; setTimeout(()=>{ b = 'b'; },10); a = 'a' + b; console.log(a);/*输出aundefined,因为此时setTimeout里面的代码还未执行,b的值为undefined, 为什么会是这个结果?因为给b赋值‘b’是个异步操作,而打印a是个同步操作,同步操作还没有执行完成,是拿不到异步执行结果的,*/
而这样改一下:
let a,b; setTimeout(()=>{ b = 'b'; a = 'a' + b; console.log(a);//输出ab },10);
对于理解第二点,我觉得你可以这样!初学者你只要记住,所有dom中的事件触发的调用、所有i/o操作以及setTimeout/setInterval等等操作都是异步的,你用多了,就理解什么是异步了。而对于老手来说呢!你可以从js是单线程、事件队列来理解,详情可以看这里,我之前写的一篇关于事件队列的代码。
Promise解决了什么问题
首先举例这样一个业务场景:我们要读取一个文件,读取文件之后,如果文件小于某个值,那么我们需要读取其中的一张图片,如果对应的这张图片存在,我需要做一个美化图片工作,在这之后,我需要把这张图片发送给我的朋友,我朋友收到之后,如果她喜欢这张图片的话,那么她会反馈她喜欢这张图片的信息给我!因为该义务场景涉及到相关i/o等异步操作,不会使用同步代码来完成,否则将使你的程序阻塞很久。如此这样一个过程,如果用传统的回调函数解决的话,那么代码将是下面这样:
注:下面的代码为了说明问题,相关细节采用同步代替异步模拟实现,并相关细节代码省略
//读取文件操作伪代码将是这样 function readFile(source,callback){ /*具体读取文件代码省略*/ console.log("读取文件"); callback({size:999}); }; //读取图片 function readImage(file,callback){ /*具体代码省略*/ console.log("读取图片"); callback('image'); }; //美化图片 function beautifulImage(image,callback){ /*具体代码省略*/ console.log("美化图片"); callback('image'); }; //发送给朋友 function sendToFrient(image,callback){ /*具体代码省略*/ console.log("发送给朋友"); callback(); }; //反馈给自己 function toSelf(message){ /*具体代码省略*/ console.log(message); }; //最终代码将会是这样 (()=>{ readFile('source',(file)=>{ if(file.size < 1000){ readImage(file,(image)=>{ if(image){ beautifulImage(image,(friendImage)=>{ sendToFrient(friendImage,()=>{ toSelf('i had received your image,i gled to like it,thank you!'); }); }); }; }); }; }); })();
我们发现上面的代码确实解决了问题,但是你会发现实现这样一个过程,嵌套了好多层,回调嵌套了好多层,实际项目中可能还会更多,这就是传说中回调地狱!将来维护代码时对于开发人员来说简直是灾难。好,我们来直面问题,上面的代码很明显暴露出两个简单的问题:1、代码嵌套了好多层,需要严格地控制if语句的开始与结束,并作相应的缩进,很容易写错和维护;2、相关代码需要开发人员严格的执行调用代码的先后顺序,需要做好严格的回调函数功能注释,当义务场景比较复杂的时候,可能写出来的代码嵌套10+层,稍不注意就写错。
如果我们把上面的如果用Promise来实现,代码如:
//读取文件 function readFile(source){ return new Promise((rs,rj)=>{ //部分读取文件i/o操作代码省略 console.log("读取文件"); rs('file'); }); }; //读取图片 function readImage(file){ return new Promise((rs,rj)=>{ //具体读取图片操作代码省略 console.log("读取图片"); rs('image'); }) }; //美化图片 function beautifulImage(image){ return new Promise((rs,rj)=>{ //具体美化图片操作代码省略 console.log("美化图片") rs('image'); }); }; //发送给朋友代码 function toSendFriend(image){ return new Promise((rs,rj)=>{ //具体发送代码省略 console.log("发送图片给朋友"); rs(); }); }; //反馈信息给自己 function toSelf(message){ console.log(message); /*具体细节代码省略*/ }; //最终调用代码如下: readFile('source') .then(file=>readImage(file)) .then(image=>beautifulImage(image)) .then(image=>toSendFriend(image)) .then(_=>{ toSelf('i had received your image,i gled to like it,thank you!'); }) .catch(err=>{ console.log("以上其中一步有错误!"); });
我们发现代码经过这样改过之后,整段代码条理、层次清晰了许多!对比代码,Promise主要有以下优点:
1、没有了传统回调函数方式的回调地狱问题,代码层次清晰;
2、在每一步操作中,不必对承诺作过多的处理,只需将结果交给下一层操作,由下一层来过滤相关条件。这样有助于提高上一层功能的复用性!
promise改进
虽然上面的例子演示了,用Promise解决传统回调函数带来回调函数地域问题,但其实Promise本身也是有缺点的,主要如下:
-
无法取消promise;
-
无法得知当前进入什么状态;
-
Promise内部的错误如果不catch的话,无法向外部抛出;
-
Promise实际上还是需要一步一步的调用then方法来实现接下来的异步操作。当你有很多异步操作的时候,你就会发现你的业务代码好多的then语句,这些语句毫无语义可言。而且Promise语句会让你的代码变得很冗余,因为无论什么操作,你都需要封装成Promise。
如果可以的话,我们可以使用es7的async函数来解决promise不能像写同步语句的问题,具体可以来这里看我关于async/await等的理解!
这里需要特别注意一下:无论是Promise还是generator,又或async等语句,本质上并没有改变javascript是单线程的本质,它们顶多就是一些语法糖而已,只是从语义及语法上更利于阅读及编码方面进行了一些高级装饰和原生api支持而已!