异步和回调函数、promise之间的关系、以及promise的使用和理解
参考阮一峰的Generator 函数的含义与用法http://www.ruanyifeng.com/blog/2015/04/generator.html,有兴趣的朋友可以去看一下
什么是异步?
所谓”异步”,简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
-
同步:假设你去了一家饭店,找个位置,叫来服务员,这个时候服务员对你说,对不起我是“同步”服务员,我要服务完这张桌子才能招呼你。那桌客人明明已经吃上了,你只是想要个菜单,这么小的动作,服务员却要你等到别人的一个大动作完成之后,才能再来招呼你,这个便是同步的问题:也就是“顺序交付的工作1234,必须按照1234的顺序完成”。
-
异步:则是将耗时很长的A交付的工作交给系统之后,就去继续做B交付的工作,。等到系统完成了前面的工作之后,再通过回调或者事件,继续做A剩下的工作。
AB工作的完成顺序,和交付他们的时间顺序无关,所以叫“异步”。
附带图片说明:
以下是读取文件事例说明(异步)
以下是读取文件事例说明(同步)
异步编程的方法大概有四种:
一、回调函数
二、事件监听
三、发布/订阅
四、promise对象
回调函数的概念
转载大神https://www.jianshu.com/p/40e459cfdc6f
一个函数被作为参数传递给另一个函数(在这里我们把另一个函数叫做“otherFunction”),回调函数在otherFunction中被调用。
/注意到click方法中是一个函数而不是一个变量 //它就是回调函数 //对象.属性(){} $("#btn_1").click(function() { alert("Btn 1 Clicked"); }); //以上相当于下面的说法: function click() { // 它就是回调函数 alert("Btn 1 Clicked"); } $("#btn_1").click(click);
回调函数是怎样运作的
因为函数在Javascript中是第一类对象,我们像对待对象一样对待函数,因此我们能像传递变量一样传递函数,在函数中返回函数,在其他函数中使用函数。当我们将一个回调函数作为参数传递给另一个函数是,我们仅仅传递了函数定义。我们并没有在参数中执行函数。我们并不传递像我们平时执行函数一样带有一对执行小括号()的函数。
需要注意的很重要的一点是回调函数并不会马上被执行。它会在包含它的函数内的某个特定时间点被“回调”(就像它的名字一样)实现回调函数的基本原理
使用命名函数或者匿名函数作为回调
像之前的例子一样,第一种方法就是匿名函数作为回调(使用了参数位置定义的匿名函数作为回调函数)。第二种方式就是命名函数作为回调(定义一个命名函数并将函数名作为变量传递给函数)。
//第一种方法:匿名函数作为回调函数 var generalLastName = "Cliton"; function getInput(options, callback){ var arr = []; arr.push(options); //将全局变量generalLastName传递给回调函数 callback(generalLastName,arr); } getInput({name:"Rich",speciality:"Javascript"}, function(generalLastName,arr){ console.log(generalLastName + ":" + arr[0].speciality) // Cliton:Javascript }); //第二种方法:命名函数作为回调函数 var generalLastName = "Cliton"; function getInput(options, callback){ var arr = []; arr.push(options); //将全局变量generalLastName传递给回调函数,就算里面形参名字一致也可以,使用全局变量 callback(generalLastName,arr); } function call(generalLastName,arr){ console.log(generalLastName + ":" + arr[0].speciality) // Cliton:Javascript } getInput({name:"Rich",speciality:"Javascript"}, call);
在执行之前确保回调函数是一个函数
function getInput(options, callback){ //确保callback是一个函数 if(typeof callback === "function"){ //调用它,既然我们已经确定了它是可调用的 callback(options); } }
回调地狱(回调十八层地狱)
var p_client = new Db('integration_tests_20', new Server("127.0.0.1", 27017, {}), {'pk':CustomPKFactory}); p_client.open(function(err, p_client) { p_client.dropDatabase(function(err, done) { p_client.createCollection('test_custom_key', function(err, collection) { collection.insert({'a':1}, function(err, docs) { collection.find({'_id':new ObjectID("aaaaaaaaaaaa")}, function(err, cursor) { cursor.toArray(function(err, items) { test.assertEquals(1, items.length); // Let's close the db p_client.close(); }); }); }); }); }); }); 解决方案: 1.给你的函数命名并传递它们的名字作为回调函数,而不是主函数的参数中定义匿名函数。 2.模块化L将你的代码分隔到模块中,这样你就可以到处一块代码来完成特定的工作。然后你可以在你的巨型应用中导入模块。
阮大神的解释我一个小白确实不是看得很懂,转载了一个大神的的文章,在这里补充说明一下阮大神里面的fs.readFile——这个是node.js里面的读取文件内容的回调函数语法,使用这个函数就可以读取文件内容。里面还有一个if()throw "500"主要是用来抛出错误的。
Promise
转载大神的promise说明和用法https://blog.csdn.net/qq_34645412/article/details/81170576#catch%E7%9A%84%E7%94%A8%E6%B3%95,大神这个博客说得很详细,看了基本都懂。
转载这个大神的简单版说明https://www.cnblogs.com/lunlunshiwo/p/8852984.html(如果不看上面大佬的解释,可能看不懂,反正我是这样)
Promise是JS异步编程中的重要概念,异步抽象处理对象,是目前比较流行Javascript异步编程解决方案之一。这句话说的很明白了,Promise是一种用于解决异步问题的思路、方案或者对象方式。在js中,经常使用异步的地方是Ajax交互。比如在es5时代,jQuery的ajax的使用success来完成异步的:
$.ajax({ url:'/xxx', success:()=>{}, error: ()=>{} })
这种方法可以清楚的让读代码的人明白那一部分是Ajax请求成功的回调函数和失败的回调函数。但是问题来了,当一次请求需要连续请求多个接口时,这段代码仿佛进入了一团乱麻中
// 第一次 $.ajax({ url:'/xxx', success:()=>{ // 第二次 $.ajax({ url:'/xxx', success:()=>{ // 第三次 $.ajax({ url:'/xxx', success:()=>{ // 可能还会有 }, error: ()=>{} }) }, error: ()=>{} }) }, error: ()=>{} })
也许因为success和error这两个函数的存在,理解这段代码会很简单,但是当我们更改需求的时候,这将成为一个棘手的问题。这就是回调地狱。
当然,这是es5时代。当js这门语言发展到es6时代时,Promise的出现给异步带来了变革。Promise提供一个then,来为异步提供回调函数:
$.ajax({ url:'/xxx', }).then( ()=>{ // 成功的回调 }, ()=>{ // 失败的回调 })
而其先进之处则是,可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作。
Promise的用法
说完了Promise是什么,下面让我们研究一下Promise怎么使用。首先,Promise是一个对象,因此,我们使用new的方式新建一个。然后给它传一个函数作为参数,这个函数呢也有两个参数,一个叫resolve(决定),一个叫reject(拒绝),这两个参数也是函数。紧接着,我们使用then来调用这个Promise:
const fn = new Promise(function (resolve, reject) { setTimeout(()=>{ let num = Math.ceil(Math.random() * 10) // 假设num为7 if (num > 5) { resolve(num) //返回7 } else { reject(num) } },2000) }) fn.then((res)=>{ console.log(res) // 7 },(err)=>{ console.log(err) })
这就是最简单的Promise的使用。假设2秒钟之后生成随机数为7,因此resolve回调函数运行,then走第一个函数,console.log(7)。假设2秒钟之后生成随机数为3,因此reject回调函数运行,then走第二个函数,console.log(3)。
那你可能说了,Promise要是就这点能耐也没什么大不了的啊?我们上面说了Promise的先进之处在于可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作:
fn = new Promise(function (resolve, reject) { let num = Math.ceil(Math.random() * 10) if (num > 5) { resolve(num) } else { reject(num) } }) // 第一次回调 fn.then((res)=>{ console.log(`res==>${res}`) return new Promise((resolve,reject)=>{ if(2*res>15){ resolve(2*res) }else{ reject(2*res) } }) },(err)=>{ console.log(`err==>${err}`) }).then((res)=>{ // 第二次回调 console.log(res) },(err)=>{ console.log(`err==>${err}`) })
这就可以代替了上面类似es5时代的jQurey的success的嵌套式的回调地狱的产生,让代码清爽了许多。这里的resolve就相当于以前的success。
Promise的原理
在Promise的内部,有一个状态管理器的存在,有三种状态:pending、fulfilled、rejected。
(1) promise 对象初始化状态为 pending。
(2) 当调用resolve(成功),会由pending => fulfilled。
(3) 当调用reject(失败),会由pending => rejected。
因此,看上面的的代码中的resolve(num)其实是将promise的状态由pending改为fulfilled,然后向then的成功回掉函数传值,reject反之。但是需要记住的是注意promsie状态 只能由 pending => fulfilled/rejected, 一旦修改就不能再变(记住,一定要记住,下面会考到)。
当状态为fulfilled(rejected反之)时,then的成功回调函数会被调用,并接受上面传来的num,进而进行操作。promise.then方法每次调用,都返回一个新的promise对象 所以可以链式写法(无论resolve还是reject都是这样)。
Promise的几种方法
then
then方法用于注册当状态变为fulfilled或者reject时的回调函数:
// onFulfilled 是用来接收promise成功的值 // onRejected 是用来接收promise失败的原因 promise.then(onFulfilled, onRejected);
需要注意的地方是then方法是异步执行的。
// resolve(成功) onFulfilled会被调用 const promise = new Promise((resolve, reject) => { resolve('fulfilled'); // 状态由 pending => fulfilled }); promise.then(result => { // onFulfilled console.log(result); // 'fulfilled' }, reason => { // onRejected 不会被调用 }) // reject(失败) onRejected会被调用 const promise = new Promise((resolve, reject) => { reject('rejected'); // 状态由 pending => rejected }); promise.then(result => { // onFulfilled 不会被调用 }, reason => { // onRejected console.log(rejected); // 'rejected' })
catch
catch在链式写法中可以捕获前面then中发送的异常。
fn = new Promise(function (resolve, reject) { let num = Math.ceil(Math.random() * 10) if (num > 5) { resolve(num) } else { reject(num) } }) fn..then((res)=>{ console.log(res) }).catch((err)=>{ console.log(`err==>${err}`) })
其实,catch相当于then(null,onRejected),前者只是后者的语法糖而已。
resolve、reject
Promise.resolve 返回一个fulfilled状态的promise对象,Promise.reject 返回一个rejected状态的promise对象。
Promise.resolve('hello').then(function(value){ console.log(value); }); Promise.resolve('hello'); // 相当于 const promise = new Promise(resolve => { resolve('hello'); }); // reject反之
all
但从字面意思上理解,可能为一个状态全部怎么样的意思,让我看一下其用法,就可以看明白这个静态方法:
var p1 = Promise.resolve(1), p2 = Promise.reject(2), p3 = Promise.resolve(3); Promise.all([p1, p2, p3]).then((res)=>{ //then方法不会被执行 console.log(results); }).catch((err)=>{ //catch方法将会被执行,输出结果为:2 console.log(err); });
大概就是作为参数的几个promise对象一旦有一个的状态为rejected,则all的返回值就是rejected。
当这几个作为参数的函数的返回状态为fulfilled时,至于输出的时间就要看谁跑的慢了:
let p1 = new Promise((resolve)=>{ setTimeout(()=>{ console.log('1s') //1s后输出 resolve(1) },1000) }) let p10 = new Promise((resolve)=>{ setTimeout(()=>{ console.log('10s') //10s后输出 resolve(10) },10000) }) let p5 = new Promise((resolve)=>{ setTimeout(()=>{ console.log('5s') //5s后输出 resolve(5) },5000) }) Promise.all([p1, p10, p5]).then((res)=>{ console.log(res); // 最后输出 })
这段代码运行时,根据看谁跑的慢的原则,则会在10s之后输出[1,10,5]。over,all收工。
race
promise.race()方法也可以处理一个promise实例数组但它和promise.all()不同,从字面意思上理解就是竞速,那么理解起来上就简单多了,也就是说在数组中的元素实例那个率先改变状态,就向下传递谁的状态和异步结果。但是,其余的还是会继续进行的。
let p1 = new Promise((resolve)=>{ setTimeout(()=>{ console.log('1s') //1s后输出 resolve(1) },1000) }) let p10 = new Promise((resolve)=>{ setTimeout(()=>{ console.log('10s') //10s后输出 resolve(10) //不传递 },10000) }) let p5 = new Promise((resolve)=>{ setTimeout(()=>{ console.log('5s') //5s后输出 resolve(5) //不传递 },5000) }) Promise.race([p1, p10, p5]).then((res)=>{ console.log(res); // 最后输出 })
因此,在这段代码的结尾我们的结果为
1s 1 //因为这个是速度最快的,rate的性质是只输出最快的,其他的就不输出了,至于为什么会打印是因为,已经new了一个对象出来,这个对象会自己执行,可是不会返回数据 5s 10s
我们可以根据race这个属性做超时的操作:
//请求某个图片资源 let requestImg = new Promise(function(resolve, reject){ var img = new Image(); img.onload = function(){ resolve(img); } }); //延时函数,用于给请求计时 let timeOut = new Promise(function(resolve, reject){ setTimeout(function(){ reject('图片请求超时'); }, 5000); }); Promise.race([requestImg, timeout]).then((res)=>{ console.log(res); }).catch((err)=>{ console.log(err); });
重头戏!!!!实现一个简单的Promise
function Promise(fn){ var status = 'pending' function successNotify(){ status = 'fulfilled'//状态变为fulfilled toDoThen.apply(undefined, arguments)//执行回调 } function failNotify(){ status = 'rejected'//状态变为rejected toDoThen.apply(undefined, arguments)//执行回调 } function toDoThen(){ setTimeout(()=>{ // 保证回调是异步执行的 if(status === 'fulfilled'){ for(let i =0; i< successArray.length;i ++) { successArray[i].apply(undefined, arguments)//执行then里面的回掉函数 } }else if(status === 'rejected'){ for(let i =0; i< failArray.length;i ++) { failArray[i].apply(undefined, arguments)//执行then里面的回掉函数 } } }) } var successArray = [] var failArray = [] fn.call(undefined, successNotify, failNotify) return { then: function(successFn, failFn){ successArray.push(successFn) failArray.push(failFn) return undefined // 此处应该返回一个Promise } } }
下面附带正式使用promise的场景
// Promise 写法 // 第一步:获取城市列表 const cityList = new Promise((resolve, reject) => { $.ajax({ url: 'https://www.easy-mock.com/mock/5a52256ad408383e0e3868d7/lagou/city', success (res) { resolve(res) } }) }) // 第二步:找到城市是北京的id cityList.then(res => { let findCityId = res.filter(item => { if (item.id == 'c1') { return item } })[0].id findCompanyId().then(res => { // 第三步(2):根据北京的id -> 找到北京公司的id let findPostionId = res.filter(item => { if(item.cityId == findCityId) { return item } })[0].id // 第四步(2):传入公司的id companyInfo(findPostionId) }) }) // 第三步(1):根据北京的id -> 找到北京公司的id function findCompanyId () { let aaa = new Promise((resolve, reject) => { $.ajax({ url: 'https://www.easy-mock.com/mock/5a52256ad408383e0e3868d7/lagou/position-list', success (res) { resolve(res) } }) }) return aaa } // 第四步:根据上一个API的id(findPostionId)找到具体公司,然后返回公司详情 function companyInfo (id) { let companyList = new Promise((resolve, reject) => { $.ajax({ url: 'https://www.easy-mock.com/mock/5a52256ad408383e0e3868d7/lagou/company', success (res) { let comInfo = res.filter(item => { if (id == item.id) { return item } })[0] console.log(comInfo) } }) }) }