JavaScript一般来说是单线程的(single threaded),此线程称为主线程(main thread)
一个线程是一个基本的处理过程,程序用它来完成任务。每个线程一次只能执行一个任务
同步代码:只有当一段代码块(或一个任务)完成后才能执行下一段代码块(或任务),这些代码称为同步代码
异步代码:JS通过 Web workers 可以把任务交给名为worker的单独线程,交给worker的代码就称为异步代码
同步代码的执行有可能会导致阻塞,但异步代码在执行的过程中就不会阻塞主线程
Web workers 并不能直接访问DOM——不能直接让一个worker直接更新UI
当一个函数依赖在它之前执行的异步代码的结果,需要如何解决?
- 起初的解决方案是使用回调函数(callback)
- 但容易出现回调地狱
- 且不容易处理错误
- 后来Promise的诞生不仅处理了异步代码结果问题,还避免了使用callback出现的弊端
以ajax为例
ajax就是异步操作JS向服务器发送请求并获取响应const request = new XMLHttpRequest()
- request.send()之后并不能直接得到request.response
- request.onreadystatechange(callback)就是异步代码,需要readystate改变时,调用里面的callback
- 而这个时候我们才可以得到request.response,并在callback中操作它
回调callback
写一个f1函数,传给f2函数当参数,让f2函数调用
function f1(){} 声明f1函数
function f2(fn){ fn() } 声明f2函数
function f2(f1) 调用f2函数,将f1作为参数传入f2函数,让f2函数调用它
所以f1就是回调
异步和回调的关系
关联:
异步任务在得到结果会通知JS
通过JS留给浏览器的的回调函数,把结果作为参数传给回调函数
区别:异步任务需要用到回调函数来通知结果
但函数回调不一定只用在异步任务里,回调函数同样可以用于同步任务
array.forEach(n=>console.log(n))就是同步回调
判断同步异步
如果一个函数的返回值处于
setTimeout
AJAX(即XMLHttpRequest)
addEventListener
这三个东西内部,那么这个函数就是异步函数
例:摇骰子函数
function f2(){
setTimeout(()=>{
return parseInt(Math.random()*6,10)+1
},1000)
}
问:如何拿到异步结果?
答:用回调。写个函数,然后把函数传给骰子函数,把结果传入回调函数
function f1(x){console.log(x)}
//结果:parseInt(Math.random()*6,10)+1
//回调函数: f1
function f2(fn){
setTimeout(()=>{
fn(parseInt(Math.random()*6,10)+1)
},1000)
}
f2(f1)
//调用摇骰子函数,把回调函数f1当做参数传入
//摇骰子函数的输出结果就是1~6了
//由于f1函数声明过后只用了一次,因此可以简化成箭头函数,节省变量声明
function f2(fn){
setTimeout(()=>{
fn(parseInt(Math.random()*6,10)+1)
},1000)
}
//调用时直接
f2(x=>console.log(x))
//如果回调函数的参数个数一致就能进一步简化
f2(console.log)
//这样写就表示,f2给f1传多少参数,f1就拿多少参数。
//面试题:
const array = ['1','2','3'].map(parseInt())
console.log(array)
/*问:输出结果是什么?为什么?
答:输出结果是 [1,NaN,NaN],
map函数会得到三个结果,上述代码里map会将这三个结果作为参数传入到匿名回调函数里,
回调函数再把这三个参数传入它里面调用的parseInt()函数
上述简化代码可以还原为*/
const array = ['1','2','3'].map((item,index,arr)=>{ return parseInt(item,index,arr)})
/*因为parseInt函数只接受两个参数string和radix,所以会自动忽略map传的第三个函数
对应起来就是item传到string位置,index传到radix位置
会得到parseInt("1",0) === 1 ,当传入的radix=0 ,相当于不传
parseInt("2",1) === NaN radix = 1 按照一进制解析 "2",将其转为十进制数
parseInt("3",2) === NaN radix = 2 按照二进制解析 "3",将其转为十进制数
所以最后得到的结果 array = [1,NaN,NaN]*/
//正确的写法是
const array = ['1','2','3'].map((item)=>parseInt(item))
异步总结:
异步任务不能拿到结果
于是我们传一个回调函数给异步任务
异步任务完成时调用回调
调用的时候把结果作为参数
//如果异步任务有两个结果,成功和失败需要怎么做
//方法一:回调函数接收两个参数
fs.readFile(./1.txt,(error,data)=>{
if(error){console.log("失败")
}else{console.log(data.toString())
}
})
//方法二:写两个回调函数
ajax('get','/1.json',data=>{},error=>{})
//或
ajax('get','/1.json',{success:()=>{},fail:()=>{}})//接收一个对象,有两个key表示成功和失败
/*
方法一和二都存在问题:
不规范,关于成功和失败的用词和位置五花八门
容易出现回调地狱
很难进行错误处理
该怎么处理这些问题:
规范回调的名字和顺序
拒绝回调地狱,让代码的可读性更强
很方便捕捉错误
*/
//以ajax的封装为例
ajax = (method ,url ,options) =>{
const {success,fail} = options //解构赋值
const request = new XMLHttpRequest()
request.open(method,url)
request.onreadystatechange = ()=>{
if(
request.readyState === 4 &&
request.status >= 200 &&
request.status < 400
){
success.call(null,request.response)
}else if(request.readyState === 4 &&
request.status >= 400){
fail.call(null,request,request.status)
}
}
request.send()
}
ajax('get','/xxx',{success(response){},fail:(resquest,status)=>{}
})//success是function缩写,fail是箭头函数
//但是promise觉得这ajax封装得太傻了
//先更改调用方式:原来的用了两个回调函数,还是用了success和fail
//改成promise思想封装ajax的写法:
ajax('get','/xxx').then((response)=>{},(request,status)=>{})
//方法then的两个参数就是回调函数,两个回调函数接收的参数就是从异步操作那传来的结果
成功时执行的回调函数接收一个最终结果作为参数
失败时执行的回调函数接收一个拒绝原因作为参数
/*
虽然该方法依旧是回调
但是不需要即success和fail了
then的第一个参数就是success
第二个参数就是fail
通过改写ajax封装让ajax()返回一个含有then()方法的对象
*/
ajax = (method,url)=>{
return new Promise((resolve,reject)=>{
const request = new XMLHttpRequest()
request.open(method,url)
request.onreadystatechange = ()=>{
if(request.readyState === 4){
if(request.status < 400){
resolve.call(null,request.response)
}else if(request.status >= 400){
reject.call(null,request,request.status)
}
}
}
request.send()
})
}
/*
第一步:return new Promise((resolve,reject)=>{...})
任务成功则调用resolve(result)
失败则调用reject(error)
resolve和reject分别会再去调用成功和失败的函数
第二步:使用.then(success,fail)传入成功和失败函数
*/