异步和 Promise Note-FrontEnd-29

408 阅读8分钟

有关异步和 Promise 的相关知识(JS 异步编程模型,这节内容特别硬核,你品,你细品),内容包括异步和同步的区别、回调、异步和回调的关系、举例判断同步和异步、异步任务有两个结果、怎么解决回调问题 - Promise、以 AJAX 的封装为例来解释 Promise 的用法、jQuery.ajax 和 axios、总结。

一、异步和同步的区别

1. 区别

如果能直接拿到结果,那就是同步。比如你在医院挂号,你拿到号才会离开窗口,同步任务可能消耗 10 毫秒,也可能需要 3 秒,总之不拿到结果你是不会离开的。

如果不能直接拿到结果,那就是异步。比如你在餐厅门口等位,你拿到号可以去逛街,什么时候才能真正吃饭呢?,你可以每 10 分钟去餐厅问一下(轮调),你也可以扫码用微信接收通知(回调)。

2. 异步举例 - 以 AJAX 为例

  • request.sent() 之后,并不能直接得到 response
  • 必须等到 readyState 变为 4 后,浏览器回头调用 request.onreadystatechange 函数
  • 我们才能得到 request.response
  • 这跟餐厅给你发送微信提醒的过程是类似的。

回调 callback

  1. 你写给自己用的函数,不是回调
  2. 你写给别人用的函数,就是回调
  3. request.onreadystatechange 就是我写给浏览器调用的
  4. 意思就是你(浏览器)回头调一下这个函数
  5. 在中文里,「回头」也有「将来」的意思,如「我回头请你吃饭」

二、回调

写了却不调用,给别人调用的函数,就是回调,「回头你调用一下呗」大家意会

1. 回调举例

// 把函数 1 给另一个函数 2
function f1(){}
function f2(){
    fn()
}
f2(f1)
  1. 我调用 f1 没有?答:没有调用
  2. 我把 f1 传给 f2(别人)了没有?答:传了
  3. f2 调用 f1 了没有?答:f2 调用了 f1
  • 那么,f1 是不是我写给 f2 调用的函数?答:是
  • 所以,f1 是回调。

2. 抬杠 1

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

如果 f2 没有调用 f1 呢?

  • f2 有病啊?它不调用 f1,那它为什么要接受 fn 参数

3. 抬杠 2

function f1(){}
function f2(fn){
    fn()
}
fn('字符串')

如果我传给 f2 的参数不是函数呢?

  • 你有病啊?用函数之前不看看函数的文档吗?
  • 会报错:fn 不是一个函数。看到报错你不就知错了?

4. 抬杠 3

function f1(x){
    console.log(x)
}
function f2(fn){
    fn('你好')
}
f2(f1)

f1 怎么会有一个 x 参数?

  • fn('你好') 中的 fn 就是 f1 对吧
  • fn('你好') 中的 ‘你好’ 会被赋值给参数 x 对吧
  • 所以 x 就是 '你好' 啊!
  • x 可以改成任意其他名字,x 表示第一个参数而已

三、异步和回调的关系

1. 回调和异步是什么

回调就是我把一个函数传给你,我可以直接传给你,也可以把它传给你全局对象上面,比如说 request 上面,然后你去 request 上面找。或者我把这个函数放在你手里

异步就是我不能马上得到结果给你,你要等一会

2. 关联

  • 异步任务需要在得到结果时通知 JS 来拿结果
  • 怎么通知呢?
  • 可以让 JS 写一个函数地址(电话号码)给浏览器
  • 异步任务完成时浏览器调用该函数地址即可(拨打电话)
  • 同步时把结果作为参数传给该函数(电话可以来吃了)
  • 这个函数是我写给浏览器调用的,所以是回调函数

3. 区别

  • 异步任务需要用回调函数来通知结果
  • 但回调函数不一定只用在异步任务里
  • 回调可以用到同步任务里
  • 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,返回真正的结果
  • 所以这是一个异步函数/异步任务

2. 摇骰子 - 续 1

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

那么怎么拿到异步结果?

答:可以用回调。写个函数,然后把函数地址给它

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

然后我要求摇骰子函数得到结果后把结果作为参数传给 f1

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

3. 摇骰子 - 续 2

摇骰子函数不调用 fn 怎么办?

答:不调?不调我neng死写代码的人(包括自己)

简化为箭头函数

由于 f1 声明之后只用了一次,所以可以删掉 f1

function f1(x){ console.log(x) }
摇骰子(f1)
// 改为
摇骰子(x=>{
    console.log(x)
})
// 因为传的参数和接收的参数个数一致,所以可以再简化为
摇骰子(console.log)
// 如果参数个数不一致就不能这样简化,有个面试题

4. 一道题

问打印出什么?

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

答:

[1,NaN,NaN]

map 传了 3 个参数,parseInt 于是接收 3 个参数,这不是我们想要的方式

map((item,i,arr)=>{
    return parseInt(item,i,arr)  // 数组元素、下标、数组
    // parseInt('1',0,arr) => 1  // 第二个参数 0 无效,正常解析为 1
    // parseInt('2',1,arr) => NaN // 把 '2' 以一进制数字解析,NaN
    // parseInt('3',2,arr)  // 把 '3' 以二进制数字解析,NaN
})

正确的写法不能简写

map((item,i,arr)=>{
    return parseInt(item)
})

五、异步任务有两个结果的处理

1. 方法一:回调接收两个参数呗

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

2. 方法二:搞两个回调呗

ajax('get','/1.json', data=>{}, error=>{})
// 前面函数是成功回调,后面函数是失败回调
ajax('get', '/1.json', {
    success: ()=>{}, fail: ()=>{} 
})
// 接收一个对象,对象有两个 key 表示成功和失败

3. 这些方法的不足

  1. 不规范,名称五花八门,有人用 success + error,有人用 success + fail,有人用 done + fail
  2. 容易出现回调地狱,代码变得看不懂
  3. 很难进行错误处理
getUser( user => {
    getGroups(user, (groups)=>{
        groups.forEach( (g)=>{
            g.filter(x => x.ownerId === user.id)
                .forEach(x => console.log(x))
        })
    })
})
// 这还只是四层回调,你能想象20层回调吗?

六、怎么解决回调问题 - Promise

  1. 规范回调的名字或顺序
  2. 拒绝回调地狱,让代码可读性更强
  3. 很方便地捕获错误

前端程序员开始翻书了

  • 1976 年,Daniel P.Friedman 和 David Wise 两人提出 Promise 思想
  • 后人基于此发明了 Future、Delay、Deferred 等
  • 前端结合 Promise 和 JS,制定了 Promise/A + 规范
  • 该规范详细描述了 Promise 的原理和使用方法

七、以 AJAX 的封装为例来解释 Promise 的用法

1. 改写成 Promise 写法

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, status)=>{}
}) // 左边是 function 缩写,右边是箭头函数,记下来别问

先改一下调用的姿势

ajax('get', '/xxx', { 
    success(response){}, fail: (request, status)=>{}
})
// 上面用到了两个回调,还使用了 success 和 fail

改成 Promise 写法

ajax('get', '/xxx')
    .then((response)=>{}, (request, status)=>{})
  • 虽然也是回调,但是不需要记 success 和 fail 了
  • then 的第一个参数就是 success,then 的第二个参数就是 fail
  • 请问 ajax() 返回了个啥?
  • 返回了一个含有 .then() 方法的对象呗
  • 那么再请问如何得到这个含有 .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){
            // 成功就调用 resolve,失败就调用 reject
                if(request.status < 400){
                    resolve.call(null, request.response)
                }else if(request.status >= 400){
                    reject.call(null, request)
                }
            }
        }
        request.send()
    })
}

2. 小结

第一步:

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

第二步:

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

3. 我们封装的 ajax 的缺点

  • post 无法上传数据,request.send(这里可以上传数据)
  • 不能设置请求头,request.setRequestHeader(key,value)
  • 怎么解决呢?
    • 花时间把 ajax 写到完美(有时间可以做)
    • 使用 jQuery.ajax(这个可以)
    • 使用 anios(这个库比 jQuery 逼格高)

八、jQuery.ajax 和 axios

1. jQuery.ajax

已经非常完美,进入 jQuery 的文档,搜索 ajax,找到 jQuery.ajax(网址),看看参数说明,然后直接看代码示例,看看 jQuery 的封装,就知道自己的封装是辣鸡了

封装的优点:支持更多形式的参数,支持 Promise,支持的功能超多

2. axios

目前最新的 AJAX 库,显然它抄袭了 jQuery 的封装思路,代码示例

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

3. anios 高级用法

  1. JSON 自动处理
    • axios 如何发现响应的 Content-Type 是 json
    • 就会自动调用 JSON.parse
    • 所以说正确设置 Content-Type u是好习惯
  2. 请求拦截器
    • 你可以在所有请求里加些东西,比如加查询参数
  3. 响应拦截器
    • 你可以在所有响应里加些东西,甚至改内容
  4. 可以生成不同实例(对象)
    • 不同的实例可以设置不同的配置,用于复杂场景

九、总结

  1. 异步是什么
  2. 异步为什么会用到回调
  3. 回调有哪三个问题:地狱、名字、错误处理
  4. Promise 是什么:1976 年的一种设计模式
  5. 如何使用 Promise: 背下来五个词
  6. 如何使用 Axios: 发个请求试用看看
  7. Promise 是前端解决异步问题的统一方案

「资料来源:©饥人谷」