一、异步与回调
- JS异步编程模型
1.什么是异步?什么是同步?
- 如果能直接拿到结果——同步
1.比如在医院排队挂号,你拿到号才能离开窗口
2.同步任务可能消耗10毫秒,也可能需要3秒
3.总之不拿到结果不会离开 - 如果不能直接拿到结果——异步
1.比如在餐厅门口等位置,拿到号可以去逛街,先做其他事
2.什么时候能真正吃饭呢?
3.你可以每10分钟取餐厅问一下(轮询)
4.你也可以扫码用微信接收通知(回调) - 轮询和回调都可以拿到结果,但两者形式不同
前者自己去问,后者是接收通知
2.异步举例
2.1 以AJAX为例
- request.send() 之后,并不能直接得到response
- 可以在request.send() 之后加一个 console.log(request.response),是没办法得到结果的
1.但当添加一个延迟函数,2秒之后在console.log,就可以得到response
2.这是因为:在JS中成功发送网络请求到得到响应大概需要几百毫秒至一两秒之间 - 必须要等到
readyState
变成4后,浏览器回头调用request.onreadystatechang
函数 - 我们才能得到
request.response
- 就像是餐厅吃饭排队,我们留一个电话号码在那里,到号就给我们打电话
这里是:先写一个函数在那里,一旦readyState=4,浏览器就去调用函数
request.send();
console.log(request.response);//无法打出response
setTimeout(() => {//两秒后可以打出response
console.log(request.response);
}, 2000);
2.2 回调 callback
- 回头调用:写了一个函数却不自己调用,给别人调用,就是回调。[回头你调用以下呗~]
- 你写给自己用的函数,不是回调
- 你写给别人用的函数,就是回调
request.onreadystatechang
就是我写给浏览器调用的- 意思就是 你(浏览器)回头调一下这个函数
- 在中文里,[回头] 也有 [将来] 的意思,比如 [回头请你吃饭]
2.3 回调举例
- 代码
function f1(){}
function f2(fn){
fn()
}
f2(f1)
- 分析
1.我调用 f1 没有? ——没有
2.我把 f1 传给 f2(别人) 了没有——传了
3.f2 调用 f1 了没有?——f2 调用了 f1
4.那么,f1 是不是我写给 f2 调用的函数?——是
5.所以 f1是回调,f2不是回调
3.异步和回调的关系
3.1 关联
- 异步任务需要在得到结果时通知JS(已经走了的JS代码)来拿结果
- 怎么通知呢?
- 可以让 JS 留一个函数地址(电话号码)给浏览器
- 异步任务完成时,浏览器调用该函数地址即可(拨打电话)
- 同时把结果作为参数传给该函数(电话里说可以来吃了)
- 这个函数使我写给浏览器调用的,所以是回调函数
3.2 区别
- 异步任务需要用到回调函数来通知结果(但不是一定要用回调,还可以用轮询)
- 回调函数不一定只用在异步任务里
- 回调可以用到同步任务里
array.forEach(n=>console.log(n))
就是同步回调
传了一个函数给 forEach ,但没有异步的代码
4.异步函数的例子
- 如何知道一个函数是同步还是异步?
4.1 判断同步异步
- 如果一个函数的返回值处于
1.setTimeout
2.AJAX
(即:XMLHttpRequest)
3.AddEventListener
- 返回值在这三个东西内部,那么这个函数就是异步函数
还有其他API,现在还没说明 - AJAX可以设置为同步?
可以,但这样做会让页面卡住,因为同步的话在请求之后页面2s期间什么都不能做,做了也没反应,浏览器就傻等着得到结果才会进行下一步。
4.2 摇骰子
- 代码
function 摇骰子(){
setTimeout(()=>{//箭头函数
return parseInt(Math.random()*6)+1 //return一个1~6的数
},1000)
}
- 分析
1.摇骰子() 函数没有写return
,那就是 return undefined
2.箭头函数里有return,返回真正的结果
3.所以这是一个异步函数/异步任务
4.3 摇骰子(续)
- 添加代码
const n = 摇骰子()
console.log(n)
- 得到的结果是:undefined(因为摇骰子函数没有return)
- 那如何拿到异步的结果?
- 答:用回调。写一个函数,然后把函数的地址给摇骰子。
一个很明显的回调特征:声明了一个函数f1,自己不调,给别人调
function f1(x){ console.log(x) }
摇骰子(f1)
- 要求把摇骰子函数得到结果后,把结果作为参数传给f1,并且调用f1
function 摇骰子(fn){
setTimeout(()=>{//箭头函数
fn(parseInt(Math.random()*6)+1) //return一个1~6的数
},1000)
}
4.4 摇骰子(简化)
- 摇骰子函数必须调用f1
- 简化箭头函数
由于 f1 声明之后只用了一次,可以删掉 f1
用两次才需要声明,用一次可以直接用匿名函数 摇骰子(x=>{console.log(x)})
还可以简化为摇骰子(console.log)
注意:如果参数个数前后不一致,就不能这样简化,简化后默认前后参数一样- 自调用:一个函数后面加了括号表示调用
一个函数后面没有括号,就表示没有调用
4.5 一个刁钻的面试题(错误的参数简化)
const array = ['1','2','3'].map(parseInt)
console.log(array)
- 注:
1.parseInt(string, radix)
解析一个字符串并返回指定基数的十进制整数, radix 是2-36之间的整数,表示被解析字符串的基数。(如:radix 为2,表示二进制)
2.map()
方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。 - 简化前的正确代码为:
const array = ['1','2','3'].map((item,i,arr)=>{//参数为:item、下标、原数组
return parseInt(item)
})
console.log(array)
- 由于简化后,默认前后参数一致,因此系统默认:
parseInt(item,i,arr)
- 但 parseInt 最多接收 2 个参数,arr 被忽略,而下标 i 被当做 radix 基数来使用
parseInt('1',0,arr) => 1 //arr被忽略,0为无效参数,则结果为1
parseInt('2',1,arr) => NAN //2被当做1进制数解析为十进制,则2不合法
parseInt('3',2,arr) => NAN //3被当做二进制数解析,不合法
- 因此:永远使用箭头函数,不要随便简化
const array = ['1','2','3'].map(item => parseInt(item))
console.log(array)
5.总结
- 异步任务不能拿到结果
- 于是传一个回调给异步任务
- 异步任务完成时调用回调
- 调用的时候把结果作为参数