何为同步?何为异步?
同步
能直接拿到结果,就是同步
比如在医院挂号,需要拿到号码我们才会离开窗口
同步任务可能消耗几毫秒到几秒不等,但有一个原则就是不拿到结果是不会离开的
异步
如果不能直接拿到结果,就是异步
比如在餐厅门口排队吃饭,我们在拿到号码之后,可以不用在餐厅门口傻等,在这段时间可以去做一些其他事,
那什么时候可以进店吃饭?
你可以每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等等,而且第一个参数是成功,还是第二个参数是成功,也没有明确规定
-
容易出来回调地狱,导致代码很难读
-
很难进行错误处理
为了避免这三个问题,前端程序员开始从其他领域借鉴方法 —— 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使用小结
第一步:
return new Promise((resolve, reject) =>{...})- 任务成功调用
resolve.call(null, result) - 任务失败就调用
reject.call(null, error) - resolve和reject会再去调用成功和失败函数
第二步
- 使用
.then(success,fail)传入成功和失败函数
当然,Promise还有很多高级用法,之后边学边总结