同步与异步
同步: 等待结果
异步: 不等待结果
同步的 sleep
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秒 => 打印'wake up' => 打印2
异步的 sleep
function sleep(seconds, fn){
// setTimeout是浏览器提供的API,因此3秒后浏览器去调用fn
setTimeout(fn, seconds * 1000)
}
console.log(1)
sleep(3, ()=> console.log('wake up'))
console.log(2)
运行结果:打印1 => 打印2 => 3秒后打印'wake up'(这3秒JS是闲下来的,因为输出'wake up这件事交给浏览器去做了)
区别
1.同步的sleep中:sleep的下一句要等 sleep 执行完
2.异步的sleep中: sleep的下一句不等 sleep 执行完
时序图
异步的优势
同步的sleep
上从图可以看出同步的sleep中,sleep的3秒JS引擎一直处于"忙碌的看表"状态(是否到了3秒)
异步的sleep
上从图可以看出异步的sleep中,浏览器承担了"忙碌的看表"状态(是否到了3秒)的任务,JS空闲下来了
用了异步之后,JS 的空闲时间,多了许多。
但是注意,在 JS 空闲的这段时间,实际上是浏览器中的计时器在工作(很有可能是每过一段时间检查是否时间到了)
异步面试题
前端经常遇到的异步
document.getElementsByTagNames('img')[0].width // 宽度为 0
console.log('done')
为什么宽度是 0 ?
原因是没有等异步的结果
图片放到页面是需要时间去下载的
如果网速较慢图片没有加载出来,此时去获取图片的宽度当然是0
如何解决这个问题?
var img = document.getElementsByTagNames('img')[0]
img.onload = function(){
var w = img.width
console.log(w)
}
onload是异步的回调函数
图片如果加载成功会触发onload,浏览器会自动的调用onload
我们在异步的回调里去等结果
面试题中的异步
let liList = document.querySelectorAll('li')
for(var i=0; i<liList.length; i++){
liList[i].onclick = function(){
console.log(i)
}
}
以为的结果
点击第0个li,打印0;
点击第1个li,打印1;
点击第2个li,打印2 ... ...
实际的结果
无论点击哪个li,打印的都是6
为什么
JS中的变量会提升,无论写在哪里,都会自动的"跑到"最前面
let liList = document.querySelectorAll('li')
var i;
for(i=0; i<liList.length; i++){
liList[i].onclick = function(){
console.log(i)
}
}
也就是全局下只有一个i
时序图
异步在哪里
onclick = function(){
console.log(i)
}
onclick事件是异步的,但是浏览器并没有等上面的代码执行,而是直接把循环走完就把i变成6,然后用户的点击动作才出现
如何解决
将循环中的var变成let
let liList = document.querySelectorAll('li')
for(var i=0; i<liList.length; i++){
liList[i].onclick = function(){
console.log(i)
}
}
为什么let就可以
let不会变量提升
从之前的全局一个i,到每一个li都有一个自己的i
AJAX 中的异步
同步的AJAX
// 获取当前页面的html
let request = $.ajax({ // 假如等68毫秒才有结果
url: '.',
async: false
})
console.log(request.responseText)
同步的AJAX中JS就像"瘫痪"了一样,在这68毫秒中JS什么也没做,JS只在那等请求的结果,在这段时间内页面中的任何操作都没有反馈
异步的AJAX
$.ajax({
url: '/',
async: true,
success: function(responseText){
console.log(responseText)
}
})
console.log('请求发送完毕')
看下过程:
1.JS发送了一个AJAX请求
2.紧接着就打印'请求发送完毕'(没有等AJAX请求的结果)
3.浏览器通知JS结果出来了,『你去取吧』,就打印出了AJAX的请求结果
注:在请求结果返回之前的这段时间内,页面中的任何操作都可以照常进行,这点比同步的体验要好很多
异步的形式
傻逼方法:轮询
就好比是找一个人去帮我买苹果
如果买到了就让他就放到桌子上
我就隔一段时间去看有没有苹果(不停的去看桌子上有没有苹果...)
上代码
function buyApple(){
setTimeout(() => {
window.apple = '买到了苹果'
}, Math.random()*10*1000);
}
buyApple()
var id = setInterval(()=>{
if(window.apple){
console.log(window.apple)
window.clearInterval(id)
}else{
console.log('桌子上没有苹果')
}
},1000)
桌子上没有苹果 // 8秒都没有
买到了苹果 // 第9秒才买到水果
正规方法:回调
基础的回调
让buyApple接受一个函数,买到了苹果就『通知你』
function buyApple(fn){
setTimeout(() => {
fn.call(undefined,'买到了苹果')
}, Math.random()*10*1000);
}
buyApple(function(){
console.log(arguments[0])
})
买到了苹果 // 第3秒买到了苹果
一但买到了苹果(拿到了异步的结果)
就会调用函数function(){console.log(arguments[0])}(调用回调函数,将结果通过参数的方式传给回调函数)
使用success/error的形式改造下『买苹果』
function buyApple(fn){
setTimeout(() => {
if(Math.random>0.5){
fn.call(undefined,'买到了苹果')
}else{
fn.call(undefined,new Error())
}
}, Math.random()*10*1000);
}
buyApple(function(r){
if(r instanceof Error){
console.log('没买到')
}else{
console.log('没买到')
}
})
要好好体会使用回调的过程
回调的形式
1. Node.js 的 error-first 形式(1个回调函数作为参数)
fs.readFile('./1.txt', (error, content)=>{
if(error){
// 失败
}else{
// 成功
}
})
2. jQuery 的 success / error 形式(2个回调函数作为参数)
$.ajax({
url:'/xxx',
success:()=>{},
error: ()=>{}
})
3. jQuery 的 done / fail / always 形式
$.ajax({
url:'/xxx',
}).done( ()=>{} ).fail( ()=>{} ).always( ()=> {})
备注:
- 接受多个参数,但是不是一次传,而是分多次传,具体参照『柯里化』的思想
- ajax的结果返回一个新的对象,这个对象有一个done属性可以接受函数作为参数
- 成功了调done的第一个回调
- 失败了调fail的回调
- always是不管失败还是成功都会调用的回调
以上回调的形式都没有形成统一的规范
4. Prosmise 的 then 形式
Promise只是统一了回调的形式
$.ajax({
url:'/xxx',
}).then( ()=>{}, ()=>{} ).then( ()=>{},()=>{})
备注:
- 不管什么异步操作,都必须返回一个带有属性 『then』 的对象
- 所有的then必须接受2个回调,第一个叫成功回调,第二个叫失败回调
- ajax最后的结果如果成功了就调成功回调,如果失败了就调失败回调
- then传入了参数后必须返回一个then对象
- 第一个then里面的成功回调(return了一个值),会将return的值传给后面then的成功回调(通过参数接收到)。
- 所有的then都会监听这个成功的结果,拿到它并对齐进行不同的后续操作。
- 同理,失败回调的结果也一样,第一个then的失败回调会将结果通过return的方式,传给下一个then的失败回调,以便于不同的then对失败的结果进行不同的后续操作
Promise
Promise遵循Promise A+ 规范,但是其他的回调形式,如:axous、ajax等就不一定遵循这个规范
Promise如何处理多个success函数?
Promise并不不是按照success1 -> success2 ->success3 这样依次调用
而是看第一责任人有没有结果,如果有结果,不管是success1还是error1,都会走到success2
如果没有结果就会走到error2
( tips:有结果的意思就是本身没有写错代码/报语法错误,拿到了结果,不管是成功还是失败 )
比喻: 有很多责任人来处理异孩这件事
存在的情况
- success1 和 error1 一定有一个被调用
- 第一责任人只要『没有报错』『完整的处理』了success1或 error1(指的是本身语法没有错误),就都会走到success2
- 如果是success2 或者 erro2 报错了(本身写的语法有问题),就都会走到 error2
- 第二责任人不管是调用 success2 还是 调用 error2,只要『没有报错』『完整的处理』了success2 或 error2(指的是本身语法没有错误),就会走到success3
- 如果success2或者erro2 报错了(本身写的语法有问题),就都会走到 error3
- 第三责任人...
特殊情况
如果第一责任人处理不好这个结果,但是本身又不想『被动的报错』(语法的错误),它可以选择『主动的报错』,通过返回一个错误的结果(抛出异常)
return Promise.reject('这个活我干不鸟,第二责任人你帮我处理吧')
如何处理异常
-
如果出现异常就会返回给下一个then的error回调,如果下一个then的error回调中没有处理这个异常,那么这个异常就会在控制台抛给开发者去处理
-
通过catch去处理(相当于写了一个失败回调)
axios({ url:'', async: true }).then(()=>{},()=>{}) .then(()=>{},()=>{}) .catch((err)=>{ console.log(error) })
catch只是一个语法糖: .catch === .then(undefined,(err)=>{console.log(error)})
如何使用Promise去拿异步的结果
拿买苹果来举个例子
基本格式
function buyApple(){
var fn = ()=>{
setTimeout(()=>{
'apple'
},10000)
}
return new Promise(fn) // fn.call(undefined,success,error)
}
如何把异步的结果给外面呢?
=> 使用回调
fn必须要接收2个函数,第一个函数叫x,第二个函数叫y,成功就调x
function buyApple(){
// 2个参数都是函数
var fn = (x,y)=>{
setTimeout(()=>{
resolve('apple')
},10000)
}
return new Promise(fn) // fn.call(undefined,success,error)
}
改进:使用匿名函数
function buyApple(){
//2个参数都是函数
return new Promise((x,y)=>{
setTimeout(()=>{
resolve('apple')
},10000)
})
}
如何使用
buyApple返回的结果是一个Promise
function buyApple(){
//2个参数都是函数
return new Promise((x,y)=>{
setTimeout(()=>{
resolve('apple')
},10000)
})
}
var promise = buyFruit()
但是我们不知道什么时候成功还是失败
不需要!我们只需要知道成功后做什么,用then输入到这个Promise对象里去
function buyApple(){
//2个参数都是函数
return new Promise((x,y)=>{
setTimeout(()=>{
x('apple')
},10000)
})
}
var promise = buyApple()
promise.then(()=>{console.log('成功')},()=>{console.log('失败')})
约定俗成的名字
参数x叫resolve,参数y叫reject
function buyApple(){
//2个参数都是函数
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('apple')
},10000)
})
}
var promise = buyApple()
promise.then(()=>{console.log('成功')},()=>{console.log('失败')})
记住固定套路
如果想要在一个函数中处理异步的事情,就要遵循下面的固定格式去写
function ajax(){
// 返回的Promise对象就是then对象,这个对象的特点就是有一个then函数,拿到异步的结果后可以then
return new Promise((resolve, reject)=>{
//你要做的异步的事
如果成功就调用 resolve
如果失败就调用 reject
})
}
var promise = ajax()
promise.then(successFn, errorFn)
async&await
await
结论: await后面接返回Promise的函数
去掉回调
当我们使用下面的格式调用函数的时候,使用的还是回调的形式,只不过把它规范到了then里面,能否去掉回调
var promise = buyApple()
promise.then(()=>{console.log('成功')},()=>{console.log('失败')})
JS给了一个关键字await
var promise = await buyApple()
备注:
- await会等到Promise返回的结果才会赋值
- 也就是说此处的"="号是异步的等于号
使用关键字await
function buyApple(){
//2个参数都是函数
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('apple')
},10000)
})
}
var promise = await buyApple()
promise
async
使用场景
如果在一个函数里面写了await,那么再声明函数的时候在函数名前使用async,告诉浏览器函数里面有异步的操作
function buyApple(){
//2个参数都是函数
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('apple')
},10000)
})
}
async function fn(){
var result = await buyApple()
return result
}
var s = fn()
console.log(2)
async同await配套使用
我们发现这样不行,因为var s = fn()并没等异步的结果
既然知道fn是异步的为什么不去等它的结果呢?
function buyApple(){
//2个参数都是函数
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('apple')
},10000)
})
}
async function fn(){
var result = await buyApple()
return result
}
var s = await fn()
console.log(2)
如果await拿到的结果是失败会怎样
function buyApple(){
//2个参数都是函数
return new Promise((resolve,reject)=>{
setTimeout(()=>{
reject('apple') // 失败的结果
},10000)
})
}
var promise = await buyApple()
我们会发现报错了
try...catch
如果报错了怎么办? 使用 try...catch 来捕获和处理异常
function buyApple(){
//2个参数都是函数
return new Promise((resolve,reject)=>{
setTimeout(()=>{
reject('apple') // 失败的结果
},10000)
})
}
try{
var promise = await buyApple()
}catch(ex){
console.log('异常了',ex)
}
如果不处理,那么控制台就直接报错了