知识铺垫
什么是回调函数?
概念问题(解释什么是回调函数,知识梳理)
- 编程分为两类
- 系统编程 编写库
- 应用编程 利用写好的库来编写具有某种功用的程序,也就是应用
- 系统程序员会给自己写的库留下一些接口即 api
一般情况下,应用程序会时常通过api调用库里所预先备好的函数,但是有些库函数却要求应用先传给他一个函数,好在合适的时候调用,以便完成目标任务。这个被传入的后又被调用的函数就称为回调函数。
打个比方,有一家旅馆提供叫醒服务,但是要求旅客自己决定叫醒的方法。可以是打客房电话,也可以是派服务员去敲门,睡得死怕耽误事的,还可以要求往自己头上浇盆水。这里,“叫醒”这个行为是旅馆提供的,相当于库函数,但是叫醒的方式是由旅客决定并告诉旅馆的,也就是回调函数。而旅客告诉旅馆怎么叫醒自己的动作,也就是把回调函数传入库函数的动作,称为登记回调函数(to register a callback function)。如下图所示(图片来源:维基百科):
可以看到,回调函数通常和应用处于同一抽象层(因为传入什么样的回调函数是在应用级别决定的)。而回调就成了一个高层调用底层,底层再回过头来调用高层的过程。(我认为)这应该是回调最早的应用之处,也是其得名如此的原因。
在js中的回调函数怎么理解:
举个例子:
你有事去隔壁寝室找同学,发现人不在,你怎么办呢?
方法1,每隔几分钟再去趟隔壁寝室,看人在不
方法2,拜托与他同寝室的人,看到他回来时叫一下你
前者是轮询,后者是回调。
那你说,我直接在隔壁寝室等到同学回来可以吗?
可以啊,只不过这样原本你可以省下时间做其他事,现在必须浪费在等待上了。把原来的非阻塞的异步调用变成了阻塞的同步调用。
JavaScript的回调是在异步调用场景下使用的,使用回调性能好于轮询。
**
异步在什么场景下会用到
概念:
- 同步:代码从上到下执行,一段代码执行之后才会执行下一段代码
- 异步:每个任务有自己的回调函数,前一个任务结束后,接下来会执行回调函数,这时候不需要等待前一个任务结束就开始执行后一个任务,程序的执行顺序与任务的排列顺序是不一致的,异步的。
为什么会出现异步?
ajax操作,在服务器端,如果是同步操作,那么将会耗时很长。因为只要用一个任务耗时很长那么后面的任务都需要排队等待。
异步的场景
- 网络请求:常见的ajax
- IO操作:比如readFile
- 定时器:setTimeout
Promise与异步编程
promise:可以指定一些稍后执行的代码(比如事件和回调函数)并显示的表明该段代码是否执行成功。你可以根据代码执行的成功与否将promise串联起来以便让代码看起更清晰和容易调试
异步编程的背景
js引擎如何执行代码,在它们准备好执行时将它们放置到任务队列,当代码由js引擎执行完毕后,引擎通过event loop找到并执行队列中的下一个任务。
event loop :javascript引擎内部的线程用来监控代码的执行情况和管理任务队列。
事件模型
let button = document.getElementById("my-button")
button.onclick = function(event){
console.log("clicked")
}
回调模式
回调函数模式类似于事件模型,因为异步代码也会在后面的一个时间点才执行,不同之处在于需要调用的函数(即回调函数)是作为参数传入的。
解析:readFile()会立即开始执行,在开始读取磁盘时暂停,这时候console.log(“hi”) 会在readFile()调用后立即进行输出,早于console.log(contents)。当readFile() 结束操作后,它会将回调函数以及相关参数作为一个新的作业添加到作业队列的尾部。在之前的作业全部结束后,该作业才会执行。
这个例子中
function是一个回调函数,这个回调函数是readFile的一个参数,作为参数传入
readFile("example.txt",function(err,contents){
if(err){
throw err;
}
console.log(contents)
})
console.log("hi")
缺点
- 事件模式倾向于在出错时不被触发
- 而在回调函数模式中你必须始终记得检测错误参数。
但是当串联多个调用的时候会陷入回调地域。
promise基础
promise的生命周期
- 初始:挂起态 pending 表示异步操作尚未结束。(也被认为是未决的(unsettled))
- 异步结束:已决 settled,并进入两种状态之一
- 1.已完成(fulfilled)promise的异步加载已成功结束
- 2.已拒绝(rejected)promise的异步操作未成功结束,可能是一个错误,或由其他原因导致
then() 方法,接受两个参数。
第一个:promise被完成时要调用的函数,与异步操作关联的任何附加数据都会被传入这个完成函数
第二个:promise被拒绝时要调用的函数,拒绝函数会被传入与拒绝相关联的任何附加数据
then()的两个参数都是可选的,因此可以随意监听完成与拒绝的任意组合形式
即使完成或拒绝处理函数在promise已经被解决之后才添加到作业队列,他们仍然会被执行。这允许你随时添加新的完成或者拒绝处理函数,并保证他们会被调用。
下面代码,完成处理函数又为同一个promise添加了另一个完成处理函数,这个promise此刻已经完成了,因此新的处理程序就被添加到任务队列中,并在就绪时(前面的作业执行完毕后)被调用。
let promise = readFile("example.txt")
//原始的完成处理函数
promise.then(function(contents){
console.log(contents)
//现在添加一个
promise.then(function(contents){
console.log(contents)
})
})
创建未决(初始状态)的promise
新的promise使用promise构造器来创建,构造器接收一个参数。
这个参数就是被称为执行器的函数。这个函数会被传递 resolve() 函数与 reject()函数作为参数
resolve() 在执行器成功结束时被调用。reject()函数表明执行器的操作已失败。
let fs = require('fs')
function readFile(filename){
return new Promise(function(resolve,reject){
fs.readFile(filename,{encoding:"utf-8"},function(err,contents){
if(err){
reject(err)
return
}
resolve(contents)
})
})
}
let promise = readFile("example.txt")
promise.then(function(contents){
console.log(contents)
},function(err){
console.log(err.message)
})
执行器(也就是promise)会在readFile()被调用时立即运行,当resolve() 或 reject()在执行器内部被调用时,一个作用被添加到作业队列中,以便决议(resolve)这个promise.这被称为作业调度。
理解:此时的等于是调用 .then的时候才会执行这个作业队列、
例如:setTimeout() 函数能让你指定一个延迟时间,延迟之后作业才会被添加到队列
setTimeout(function(){
console.log("Timeout")
},500)
console.log("hi")
上述代码安排一个作业在500毫秒之后被添加到作业队列,此处先输出hi。
注意:此处的输出顺序与500毫秒没有关系,而与setTimeout()的机制有关。把延时改为0,也是一样的。
//在o毫之后添加此函数到作业队列
setTimeout(function(){
console.log("Timeout")
},0)
console.log("hi")
promise工作方式与之类似,promise的执行器会立即执行,早于源代码中在其之后的任何代码。例如:
let promise = new Promise(function(resolve,reject){
console.log("promise")
resolve()
})
console.log("hi")
//promise
//hi
调用resolve()触发一个异步操作,传递给then()与catch()的函数会异步的被执行,并且它们也被添加到了作业队列(先进队列再执行)
let promise = new Promise(function(resolve,reject){
console.log("promise")
resolve()
})
promise.then(function(){
console.log("resolved")
})
console.log("hi")
//promise
//hi
//resolved
原因:因为完成处理函数与拒绝处理函数总是在执行器的操作结束后被添加到作业队列的尾部。
创建已决的promise
使用 Promise.resolve()
Promise.resolve() 方法接受单个参数并返回一个处于完全态的Promise.这意味着没有任何作业调度会发生,并且你需要向Promise添加一个或更多的完成处理函数来提取这个参数值
**
let promise = Promise.resolve(42)
promise.then(function(value){
console.log(value) //42
})
使用Promise.reject()
Promise.reject()创建一个已拒绝的Promise,被创建的Promise处于拒绝态。
let promise = Promise.reject(42)
promise.catch(function(value){
console.log(value) //42
})
非Promise的Thenable
thenable : 即带有"then" 方法
当一个对象拥有一个能接受resolve与reject参数的then()方法,该对象就会被认为是一个非Promise的thenable、
可以调用Promise.resolve()来将thenable转换为一个已完成的Promise
说明:
let thenable = {
then:function(resolve,reject){
resolve(42)
}
}
let p1 = Promise.resolve(thenable)
p1.then(function(value){
console.log(value)
})
let thenable = {
then:function(resolve,reject){
reject(42)
}
}
let p1 = Promise.resolve(thenable)
p1.catch(function(value){
console.log(value)
})
串联Promise
每次对then()或catch()的调用实际上创建并返回了另一个Promise,仅当前一个Promise被完成或拒绝时,后一个Promise才会被决议,
let p1 = new Promise(function(resolve,reject){
resolve(42)
})
p1.then(function(value){
console.log(value)
}).then(function(){
console.log("finished")
})
//42
//finished
对 p1.then() 的调用返回了第二个Promise,又在这之上调用了 then().仅当第一个Promise已被决议后,第二个then()的完成处理函数才会被调用。假若你在此例中不使用串联,如下
let p1 = new Promise(function(resolve,reject){
resolve(42)
})
let p2 = p1.then(function(value){
console.log(value)
})
p2.then(function(){
console.log("finished")
})
捕获错误
Promise链允许你获取前一个Promise的完成或拒绝处理函数中发生的错误。
p1的完成处理函数抛出一个错误,链式调用指向了第二个Promise上的catch () 方法,能通过此拒绝处理函数接收前面的错误。
let p1 = new Promise(function(resolve,reject){
resolve(42)
})
p1.then(function(value){
throw new Error("Boom")
}).catch(function(error){
console.log(error.message) //Boom
})
若是一个拒绝处理函数抛出了错误,情况也是一样。
let p1 = new Promise(function(resolve,reject){
throw new Error("Explosion")
})
p1.catch(function(error){
console.log(error.message) //Explosion
throw new Error("Boom")
}).catch(function(error){
console.log(error.message) //Boom
})
此处的执行器抛出了一个错误,就触发了p1这个Promise的拒绝处理函数,该处理函数随后抛出了另一个错误,并被第二个Promise的拒绝处理函数所捕获。链式Promise调用能察觉到链中其他Promise的错误。
为了确保能正确处理任意可能发生的错误,应当始终在Promise链尾部添加拒绝处理函数。