初识异步与Promise

188 阅读6分钟

何为同步?何为异步?

同步

能直接拿到结果,就是同步
比如在医院挂号,需要拿到号码我们才会离开窗口
同步任务可能消耗几毫秒到几秒不等,但有一个原则就是不拿到结果是不会离开的

异步

如果不能直接拿到结果,就是异步
比如在餐厅门口排队吃饭,我们在拿到号码之后,可以不用在餐厅门口傻等,在这段时间可以去做一些其他事,

那什么时候可以进店吃饭?
你可以每10分钟去餐厅问一下(轮询)
也可以扫一个微信,让微信通知你用餐时间(回调)

异步举例

以AJAX为例:

getJSON.onclick = () =>{
    const request = new XMLHttpRequest()
    request.open('GET','/5.json')
    request.onreadystatechange = () =>{
        if(request.readyState === 4 && request.status ===200){
            const object = JSON.parse(request.response)
            console.log(request.response) 
            }
        };
    request.send();
}
  • requsest.send()之后,并不能直接得到response
  • 必须等到readyState===4之后,浏览器回头调用request.onreadystatechange函数
    • 这就好比于我们在餐厅把微信号留在那里,等到可以吃的时候,餐厅会通过微信通知我们
  • 我们才能得到request.response

通过这个例子可以简单的理解一下回调callback

  • 我写给别人用,自己不用的函数,就是回调
  • 如上request.onreadystatechange就是“我”写给浏览器调用的
  • 意思就是浏览器你回头调一下这个函数
  • 这个回头,就意为将来,如“我回头请你吃饭”

回调举例

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

这里,我没有直接调用f1,而是把它传给f2,用f2调用了f1
所以f1是我写给f2调用的函数,f1即为回调

异步和回调的关系

关联

异步任务需要在得到结果时通知JS来拿结果

如何通知?

  • 可以让JS留一个函数地址(微信号)给浏览器
  • 异步任务完成时浏览器调用该函数地址即可(发送微信)
  • 同时把结果作为参数传给函数(通知可以来吃饭了)

区别

  • 异步任务需要用到回调函数来通知结果(但也不一定非要用,也可以轮询)
  • 但回调函数不一定只用在异步任务里
  • 回调可以用到同步任务里
    • 如: array.forEach((item)=>console.log(item))就是同步回调

如何判断同步异步

简单来说,如果一个函数的返回值处于

  • setTimeout
  • AJAX
    • 注:AJAX可以设置为同步的,但没人会这么做,因为这样会使页面在请求期间整个卡住
  • AddEventListener
  • 这三个东西的内容,那么这个函数就是异步函数
  • 当然还有其他的API是异步的,之后边学边总结

再次举例

摇骰子

function 摇骰子(){
	setTimeout(()=>{
  	return parseInt(Math.random()*6) + 1
  },1000)
}
const n = 摇骰子()
console.log(n)

这段代码中,
摇骰子()没有写return,它会默认return undefined
箭头函数里有return,返回真正的结果
所以这就是一个异步函数

这时console.log(n)只会拿到undefined

那么应该如何拿到结果?
可以用回调,
写一个函数,把函数地址给摇骰子(),
然后要求摇骰子函数得到结果后把结果作为参数传给result

function result(x){
    console.log(x)
}

function 摇骰子(result){
	setTimeout((result)=>{
  	result(parseInt(Math.random()*6) + 1)
    },1000)
}

上述result函数的代码可以简化

摇骰子(x=>{
    console.log(x)
})
//这里的参数和需要用到的值是一样的,所以还可以进一步简化
摇骰子(console.log)

但要注意,如果参数个数不一致就不能这样简化

一道面试题

const array = ["1","2","3"].map(parseInt)
console.log(array)

这里打印出的值会是[1,NaN,NaN]

因为这里map默认是接受三个参数的,代码会等价于

const array = ["1","2","3"].map(item,i,arr) =>{
    return parseInt(item,i,arr)
})
console.log(array)

(注:i为索引值,
parseInt(string, radix) 将一个字符串string 转换为radix 进制的整数)

则parseInt接受到的参数是这样的

  • parseInt('1',0,arr)
    • 数组会被自动忽略 因为parseInt只接受两个参数
    • 给parseInt穿0,会被当做无效参数,所以打印出1
  • parseInt('2',1,arr)
    • 这里,会把2作为一进制的数进行解析
    • 一进制的数里面只有0,所以2不合法,打印出NaN
  • parseInt('3',2,arr)
    • 同理,这里把3作为二进制进行解析
    • 二进制里只有0和1,所以3不合法,打印出NaN

所以,正常情况下应该这么写

const array = ["1","2","3"].map(item) =>parseInt(item))
//相当于
const array = ["1","2","3"].map(item,i,arr) =>{
    return parseInt(item)
})

如果异步任务有两个结果,成功或失败,怎么办?

有两个老套的办法:

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

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

Node.js就是用这个方法

方法二:搞两个回调

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

但是,不管是方法一,还是方法二,都存在一些问题

  • 不规范,名称五花八门,有人用success+error,有人用success+fail等等,而且第一个参数是成功,还是第二个参数是成功,也没有明确规定

  • 容易出来回调地狱,导致代码很难读

    • image-20211214203314037
  • 很难进行错误处理

为了避免这三个问题,前端程序员开始从其他领域借鉴方法 —— Promise

我们以AJAX封装为例:

ajax = (method,url,options)=>{
    const{success,fail} = options //析构赋值
    const request = new XMLHttpRequest()
    request.open(method,url)
    request.onreadystatechange = ()=>{
        if(request.stastus<400){
            success.call(null,request.response)//成功,调用success
        }else if(request.stastus>=400){
            fail.call(null,request)
        }
    }
}
    request.send()
}

ajax('get','/xxx',{
    success(response){},
    fail(request)=>{}
    })

我们将这段代码改成Promise写法

首先,改用一下调用姿势

ajax('get','/xxx')
    .then((response)=>{},(request)=>{})

这样写的好处:不需要记success和fail,Promise规定第一个参数就是sucess,第二个参数就是fail

这里,ajax()返回了一个含有.then()方法的对象

如何得到这个含有.then()的对象?
需要对ajax进行进一步改写

ajax = (method,url,options)=>{
   return new Promise((resolve,reject)=>{//先return一个Promise构造函数
    const{success,fail} = options
    const request = new XMLHttpRequest()
    request.open(method,url)
    request.onreadystatechange = ()=>{
        if(request.stastus<400){
            resolve.call(null,request.response)//成功,调用resolve
        }else if(request.stastus>=400){
            reject.call(null,request)//失败,调用 reject
        }
     }
   }
    request.send()
 })
}

注意:

new Promise(function(resolve, reject){}里的resolve 和 reject 并不是 .then(success, fail) 里面的 success 和 fail,而是resolve 会去调用 success,reject 会去调用 fail

Promise使用小结

第一步:

  1. return new Promise((resolve, reject) =>{...})
  2. 任务成功调用 resolve.call(null, result)
  3. 任务失败就调用 reject.call(null, error)
  4. resolve和reject会再去调用成功和失败函数

第二步

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

当然,Promise还有很多高级用法,之后边学边总结