异步与Promise

异步

  • 如果能直接拿到将结果

    那就是同步

    比如你在医院挂号,你拿到号才会离开窗口

    同步任务可能消耗 10 毫秒,也可能需要 3 秒

    总之不拿到结果你是不会离开的,这就是同步

  • 如果不能直接拿到结果

    那就异步

    比如你在餐厅门口等位,你拿到号后可以去干别的事比如逛街

    你可以每 10 分钟取餐厅问一下排到自己了没(轮询)

    也可以扫码用微信接受通知(回调)

异步举例

  • 以 AJAX 为例

    request.send() 之后,并不能直接得到 response

    console.log(request.response) 试试

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

    我们才能得到 request.response

    这就跟餐厅给你发微信提醒的过程类似

  • 回调 callback

    你写给自己用的函数,不是回调

    你写给别人用的函数,就是回调

    request.onreadystatechange 就是写个浏览器调用的

    意思是你(浏览器)回头调用一下这个函数

  • 简单来理解就是:同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效率更高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
myName.textContent = object.name;
console.log(request.response);
}
}
request.send();
// console.log(request.resopne)
setTimeout(() => {
console.log(request.response);
// 或者将 放在 request.onreadystatechange 函数中
}, 2000)
}
// request.send()执行完,再执行 request.onreadystatechange 函数
// 执行顺序为 response.open() => respone.send() => request.onreadystatechange

onreadystatechange 执行两次

回调

写了却不调用,给别人调用的函数,就是回调。需自行意会

函数例子:

1
2
3
4
5
function f1() {}
function f2(fn) {
fn()
}
f2(f1)

分析:

  1. 我调用 f1 没有?
  2. 我把 f1 传给 f2 (别人)了没有?
  3. f2 调用 f1 了没有?

答:1. 调用了。2.穿了。3.f2 调用了 f1。

那么,f1 是不是我写给 f2 调用的函数? 是。

所以,f1 是回调。


例子2:

1
2
3
4
5
6
7
8
function f1(x) {
console.log(x)
}
function f2(fn){
fn('hello')
}
f2(f1)
// hello

fn(‘hello’) 中的 fn 就是 f1 。

fn(’hello‘) 中的 ’hello’ 会被赋值给参数 x 。

所以 x 就是 ‘hello’。

异步和回调的关系

  • 关联

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

    怎么通知?

    可以让 JS 留一个函数地址给浏览器(电话号码)

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

    同时把任务作为参数传给该函数(通知)

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

  • 区别

    异步任务需要用到回调函数来通知结果

    但回调函数不一定只用在异步任务里

    回调可以用到同步任务里

    array.forEach(n=>console.log(n)) 就是同步回调

判断同步异步

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

  • setTimeout
  • AJAX(即 XMLHttpRequest)
  • AddEventListener

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

还有其他 API 是异步的,遇到再说。

举例说明:

摇骰子, 随机打印出1-6 中的一个数

1
2
3
4
5
6
function 摇骰子() {
setTimeout( () => { // 箭头函数
return parseInt(Math.random() * 6) + 1;
}, 1000)
// return undefined
}

分析:

摇骰子() 里没有写 return,那就是 return undefined

箭头函数里有 return,返回真正结果

所以这是一个异步函数/异步任务。

1
2
const n = 摇骰子();
console.log(n)

如何拿到异步结果?

用回调,写个函数,然后把函数地址给他

1
2
3
4
5
6
7
8
9
10
11
function f1(x) {
console.log(x);
}
// 然后要求 摇骰子函数 得到结果后 把结果作为参数 传给f1
function 摇骰子(fn){
setTimeout( () => {
fn(parseInt(Math.random() * 6) + 1)
}, 1000)
}

摇骰子(f1); // 结果

简化为箭头函数

1
2
3
4
5
6
7
8
9
10
11
function f1(x) { 
console.log(x);
}
摇骰子(f1);
// 改为
摇骰子(x => {
console.log(x);
});
// 再简化为
摇骰子(console.log);
// 如果参数个数不一致就不能这样简化,有个相关面试题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 面试题
const array = ['1', '2', '3'].map(parseInt);
console.log(array);
// 结果为 [1, NaN, NaN]
// parseInt('1', 0, arr) => 1
// parseInt('2', 1, arr) => NaN
// parseInt('3', 2, arr) => NaN

const array = ['1', '2' , '3'].map((item, i , arr) => {
return parseInt(item)
});
console.log(array)
// 正确结果 [1, 2, 3]

// 简写 永远使用箭头函数
const array = ['1', '2', '3'].map((item) => parseInt(item));

异步总结

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

异步任务两个结果,成功或失败

两个方法解决

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

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

方法二:两个回调

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

这些方法的不足

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

  1. 不规范,名称五花八门,有人用 success + error,有人用 success + fail,done + fail
  2. 容易出现==回调地狱== ,代码变得看不懂
  3. 很难进行错无处理

回调地狱举例

1
getUser( user => {    getGroups(user, (groups) => {        groups.forEach( (g) => {            g.filter(x => x.ownerId === user.id)            .forEach(x => console.log(x))        })    })})// 仅示例,这只是四层,二十层呢,代码会很难读
Snipaste_2021-09-25_22-12-10
吐槽回调地狱 来源网络

如何解决回调问题,用 Promise

有什么办法能解决这三个问题:

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

1976年,Daniel P.Friedman 和 David Wis 俩人提出 Promise 思想

后人基于此发明了 Future、Delay、Deferred等

前端结合 Promise 和 JS,制定了 Promise/A+规范

该规范详细描述了 Promise 的原理和使用方法。

以 AJAX 的封装为例,来解释 Promise

Promise MDN

1
// 示例ajax = (method, url, options) => {    const {success, fail} = option; // 析构赋值    // const succes = option.success;    // const fail = option.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.rend();}ajax('get', '/xxx', {    success(response){}, // function 缩写    // success: function(response){}    fail: (request, status) => {}})

Promise 写法:

1
// 先改一下调用姿势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 的源码了。

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

1
// 修改 ajaxajax = (method, url, option) => {    return new Promise((resolve, reject) => {        const {success, fail} = option;        const request = new XMLHttpRequest();        request.open(methon, url);        request.onreadystatechange = () => {            if(request.readyState === 4) {                if(request.status < 400) {                    // 成功                    resolve.call(null, request.response);                }else if(request.status >= 400) {                    // s                    reject.call(null, request);                }            }        }        request.send();    });}

小结

第一步:

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

第二步:

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

我们自己封装的 ajax 的缺点:

post 无法上传数据。request.send(这里可以上传数据)

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

使用:

Jquery.ajax

axios(推荐)