异步与Promise

447 阅读6分钟

1 同步与异步

1.1 概念

同步:能直接拿到结果,比如在医院挂号,只有拿到号才离开窗口;

异步:如果 JS 不能直接拿到一个函数的结果,可以先去执行别的代码,等结果到了再取结果,就是异步。比如餐厅门口等位,拿到号后可以去逛街,想要拿到结果,可以通过每10分钟去餐厅询问一次(轮询),或者扫码用微信接收通知(回调

轮询:定时去询问结果拿到了没有;

回调:异步的结果可以通过回调获取,一般来说结果会作为回调的第一个参数;

异步的好处是可以把用来等待的时间拿去做别的事情;

1.2 异步举例

  • AJAX 1、request.send()之后,并不能直接得到 response ,此时console.log(request.response) 没有值;

2、必须等到 readyState 变为 4 后,浏览器回头调用 request.onreadystatechange 函数;

3、此时才能得到 request.response ;

1.3 回调

1、写了却不调用,给别人调用的函数,就是回调 callback,回调除了用函数形式,还可以用对象形式,如 request.onreadystatechange;

2、例如:request.onreadystatechange 就是自己写给浏览器调用的,意思就是 你(浏览器)回头调一下这个函数;

3、例子

function f1(){}  //写了 f1 函数,自己没有调用;
function f2(fn){
  fn()  // f2 调用了 f1,所以 f1 是回调函数;
}
f2(f1)

function f1(){}  //写了 f1 函数,自己没有调用;
function f2(fn){
 // fn()  此时 f2 也没有调用 f1,所以 f1 不是回调函数;
}
f2(f1)

1.4 异步与回调的关系

  • 关联 1、异步任务需要在得到结果时通知 JS 来拿结果;

2、通知方式是,让 JS 留一个函数地址(电话号码)给浏览器;

3、异步任务完成时浏览器调用该函数地址即可(拨打电话);

4、同时把结果作为参数传给该函数(电话里说可以来吃了);

5、这个函数是我写给浏览器调用的,所以是回调函数;

  • 区别 1、异步任务一般用回调函数来通知结果,也可以用轮询;

2、回调函数不一定只用在异步任务里,还可以用在同步任务里;

3、例如:array.forEach(n=>console.log(n)) 就是同步回调;

2 判断同步与异步

  • 如果一个函数的返回值处于 1、setTimeout

2、AJAX(即 XMLHttpRequest)

3、AddEventListener

这三个东西内部,那么这个函数就是异步函数;

  • AJAX 通过第三个参数可以设为同步的,但是不要这么做,不然会在请求期间使页面卡住; request.open('GET','/2.js',true/false) // 默认为true, 改为 false 时即为同步

  • 例子 1、摇骰子

function 摇骰子(){
  setTimeout(()=>{
    return parseInt(Math.random()*6) + 1
  },1000)
}
// 摇骰子()没有写 return ,就是 return undefined ;
// 箭头函数里有 return ,返回真正的结果,所以是一个异步函数/异步任务。

2、摇骰子续

const n = 摇骰子()
console.log(n)  // 返回 undefined

怎么拿到异步结果:
1、用回调,写个函数,然后把函数地址给它
2、摇骰子函数得到结果后,将结果作为菜蔬传给 f1 ;
function f1(x){ console.log(x) }
摇骰子(f1)
function 摇骰子(fn){
  setTimeout(()=>{
    fn(parseInt(Math.random()*6) + 1)
  },1000)
}

3、简化函数 由于 fa 声明之后只用了一次,所以可以删掉 f1

function f1(x){console.log(x)}
摇骰子(x)

改为:

摇骰子(x =>{console.log(x)} 

再减化为:

摇骰子(console.log}  // 参数个数一致时才能这样简化

4、参数个数不一致时,不能简化;

const array = ['1','2','3'].map(parseInt)  // 简化
console.log(array)  // [1, NaN, NaN]

因为 map() 接收三个参数,如果简化,则 map(parseInt) 相当于 map((item,i,arr1)=>{ return parseInt(item,i,arr1)}),而 parseInt() 只接受两个参数,即参数不一致,所以不能简化。
第一次:parseInt('1',0) ,给 parseInt0,相当于没有传;
第二次:parseInt('2',1), 将 2 作为 1 进制的树进行解析,所以是 NaN ;
第三次:parseInt('3',2), 将 3 作为 2 进制的进行解析,所以是 NaN ;



const array1 = ['1','2','3'].map((item,i,arr1)=>{ return parseInt(item)})  
console.log(array1)  // [1, 2, 3]
  • 总结 1、异步任务不能拿到结果;

2、于是传一个回调给异步任务;

3、异步任务完成时调用回调;

4、调用的时候把结果作为参数;

3 异步任务有两个结果:成功或失败

3.1 方法一:回调接受两个参数

fs.readFile('./1.txt',(error,data)=>{
  if(error){ console.log('失败'); return }
  console.log(data.toString()) //成功
}

3.2 搞两个回调

ajax('get','/1.json', data=>{}, error=>{}) 
//前面是成功回调,后面是失败回调
或者

ajax('get','/1.json',{
  success: ()=>{},
  fail: ()=>{}
})
接受一个对象,对象有两个 key 表示成功和失败

3.3 缺点

1、不规范,有人用 success + error, 有人用 success + fail, 有人用 done + fail ;

2、容易出现回调地狱;

getUser( user =>{
  gerGroups(user, (groups)=>{
    groups.forEach((g)=>{
      g.fileter(x => x.ownerId === user.id)
        .forEach( x => console.log(x))
    })
  })
})

3、很难进行错误处理;

4 Promise

Promise - JavaScript | MDN (mozilla.org)

  • Promise 是目前前端解决异步问题的统一方案

  • window.Promise 是一个全局函数,可以用来构造 Promise 对象;

  • 使用 return new Promise((resolve,reject)=>{}) 可以构造一个 Promise 对象;

  • 构造出来的 Promise 对象含有一个 .then() 函数属性;

  • Promise 函数本身不含有一个 .then() 函数属性;

  • Promise 不可以取消,axios可以通过 cancelToken 取消,但取消的不是 Promise,而是发送的那个请求;

  • 以封装 AJAX 为例

ajax = (method,url,options)=>{
  const {success, fail} = options
  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,request.status)=>{}
}
  • 将上面代码改成 Promise 写法
// 先改调用方式
ajax('get','/xxx',{
  success(response){},
  fail: (request,status)=>{}
})    //这里用到了两个回调,还使用了 success 和 fail

// 改成 Promise 写法
ajax('get','/xxx')
  .then((response)=>{},(request,status)=>{})
  
// 虽然也是回调,但是不需要记 success 和 fail 了
// then 的第一个参数就是 success, 第二个参数就是 fail;
// 这里 ajax 返回了一个含有 .then() 方法的对象,需要改造 ajax 的源码,才能得到这个对象;

ajax = (method, url, options)=>{
  return new Promise((resolve,reject)=>{
    const {success, fail} = options
    const request = new XMLHttpRequest()
    request.open(method,url)
    request.onreadystatechange = ()=>{
      if(request.readyState === 4){
      //成功就调用 success, 失败就调用 fail
        if(request.status < 400){
          resolve.call(null,request.response)
        }else if(request.status >= 400){
          reject.call(null,request,request.status)
      }
    }
  }
  request.send()
  })
}
  • 小结 1、return new Promise((resolve,reject)=>}{});

2、任务成功则调用 resolve(result);

3、任务失败则调用 reject(result),resolve、reject 都只接受一个参数;

4、resolve 和 reject 不是 .then(success,fail)里面的 success 和 fail,resolve 调用成功函数,reject 调用失败函数;

5、使用 .then(success,fail) 传入成功函数和失败函数

  • 上面封装的 ajax 的缺点 1、post 无法上传数据,request.sen(这里可以上传数据)

2、不能设置请请求头, request.setRequestHeader(key,value)

3、解决方法:使用 jQuery.ajax 或 axios

2、优点:支持更多形式的参数,支持Promise,支持超多功能;

axios/axios

1、目前最新的 AJAX 库;

2、axios.get('/xxx') 返回一个 Promise 对象, axios.get('/xxx').then(s,f) 在请求成功的时候调用 s, 失败的时候调用 f ;

3、高级用法:

JSON自动处理:axios 如果发现响应的 Content-Type 是 json,就会自动调用 JSON.parse;

请求拦截器:可以在所有请求里加东西,比如查询参数;

响应拦截器:可以在所有响应里加东西,甚至改内容;

可以生成不同实例(对象):不同的实例可以设置不同的配置,用于复杂场景;