Promise详解
Promise
介绍:
- 用于异步计算
- 将异步操作队列化,按照期望的顺序执行,返回符合预期的结果
- 可以在对象之间传递和操作promise,帮助我们处理队列
由于promise是控制异步操作的,所以先来介绍一下在promise之前异步操作的常见语法。
- 事件侦听与响应
- 回调函数(例如ajax请求回调)
异步回调的问题:
- 回调地狱问题(一个回调嵌入一个回调,特别是一些数据库操作和文件操作 , 难以维护)
- 无法正常使用 return 和 throw
- 异步回调的回调函数都是在一个新栈中,所以在现在的栈无法获取到先前栈的信息。之前的栈也捕获不到当前栈抛出的错误,所以在异步回调中无法正常使用try catch正常处理错误。
- 在异步回调中经常需要在外层回调中去定义一些变量给内层回调使用。
talk is cheap , show me the code
const path = require('path'); const fs = require('fs'); //寻找最大文件的函数 function findLargest(dir,callback){ fs.readdir(dir,function(err,files){ if(err) return callback(err); //[错误使用回调来处理] let count = files.length; //获取文件长度 let errored = false; //是否错误 let stats = []; //遍历文件夹下的所有文件 files.forEach(file => { fs.stat(path.join(dir,file),(err,stat) =>{ if(errored) return; if(err){ errored = true; return callback(err); } stats.push(stat); if(--count === 0){ let largest = stats .filter(function(stat){ console.log('-----'); console.log(stat.isFile()); return stat.isFile(); }) //先判断是否是文件 .reduce(function(prev,next){ //判断大小 if(prev.size > next.size) { return prev; } return next; }); callback(null,files[stats.indexOf(largest)]) } }) }) }) } findLargest('../blog/blogDemo/移动端滚动详解demo',function(err,filename){ if(err) return console.error(err); console.log('largest file was:',filename); })
上面就是一个查找最大文件的例子,其中有许多回调带来的问题。接下来我们先回归主题,学习一些promise的使用,然后使用promise来改写这个例子。
promise详解
new Promise( /*实例化Promise时传入一个执行器,也就是一个函数*/ function(resolve,reject){ //异步操作放在这里 resolve(); //处理成功,修改实例化的promise对象的状态为fulfilled reject(); //处理失败,修改实例化的promise对象的状态为rejected } ) .then(function A(){ //成功之后的处理,即调用resolve()就执行A中的内容 },function B(){ //失败之后的处理,即调用reject()或者抛出了错误,就执行B中的内容 })
promise有三个状态:
pending 【待定】初始状态
fulfilled 【实现】操作成功
rejected 【否决】操作失败
promise的状态一发生改变,立马调用.then()中的响应函数处理后续步骤,如果then()中返回了一个新的promise实例,则继续循环下去。
promise常用的场景:
console.log('start'); new Promise(function(resolve,reject){ setTimeout(function(){ //定时器模拟异步 resolve('hello'); //修改promise状态调用then中的第一个函数 },2000); }).then((value)=>{ console.log(value); //接收resolve传来的值 return new Promise(function(resolve){ //该then()返回一个新的promise实例,后面可以继续接then setTimeout(function(){ resolve('world'); //修改新promise的状态,去调用then },3000) }) }).then((value)=>{ console.log(value); }) //输出结果: /* 立即输出 start 两秒输出 hello 再三秒 world */
上面我们在 then() 函数中返回的是一个新的promise,如果返回的不是一个新的promise会怎样呢?依然是上面的代码,稍作修改。
console.log('start'); new Promise(function(resolve,reject){ setTimeout(function(){ resolve('hello'); },2000); }).then((value)=>{ console.log(value); (function(){ return new Promise(function(resolve){ setTimeout(function(){ resolve('world'); },3000) }) })(); return false; }).then((value)=>{ console.log(value); }) /* 结果: 立即输出 start 两秒输出 hello 三秒输出 flase */
根据上面的运行结果来看,如果在一个then()中没有返回一个新的promise,则return 什么下一个then就接受什么,在上面的实例代码中return的是false,下一个then中接受到的value就是false,如果then中没有return,则默认return的是undefined.
注意:then中return Promise必须是在then函数的作用域中return,不能在其他函数作用域中return,无效。上面的例子中return Promise就是在一个立即执行函数中返回的,所以无效。
.then()中包含.then()的嵌套情况
then()的嵌套会先将内部的then()执行完毕再继续执行外部的then();在多个then嵌套时建议将其展开,将then()放在同一级,这样代码更清晰。
console.log('start'); new Promise((resolve,reject)=>{ setTimeout(function(){ console.log('step'); resolve(110); },1000) }) .then((value)=>{ return new Promise((resolve,reject)=>{ setTimeout(function(){ console.log('step1'); resolve(value); },1000) }) .then((value)=>{ console.log('step 1-1'); return value; }) .then((value)=>{ console.log('step 1-2'); return value; }) }) .then((value)=>{ console.log(value); console.log('step 2'); }) /* start step step1 step 1-1 step 1-2 110 step 2 */ //展开之后的代码 console.log('start'); new Promise((resolve,reject)=>{ setTimeout(function(){ console.log('step'); resolve(110); },1000) }) .then((value)=>{ return new Promise((resolve,reject)=>{ setTimeout(function(){ console.log('step1'); resolve(value); },1000) }) }) .then((value)=>{ console.log('step 1-1'); return value; }) .then((value)=>{ console.log('step 1-2'); return value; }) .then((value)=>{ console.log(value); console.log('step 2'); })
错误处理
promise处理错误有两种方式,一种是发现错误执行then中的第二个回调函数来处理错误,一种是.catch()来处理错误
注意:抛出ERROR时,只有在执行函数的顶层抛出后面的catch才会接受到。这里如果在setTimeout中抛出错误,catch和then中的错误处理函数是接受不到的
//1.根据错误执行then中第二个回调来处理错误 new Promise((resolve,reject)=>{ setTimeout(function(){ //只要出现了错误或者调用了reject,就可以在then的第二个函数中获取到 reject('err'); },1000) }).then((value)=>{ console.log(value); }, //出错之后的执行函数 (err)=>{ console.log('出错了'); console.log(err); }) /* 出错了 err */ //2.根据catch来获取错误,抛出err或者执行reject()都会在catch中获取到错误信息 new Promise((resolve,reject)=>{ //只要出现了错误,就可以在then的第二个函数中获取到 setTimeout(function(){ reject('一个错误'); },1000) }).then((value)=>{ console.log(value); }).catch(err=>{ console.log('错误信息:'+err); }) /* 错误信息:一个错误 */
更推荐使用catch的方式进行处理错误,因为catch能获取到之前所有then中出现的错误
catch和then的连用
如果每一步都有可能出现错误,那么就可能出现catch后面接上then的情况。上代码
new Promise((resolve,reject)=>{ resolve(); }) .then(value=>{ console.log('done 1'); throw new Error('done 1 error'); }) .catch(err=>{ console.log('错误信息1:'+err); }) .then(value=>{ console.log('done 2'); }) .catch(err=>{ console.log('错误信息2:'+err); }) /* done 1 错误信息1:Error: done 1 error done 2 说明catch后面会继续执行then,catch返回的也是一个promise实例 */ new Promise((resolve,reject)=>{ resolve(); }) .then(value=>{ console.log('done 1'); throw new Error('done 1 error'); }) .catch(err=>{ console.log('错误信息1:'+err); throw new Error('catch error'); }) .then(value=>{ console.log('done 2'); }) .catch(err=>{ console.log('错误信息2:'+err); }) /* done 1 错误信息1:Error: done 1 error 错误信息2:Error: catch error 如果在catch中也抛出了错误,则后面的then的第一个函数不会执行,因为返回的promise状态已经为rejected了 */
总的来说,catch之后可以接then,catch也是返回的一个promise对象。如果catch中出现错误,则promise状态修改成reject,否则为fullfilled状态
Promise.all()
将多个Promise批量执行,所有的Promise都完毕之后返回一个新的Promise。
- 接收一个数组作为参数
- 数组中可以是Promise实例,也可以是别的值,只有Promise会等待状态的改变
- 所有子Promise完成,则该Promise完成,并且返回值是参数数组中所有Promise实例的结果组成的数组
- 有任何一个Promise失败,则该Promise失败,返回值是第一个失败的Promise的结果
console.log('here we go'); Promise.all([1,2,3]) .then(all=>{ console.log('1: ' + all); return Promise.all([function(){ console.log('ooxx'); },'xxoo',false]) }) .then(all=>{ console.log('2: ' + all); let p1 = new Promise(resolve=>{ setTimeout(function(){ resolve('I\'m p1'); },1500) }); let p2 = new Promise(resolve=>{ setTimeout(function(){ resolve('I\'m p2'); },2000) }); return Promise.all([p1,p2]); }) .then(all=>{ console.log('3: '+all); let p1 = new Promise((resolve,reject)=>{ setTimeout(function(){ resolve('P1'); },1000) }) let p2 = new Promise((resolve,reject)=>{ setTimeout(function(){ reject('P2'); },3000) }) let p3 = new Promise((resolve,reject)=>{ setTimeout(function(){ reject('P3'); },2000) }) return Promise.all([p1,p2,p3]); }) .then(all=>{ console.log('all: ' + all); }) .catch(err=>{ console.log('Catch:' + err); }) /* here we go 1: 1,2,3 2: function(){ console.log('ooxx'); },xxoo,false 3: I'm p1,I'm p2 Catch:P3 证明了上面的四点。 */
Promise.race()
和Promise.all()差不多,区别就是传入的数组中有一个Promise完成了则整个Promise完成了。
let p1 = new Promise(resolve=>{ setTimeout(function(){ resolve('p1'); },10000); }) let p2 = new Promise(resolve=>{ setTimeout(function(){ resolve('p2'); },1000); }) Promise.race([p1,p2]) .then((value)=>{ console.log(value); }) /* p1 1s之后输出 。。 等待十秒后代码才算执行完毕 */
常见用法:
将定时器和异步操作放在一起,如果定时器先触发,则认为超时,告知用户。
let p1 = new Promise(resolve=>{ $.ajax({ success:function(result){ resolve(result); } }); //异步操作 }) let p2 = new Promise(resolve=>{ setTimeout(function(){ resolve('timeout'); },10000); }) Promise.race([p1,p2]) .then(value=>{ if(value === 'timeout'){ alert('请求超时'); } })
将回调包装为Promise
好处:1.可读性好 2. 返回的结果可以放在任意Promise队列
// 将nodejs中fs模块的readDir和readFile方法包装为Promise //FileSystem.js const fs = require('fs'); module.exports = { readDir: function(path,options){ return new Promise(resolve=>{ fs.readdir(path,options,(err,files)=>{ if(err){ throw err; } resolve(files); }) }) }, readFile: function(path,options){ return new Promise(resolve=>{ fs.readFile(path,options,(err,content)=>{ if(err) throw err; resolve(content); }) }) } } //test.js const fs = require('./FileSystem'); fs.readFile('./test.txt','utf-8') .then(content=>{ console.log(content); })
寻找最大文件Promise改版
const fs = require('fs'); const path = require('path'); const FileSystem = require('./FileSystem'); //用上面封装的FileSystem function findLargest(dir) { return FileSystem .readDir(dir, 'utf-8') .then(files => { return Promise.all(files.map(file=>{ return new Promise(resolve =>{ fs.stat(path.join(dir,file),(err,stat)=>{ if err throw err; if(stat.isDirectory()){ return resolve({ size: 0 }); } stat.file = file; resolve(stat); }); }); })); }) .then( stats =>{ let biggest = stats.reduce((memo,stat)=>{ if(memo.size < stat.size){ return stat; } return memo; }); return biggest.file; }) }
我的文章都可以在我的gitbook上找到。欢迎star哈哈!gitbook地址