异步与Promise

400 阅读7分钟

术语

同步

  • 如果能直接拿到结果,就是同步

异步

  • 如果不能直接拿到结果就是异步
  • 每隔一段时间询问数据,叫做轮询
  • 直接调用,叫做回调

举例

  • 以AJAX请求JSON文件为例
  • 当在本地请求JSOn文件,得到响应数据(response),大概需要几毫秒
    6e0590dcd2729e6e6fa6bae914e7061.png
  • 在发送请求后立即打印出响应数据,不能直接得到数据

864b42cf4a8559574516a3489c0a118.png

be6492f45d9598fae0e47abe5ef21ef.png

  • 需要在这几毫秒之后才可以
  • 调用setTiemout(),2秒之后打印,得到响应数据 1634800424(1).png

1634800464(1).png

  • 当console.log放入onreadyStateChange函数里面时
  • 直接得到响应数据
  • 因为此时,必须等到oneadyState变为4(即处于完成响应阶段)时,才能得到响应数据
  • 由此证明浏览器回头调用这个函数,得到了响应数据,此过程叫做回调

1634800837(1).png

1634800920(1).png

回调 callback

  • 写给自己的函数,不是回调
  • 写给别人用的函数,就是回调
  • request.onreadystteChange就是写给浏览器调用的
  • 即浏览器回头调用一下这个函数
  • 这里的“回头”指将来的某一时刻

回调举例

例:把f1给函数f2

function f1(){  }
function f2(fn){
                fn()
                }
   f2(f1)

分析

  • f1没有调用- 因为直接调用的是f2
  • f1传给了f2
  • f2调用了f1
  • 那么f1就是写给f2的调用的函数,所以f1是调用函数

异步和回调的关系

  • 关联
    • 异步任务需要在得到结果时通知JS来拿结果
    • 方法:让JS留一个函数地址给浏览器
    • 异步任务完成时,浏览器调用改函数地址即可
    • 同时把结果作为参数传给该函数
    • 这个函数是给浏览器调用,拿到结果的所以是回调函数
  • 区别
    • 异步任务需要用到回调函数来通知结果
      • 也可以用到轮询,轮询性能太低
    • 但回调函数不一定只用你在异步任务里
    • 回调可以用到同步任务里
    • aeeray.forEach(n=>console.log(n))

判断函数是同步还是异步

  • 如果一个函数的返回值处于
    • setTimeout
    • AJAX(即XMLHttpRequest)
    • AddEventListener
    • 这三个常用API内部,那么函数就是异步函数
  • 非常不合理的做法:AJAX设置成同步
    • 正常情况下,异步操作,先请求JSON,再请求XML
    • 浏览器过程非常的流畅

1634884471(1).png

  • 而当强行把它变成同步(传入的参数改为false)
  • 必须先拿到JSON请求得到的数据之后,才能得到XNM的数据。在等待JSON的过程中,XML请求按钮虽然按下,但是XML数据根本拿不到,在实际开发中,会造成网页卡顿

9a67ec010c09ebaed9394a7ec68739c.png

摇骰子案例

例1:

function 摇骰子(){
setTimeOut(()=>{
           return parseInt(Math.random()*6)+1
},1000)
   // return undefined 
}
  • 摇骰子里面没有写return,那就是treturn undefined
  • 箭头函数里面没有return,返回真正的结果
  • 所以这是一个异步函数/任务
  • 摇骰子函数里面的return和箭头函数里面的return属于不同的函数

改动

const n=摇骰子()
console.log(n) //结果为undefinded
  • 拿到结果方法
  • 可以回调,写个函数,然后把地址给他
const f1(x){console.log(x)}
摇骰子(f1)
  • 然后要求摇骰子函数得到结果后把结果作为参数传给f1
function 摇骰子(fn){
    setTimeout(()=>{
          fn(parseInt(Math.random()*6)+1)
    },1000)
}

简化代码

  • 简化为箭头函数
  • 由于f1声明之后只用了一次,所以可以删掉f1
// 原本的调用机制
function f1(x){ console.log(x)}
摇骰子(f1)
  • 改为箭头函数(f1函数改为无名函数)
摇骰子(x=>{
console.log(x)   //以x为参数的函数
     })
  • 再次简化
  • x作为中间的媒介,将console.log传到摇骰子()函数,那么直接将console.log作为参数,由摇骰子()函数来调用它
摇骰子(console.log) //console.log此时不是直接调用,而是由外面的函数来调用
//与AJAX里面在reayonStateChange里面直接console.log()一样,接收的是同一个参数,request.response
  • 小结:以上简化只能在传进来的参数一致的情况下才可以否则就会产生错误(见下方例题)

案例

  • 错误简化
  • 想要得到结果[1,2,3]
  • 但是map函数需要传入三个参数,产生了错误 61d8c288b8f7877f0d8cc914a06695d.png
  • map函数完整展开
  • map函数接收三个参数:对应的元素、对应第几个、以及对应的数组
  • 里面的parseInt默认接收对应的元素
  • 所以结果正确
  • 而简化之后,接收的参数不一致 1634902932(1).png
  • 错误代码思路展示(注:以下代码错误)
  • 如果按照错误简写
  • 按照这个逻辑map和parseInt接收相同的参数
  • parseInt原本接收两个参数,分别为当前元素,和转成多少进制数
  • 此时parseInt接收了三个参数,当接收1时,第二个参数是0(i=0),但是没有0进制,所以相当于没传参数,数组也传不进去
  • 当传入2,第二个参数为1,表示1进制,直接报错
  • 传入3同理 1634911510(1).png
  • 正确简写

1634912195(1).png

异步小结

  • 异步任务不能拿到结果
  • 于是我们传一个回调任务给异步任务
  • 异步任务完成时调用回调
  • 调用的时候把结果作为参数

异步任务有两个结果 成功或者失败,解决方案

  • 方法一:回调接收两个参数
fs.readFile('./1.txt',(error.data)=>{
   if(error){console.log('失败了');return}   //失败
   console.log(data.toString())            //成功
   })
  • 方法二:使用两个回调
ajax('get','/1.json',data=>{},error=>{})
//前面是函数成功回调,后面是函数失败回调
- 另一种方式
ajax('get','1.json',{
success:()=>{},fail:()=>{}
} )
//接收一个对象,对象里面有两个key表示成功和失败

以上方法的不足之处

  • 不规范,名称五花八门(方法一)
  • 容易出现回调地狱(比如回调20层),代码变得看不懂(方法二)
getUser(user=>{
  getGroups(user,(groups)=>{
      groups.forEach((g)=>{
          g.filter(x=>x.ownerId===user.id)
          .forEach(x=>console.log(x))
      })
  })
})     //如果回调20层呢?
  • 很难进行错误处理

解决思路

  • 规范回调的名字或者顺序
  • 拒绝回调地狱,让代码可读性更强
  • 如何很方便地捕获错误

promise设计模式

以AJAX的封装为例

ajx=(method,url,options)=>{
    cosnt {success,fail}=options//析构赋值 
    //相当于 const success=options.success 
    // const fail-options.fail
    
    const request =new XMLHttpRequest()
    request.open(method,url)
    request.onreadystatechange=()=>{
        if (request.readyState===4){   //成功了就调用success,失败了就调用fail
            if (request.status<400){
                success.call(null,request.response)
            }else if(request.status>=400){
                fail.call(null,request,request.status)
            }
        }
    }
    request.send()
    }
    //调用指令
    ajax.('get','/xxx'),{
    success(response){},fail:(request,status)=>{}
    }//一个是function缩写,一个是箭头函数
  • 先改变调用方式
  • 上面调用了两个回调,还使用了success和fail
  • 改成promise写法
ajax('get','/xxx')
 .then((response)=>{} ,(request)=>{} )
  • 虽然也是回调
  • 但是不需要记success和fai了
  • then的第一参数就是success
  • then的第二个参数就是fail
  • ajax返回了一个含有.then方法的对象
  • 如果要得到这个含有.then()的对象,只能改造ajax的源码
  • 以下代码思路:ajax直接return,接收两个参数resolve和reject函数,这两个函数可以调用then后面的函数
ajax=(method,url,options)=>{
    return new Promise((resolve,reject)=>{   //ajax函数体直接return
        cosnt {success,fail}=options
        const request =new XMLHttpRequest()
        request.open(method,url)
        request.onreadystatechange=()=>{
            if (request.readyState===4){
            //如果成功就调用resolve,如果失败就调用reject
                if (request.status<400){
                    resolve.call(null,request.response) //只能传1个参数//可以不用call,直接resolve(request.success)
                }else if(request.status>=400){
                    reject.call(null,request,request.status)
                }
            }
        }
        request.send()
        
    })
    }

重要结构式

  • return new Promise((resolve,reject)=>{})

小结:如何让一个回调的异步函数变成Promis的异步函数

  • 第一步
    • return new Promise((resolve,reject)+>{....})
    • 任务成功调用resolve(result)
    • 任务失败调用reject(error)
    • resolve和reject会再去调用成功和失败函数
  • 第二步
    • 使用.then(success,fail)传入成功和失败函数
  • 补充:promise不可被取消

总结

  1. 异步是什么?
    • 不能直接得到结果
  2. 异步为什么会用到回调?
    • 因为异步不能直接得到结果,必须使用回调
  3. 回调存在的问题有哪些?
    • 回调地狱(多层回调),代码复杂、难以理解
    • 名称存在歧义
    • 无法进行错误处理
  4. Promise是什么?
    • 是一种设计模式
  5. Promise 基本的机构
    • return new Promise((resolve,reject)=>{})
  6. Promise是前端解决异步问题的统一方案