同步与异步
同步:等待结果(下一步是是否等待上一步执行完成)
异步: 不等待结果(下一步是是否等待上一步执行完成)
异步长伴随着回调一起出现,但是异步不是回调,回调也不一定是异步
栗子:
// sleep函数,询问当前时间是否已经过了3秒
function sleep(seconds) {
var start = new Date
while(new Date - start < seconds*1000){
}
return
}
console.log(1)
sleep(3)
console.log('wake up')
console.log(2)
// 结果: 1 -> 3秒后输出数字3 -> wake up -> 2
A: 如何让JS在3秒之内什么也不做?
B: JS不支持3秒之内什么也不做,JS程序不能停下来
C: 解决的办法就是让JS在3秒之内不停的做无意义的事情(如: 不停的看表是否到了3秒,如果没到就什么也不做,否则return)
console.log('wake up')这一步会等待上一步 sleep(3)的执行结果,因此sleep中的代码是同步的代码
A: 那么有没有办法让console.log('wake up')
这一步不等待上一步 sleep(3)
的执行结果?
B: 可以使用异步的sleep
栗子:
function(seconds,fn) {
setTimeout(fn,seconds * 1000)
}
console.log(1)
sleep(3, () => console.log('wake up'))
console.log(2)
// 结果: 执行顺序 1 -> 2 -> 3秒后执行'wake up'
注:
sleep(3, () => console.log('wake up'))
这段代码中,3秒之内JS引擎不去处理异步的代码而是交给浏览器去处理,jS引擎继续做其他的同步的操作,3秒后由浏览器去调用函数fn,拿到异步的结果并且通知JS引擎。 异步让CPU执行时间变少,JS引擎空闲的时间多了许多,不是活生生的让执行时间变少了,而是把比较耗时的异步操作交给浏览器去处理了。也就是说JS空闲的这段时间,实际上是浏览器中的计时器在工作(很有可能每隔一段时间去检查是不是该执行定时器中异步的代码了)
类似完成了这样的会话:
JS引擎: 浏览器同志3秒后帮我调用一下函数fn
浏览器: OK!你去忙你的,异步的事情交给我来处理,拿到执行结果我通知你!
前端经常遇到的异步
异步请求资源
栗子:
document.getElementByTagNames('img')[0].width // 宽度为0
console.log('done')
A: 为什么第一次加载页面的时候获取到的img的宽度为0呢?document.getElementByTagNames('img')[0]
和width
都是同步的代码啊
B: 原因是请求图片资源时,从浏览器发送http请求到服务器响应,再到浏览器拿到服务器返回的图片再渲染到页面是需要时间的而jS执行代码是一瞬间即完成的。也就是说,当JS去获取图片宽度的时候,浏览器还没拿到图片
A: 那么该如何解决这个问题?
B: 解决办法就是让img加载成功后触发onload事件,onload事件上绑定一个回调函数,函数体中为即将要执行的异步代码
栗子:
document.getElementByTagNames('img')[0].onload = function () {
console.log(this.width)
console.log('real done')
}
注:
回调有多种形式,上面有作为参数的回调,还有这里的事件中的回调,回调的形式胡、后面会讲
面试题中的异步
栗子:
let liList = document.querySelectorAll('li')
for (var i=0;i< lilist.length;i++) {
liList[i].onclick = function () {
console.log(i)
}
}
A: 页面中如果有6个li,为什么不管点击那个li都输出i为6呢?而不是点击第一几个li就输出i为第几个?
B: 首先JS中存在变量提升,i提升为全局的i,那么6次循环中用的都是同一个i
A: 那么异步的代码在哪里?
B: 浏览器不会等你click了才输出i,i已经计算出来为6放到内存中了,这一过程几乎是一瞬间完成的,当我们click的时候就是从内存中拿到的那个全局作用域下的那个i
A: 如何解决呢?
B:将var改为let,这样每次循环中用到的变量就是属于自己的i了而不是共用的
AJAX 中的异步
栗子:
// 同步的请求当前页面的内容
let requst = $.ajax({
url: '.',
async: false
})
console.log(request.responseText)
A: 函数ajax发送完http请求之后就会等待响应,这样存在一个问题,在这段时间里用户什么也做不了(JS就停在这里傻傻的等...);是不是可改成异步的呢?改成异步的怎么做呢?异步中可以直接拿到异步获取到的结果吗?
B: 异步中是不能直接拿到结果的,因为需要等一段时间才能拿到结果,我们可以给浏览器一个success函数,告诉浏览器如果拿到响应的内容就麻烦它调一下函数success,并将请求的结果放在函数的第一个参数中
栗子:
// 同步的请求当前页面的内容
$.ajax({
url: '.',
async: true,
success: function (responseText) {
console.log(responseText)
}
})
注:
- ajax只负责发送http请求,发送完了就立马执行下一句
- 异步的好处:JS引擎不用等待较长的时间去获取异步的结果,可以继续后续的操作;防止页面较长时间的无响应
- 同步可以直接拿到结果,而异步想拿到结果需要借助一些技巧,如:作为参数的回调函数;借助一些事件如:onload、onclick等;Ajax中的success函数
异步的形式( 如何拿到异步代码的结果? )
1.煞笔的方法: 轮询
2.正规方法:回调
那么什么是轮询呢?打一个比方: 如果我正在敲代码,让一个人去买苹果,告诉他如果买到了就放到前面的桌子上,此时我只要妹每五分钟去看一下桌子就可以在5分钟之内拿到苹果
那么用代码该如何实现这个场景呢?
// 在 0~10秒的随机时间里,往window上添加一个apple的属性表示买到了水果
function buyFruits(){
setTimeout(()=>{window.apple = '买到的苹果'},Math.random() * 10 * 1000)
}
window.apple // undefined
buyFruits()
window.apple // undefined
window.apple // undefined
window.apple
... ...
window.apple // "买到的苹果"
改进: 我可以不用每次手动的去看桌子上有没有水果(也即window.apple),我可以用setInterval
function buyFruits(){
setTimeout(()=>{window.apple = '买到的苹果'},Math.random() * 10 * 1000)
}
buyFruits()
let id = setInterval(()=>{
if(window.apple) {
console.log(window.apple)
window.clearInterval(id)
} else {
console.log('桌子上没有苹果')
}
},1000)
再来看一下比较聪明的方法 ====> 回调: 给我一个函数,时间到了(或者拿到异步的结果)就会通知我
// 在定义回调函数时,从回调函数的参数中就可以拿到调用回调函数时传的异步的结果
function buyFruits(fn){
setTimeout(()=>{
fn.call(undefined,window.apple='买到了苹果')
},(Math.random()*10+5)*1000)
}
buyFruits(function(){
console.log(arguments[0])
})
回调的形式
1.Node.js 的 error-first 形式
// 回调函数传的第一个参数中为error
fs.readFile('./1.txt', (error, content)=>{
if(error){
// 失败
}else{
// 成功
}
})
A: 为什么需要传一个error?
B: 如果上面例子中有一半的几率没有买到苹果,那么该如何通知我呢?
// 如果没买到苹果就调用函数的时候给参数传一个error来通知我,表示没有成功买到苹果
function buyFruits(fn){
setTimeout(()=>{
if(Math.random() > 0.5){
fn.call(undefined,window.apple='买到了苹果')
} else {
fn.call(undefined,new Error())
}
},(Math.random()*10+5)*1000)
}
buyFruits(function(r){
if(r instanceof Error){
console.log('没有买到苹果')
} else {
console.log('买到了苹果')
}
})
A: 也就是回调得告诉我们是获取异步结果是成功了还是失败了
B: 怎么告诉呢?以何种方式告诉呢?
A: 在Promise出现之前,这种通知成功还是失败的方式是随便的!想怎么通知就怎么通知,于是程序员就发明了各种奇奇怪怪的通知的方法!如Nodejs中,在读取文件时有可能成功有可能失败,如果失败了,就给回调函数的第一个参数传error,如果成功了就不传error,将这个位置空着
B.在Nodejs的error-first 形式中如何知道成功还是失败了?
A: 如果回调中的第一个参数是一个存在的东西,不管它是什么,那么就表示失败了,反之如果第一个参数不存在,就表示成功了
2.jQuery 的 success / error 形式
jQuery不这么想,觉得这种通知是否成功获取到异步结果的方式是很烂的,它觉得Nodejs中为什么只用一个回调的2个不同的参数来区分获取异步结果是成功还是失败呢?为什么不是用2个回调函数!
一个回调叫success,也就是说如果成功了就调这个函数
另外一个回调叫error,也就是说如果失败了就调这个函数
$.ajax({
url:'/xxx',
success:()=>{},
error: ()=>{}
})
3.jQuery 的 done / fail / always 形式
// 追加传3个参数: done、fail、always(柯里化中说过,对象中如果传多个参数可以不一次性的都传,可以分多次的传)
$.ajax({
url:'/xxx',
}).done( ()=>{} ).fail( ()=>{} ).always( ()=> {})
```
- 成功了就调done的回调
- 失败了就调fail的回调
- 不管成功还是失败最后都调always回调
通过以上可以发现,回调没有统一的规范,想怎么样都可以,很随便,于是前端就想了一个用来统一回调形式的规范 --- Promise规范
## 4. Prosmise 的 then 形式
// ajax虽不能直接拿到异步的结果,但是then可以将它的回调放到ajax这个空对象中,让它调这些回调函数
$.ajax({ url:'/xxx', }).then( ()=>{}, ()=>{} ).then( ()=>{}) ```
所有的异步操作最终会被强制要求返回一个带有then属性的对象(ajax这个异步发送请求完成之后会返回一个对象),同时then必须接受2个函数,第一个叫成功回调,第二个叫失败回调
ajax请求成功了就调then的第一个参数成功回调,失败了就调then的第二个参数失败回调
Promise是对函数回调形式的规范,即回调形式确定了
i.必要要有then
ii.then的第一个参数必须是成功函数,then的第二个参数必须是失败函数
iii.成功的结果会被放到then的第一个参数成功函数的第一个参数中,失败的结果会被放到then的第二个参数失败函数的第一个参数中
iv.前一个then传入了2个参数后必须再返回一个then对象(带有then属性的对象),后面的then就可以继续的传2个参数,分别是成功函数和失败函数
栗子:
// 注意ajax没有遵循Promise规范,axios遵循了Promise规范
$ajax({
url: '.',
async: true
})
.then((x)=>{return x},(y)=>{console.log('失败')})
.then((z)=>{console.log(z)}) // z的结果为x
第一个then的成功回调的结果会传递给第二个then的回调函数
A:为什么需要这样
B:因为有时候拿到异步的结果并不是需要直接返回,而是需要对其进行进一步的处理,因此传给第二个then,就可以拿到前一个then的结果对其进行其他的操作,第二个then的成功回调得结果,会传递给第三个then的成功回调,它可以对第二个then的成功回调得结果再进行'加工处理'如此以往...类似于链式的操作
总结 :
轮询可以获取到异步的结果,回调也可以获取到异步的结果,现在基本都用回调得形式去获取异步操作的结果。那么回调怎么告诉我们异步的结果是成功了还是失败了?对Nodejs来说如果回调的第一个参数有值就表示失败了;对于jQuery来说如果调用了第一个回调就表示成功了,如果调第二个回调就表示失败了;Promise出来之后,异步的操作都返回一个带有then属性的对象,then属性是一个函数,then函数有2个参数,第一个参数作为成功回调,第二个参数作为失败回调,then完了之后还是会返回一个带有then属性的对象可以继续的then,后面的then的2个参数(回调函数)与前一个then的2个参数(回调函数)存在一种什么样的关系,后面接着说;Promise出现的好处,就是让我们不用再纠结获取异步结果时回调得形式了,我们只需要记住then,并且then存在链式调用
如何处理异常?
1.如何使用多个 success 函数?
栗子:
axios({
url: '.',
async: true
}).then(s1,e1) // 第一个then,姑且叫它第一责任人
.then(s2,e2) // 第二个then,姑且叫它第二责任人
.then(s3,e3) // 第一个then,姑且叫它第三责任人
axios({
url: '.',
async: true
}).then((success1)=>{},(error1)=>{}) // 第一个then,姑且叫它第一责任人
.then((success2)=>{},(error2)=>{}) // 第二个then,姑且叫它第二责任人
.then((success3)=>{},(error3)=>{}) // 第一个then,姑且叫它第三责任人
Promise和回调之间的关系被写在Promise A+规范中
i.理想情况下,如果第一责任人没有出差错的处理了axios返回的结果(带有then属性的对象)
axios请求资源成功
--> s1 return x
--> s2 return y (y可以是x,也可以是对x的其他处理,s2的参数中可以接收到s1 return的结果x,或者y干脆返回和x毫不相干的东西也行)
--> s3 return z(同样的道理,z可以是y,也可以是对y的其他处理,s3的参数中可以接收s2 return的结果y,或者z干脆返回和y毫不相干的东西也行)
ii.如果第一责任人在处理axios返回的结果时出了差错
如:语法出错了 类似把return写成retun或者使用了未声明的变量等错误
这样第一责任人就没有把axios返回的值处理好,那么第二责任就会把这件事当作失败来处理,因为第一责任人没有把事情处理好,并且在第二责任中的失败回调中可以输出第一责任人处理失败的原因
JS Bin 示例链接
axios({
url:'.',
async: true
}).then((x)=>{
console.log('成功')
console.log(xxxx) // xxx变量不存在
},(y)=>{
console.log('失败')
console.log(y)
}).then((x)=>{
console.log('成功2')
},(y)=>{
console.log('失败2')
console.log(y)
})
// 结果:成功 失败2 ReferenceError: xxxx is not defined
注意:
s1与e1回调本身如果没有语法错误或者其他错误,不报错 -----> s2
s1或e1回调本身如果存在语法错误或者其他错误,报错了 -----> e2
同理 s1与e1回调本身如果没有语法错误或者其他错误,不报错 -----> s2
s1或e1回调本身如果存在语法错误或者其他错误,报错了 -----> e2
除了报错以外,还可以主动报错,比如第一责任人说我不想处理返回给我的结果了,于是就将这个结果丢给第二责任人去处理
JS Bin 示例链接
// Promise.reject() 方法是一个主动承认失败的重要的API
axios({
url:'.',
async: true
}).then((x)=>{
console.log('成功')
return Promise.reject('reject111')
return 'haha'
},(y)=>{
console.log('失败')
console.log(y)
}).then((x)=>{
console.log('成功2')
console.log(x+'hehe')
},(y)=>{
console.log('失败2')
console.log(y)
})
// 结果: 失败2 reject111
2.在有多个成功回调的情况下,如何处理异常?
如果出现异常,异常就会抛给下一个责任人的err回调函数
i. 如果第一责任人不处理(return Promise.reject()),而第二责任人也不处理(then中没有err回调函数),那么异常就会抛给开发者自己
JS Bin 示例链接
axios({
url:'.',
async: true
}).then((x)=>{
console.log('成功')
return Promise.reject('reject111')
},(y)=>{
console.log('失败')
console.log(y)
}).then((x)=>{
console.log('成功2')
console.log(x+'hehe')
})
// 结果: Uncaught (in promise) reject111
ii. 我们可以使用.catch((err)=>{})
,catch其实是一个语法糖,本质是相当于then的时候没传成功回调只传了失败回调.then(undefined,(err)=>{})
JS Bin 示例链接1
JS Bin 示例链接2
// catch
axios({
url:'.',
async: true
}).then((x)=>{
console.log('成功')
return Promise.reject('reject111')
return 'haha'
},(y)=>{
console.log('失败')
console.log(y)
}).then((x)=>{
console.log('成功2')
console.log(x+'hehe')
}).catch((z)=>{
console.log('catch')
console.log(z)
})
// 等价的catch
axios({
url:'.',
async: true
}).then((x)=>{
console.log('成功')
return Promise.reject('reject111')
return 'haha'
},(y)=>{
console.log('失败')
console.log(y)
}).then((x)=>{
console.log('成功2')
console.log(x+'hehe')
}).then(undefined,(z)=>{
console.log('等价的catch')
console.log(z)
})
总结:
不管是catch还是一个只包含失败回调函数的then,它的作用就是作一个兜底的作用,去捕获所有的异常。如果想去处理这些异常,只要继续的then在error回调函数中去处理这个异常就可以了
自己返回 Promise
栗子:
function buyFruit(){
var fn = (x,y) => {
setTimeout(() => {
x('apple')
},10000)
}
return new Promise(fn)
}
- Promise会强行的传2个回调作为参数,成功了(10秒后买到了苹果就)就调x,失败了调y
- 把我们需要做的异步的事情传给Promise作为初始化函数
- Promise 会调用函数fn并传2个函数,一个是成功回调函数,另一个是失败函数回调,类似:
fn.call(undefined,success,error)
,success会传给x,error会传给y - Promise对象的特点就是返回一个带有then属性的对象
如果将函数简化为匿名函数就变成了:
function buyFruit(){
return new Promise((x,y) => {
setTimeout(() => {
x('apple')
},10000)
})
}
var promise = buyFruit()
promise.then(()=>{console.log(1)},()=>{console.log(2)})
结果: 1
JS Bin 示例链接 - 再次强调参数x,y为Promise传过来的2个回调函数,成功了就调用x函数,失败了就调用y函数
- buyFruit的结果为返回一个Promise对象
- 我们不需要知道什么时候成功了还是失败了,我们只需要告知成功了继续做什么即可,结果就输出 1
- 需要记住的形式/关键字是: | return | new | Promise | 参数x | 参数y
- 一个不成文的约定:参数x叫resolve,y叫reject,即返回的结果成功了就掉resolve,失败了就调reject
把这个函数的形式背下来:
function ajax(){
return new Promise((resolve, reject)=>{
做事
如果成功就调用 resolve
如果失败就调用 reject
})
}
var promise = ajax()
promise.then(successFn, errorFn)
Promise 深入阅读
Promise/A+ 规范
async / await
1. await
await后面接返回Promise的函数
var promise = buyFruit()
promise.then(()=>{console.log(1)},()=>{console.log(2)})
A: 上面的代码中其实我们还是在用回调,只不过将回调规范到then的里面而已,有没有办法不用回调
B: JS中声明了一个新的关键字: await
var result = await buyFruit()
- 加了await后"="就不是立即等于了而是一个异步的等于,而是等buyFruit()的结果,10秒后拿到异步结果
- 代码会被await一分为二,前面是同步的后面是异步的,可以认为是模拟同步的代码
// 2是会立即执行还是等一段时间再执行?
var result = await buyFruit()
console.log(2) // 结果是等10秒后result拿到值后再执行2
- 这样我们就可以以同步的形式去写异步的代码
2. async
如果我们定义了一个函数,这个函数里用到了await,那么我们就需要在声明函数的时候用关键字 async
栗子:
function buyFruit(){
return new Promise((resolve,reject) => {
setTimeout(() => {
resolve('apple')
},10000)
})
}
function fn(){
var result = await buyFruit() ???
return result
}
var s = fn()
console.log(2) // 这里的2是马上执行呢?还是等10秒后再执行?
// 结果:Uncaught SyntaxError: await is only valid in async function
A: 为什么会报错呢?
B: 因为程序走到 var s = fn()
这一步的时候搞不清楚函数fn是同步的函数还是异步的函数
A: 程序可以走到fn里面去看啊
B:程序也图省事,它想你在声明函数的时候就告诉它这个函数是同步的还是异步的,如果是异步的函数请在声明的时候加上一个关键字async来提示程序它是异步的函数
function buyFruit(){
return new Promise((resolve,reject) => {
setTimeout(() => {
resolve('apple')
},10000)
})
}
async function fn(){
var result = await buyFruit()
return result
}
var s = fn()
console.log(2)
那么加上async是不是就可以了呢?
也不行!因为s没有等异步函数fn返回的结果,就执行输出2了
我们既然知道函数fn是异步的我们当然得去等它了,所以加上await
function buyFruit(){
return new Promise((resolve,reject) => {
setTimeout(() => {
x('apple')
},10000)
})
}
async function fn(){
var result = await buyFruit()
return result
}
var s = await fn()
console.log(2)
// 结果: 10秒后输出2
如果buyFruit失败了,还会等到结果吗?
function buyFruit(){
return new Promise((resolve,reject) => {
setTimeout(() => {
reject.call() // 没买到苹果
},10000)
})
}
var result = await buyFruit() // result的结果会是怎样的呢?
console.log(result)
// 结果: 10秒后报错 Uncaught undefined
那怎么办?
用try去尝试着获取结果,如果失败了就catch
function buyFruit(){
return new Promise((resolve,reject) => {
setTimeout(() => {
reject.call()
},10000)
})
}
try{
var result = await buyFruit()
console.log('正常')
} catch {
console.log('异常了')
}
// 10秒后输出 '异常了'
目前async/await只能在chrome的高版本中使用,IE中如果想要使用,需要使用bable转为Promise的形式