异步与Promise

274 阅读4分钟

什么是异步?什么是同步?什么是回调?

异步

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

  • 比如在医院挂号,你拿到号才会离开
  • 同步任务可能消耗10毫秒,也可能需要3秒
  • 总之拿不到结果不会离开

如果不能直接拿到结果那就是异步

  • 比如在餐厅门口等位,拿到号后可以去逛街
  • 什么时候才能吃饭?
  • 可以每10分钟去餐厅问一下(轮询)
  • 也可以扫码用微信接收通知(回调)

异步举例

以 AJAX 为例

  • request.send() 之后,并不能直接得到 response
  • 必须等到 readyState 变为4后,浏览器回头调用 reques.onreadystatechange 函数
  • 才能得到 request.response

回调 callback

  • 你写个自己用的函数, 不是回调
  • 你写给别人用的函数, 就是回调
  • request.onreadystatechange 就是写给浏览器调用的
  • 意思就是你(浏览器)回头调一下这个函数

回调举例

把函数1给另一个函数2

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

异步和回调的关系

关联

  • 异步任务需要在得到结果时通知 JS 来拿结果
  • 让 JS 留一个函数地址给浏览器
  • 异步任务完成时浏览器调用该函数地址即可
  • 同事把结果作为参数传递给该函数
  • 这个函数是我写个浏览器的, 所以是回调函数

区别

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

判断同步异步

如果一个函数的返回值处于

  • setTimeout
  • AJAX(即 XMLHTTPRequest)
  • AddEventListener
  • 在这三个内部,那么这个函数就是异步函数

摇骰子

举例1

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

分析

  • 摇骰子() 没有写 return,那就是 return undefined
  • 箭头函数里有return,返回真正的结果
  • 所以这是一个异步函数/异步任务

如何得到结果

    function 摇骰子(fn){
        setTimeout(()=>{
            fn(parseInt(Math.random() * 6) + 1)
        }, 1000)
        // return undefined
    }
    // 用回调函数
    function f1(x){ console.log(x) }
    摇骰子(f1)
    
    // 简化为箭头函数
    摇骰子(x => {
        console.log(x)
    })
    
    // 再简化为, 如果函数不一致就不能这样简化
    摇骰子(console.log)

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

两个结果

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

    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 表示成功和失败

这些方法的不足

不管是方法一还是方法二,都有问题

  • 不规范,名称五花八门,有人用 success + error,有人用 success + fail,有人用 done + fail
  • 容易出现回地狱,代码看不懂
  • 很难进行错误处理

回调地狱举例

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

以 AJAX 的封装为例:解释 Promise 的用法

    ajax = (method, url, options)=>{
        const {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 缩写, 右边是箭头函数

Promise 写法

    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()
        })
    }

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

记忆

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

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

第一步

  • return new Promise((resolve, reject)=>{...})
  • 任务成功则调用 resolve(result)
  • 任务失败则调用 reject(error)
  • resolve 和 reject 会再去调用成功和失败函数

第二步

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

此封装的 AJAX 的缺点

post 无法上传数据

  • request.send(这里可以上传数据)

不能设置请求头

  • request.setRequestHeader(key, value)

怎么解决呢?

  • 花时间把 ajax 写完美(有时间可以做)
  • 使用 jQuery.ajax(这个可以)
  • 使用 axios(这个库比 jQuery 逼格高)

axios 代码示例

    axios.get('/5.json')
        .then( response => 
            console.log(response)
        )

axios 高级用法

JSON 自动处理

  • axios 发现响应的 Content-Type 是 json
  • 就会自动调用 JSON.parse

请求拦截器

  • 你可以在所有请求里加些东西,比如加查询参数

响应拦截器

  • 你可以在所有响应里加些东西,甚至改内容

可以生成不同实例(对象)

  • 不同是实例可以设置不同的配置,用于复杂场景