烧脑的异步

275 阅读9分钟

同步与异步

同步: 等待结果

异步: 不等待结果

同步的 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 执行完

时序图

image.png

异步的优势

同步的sleep

image.png

上从图可以看出同步的sleep中,sleep的3秒JS引擎一直处于"忙碌的看表"状态(是否到了3秒)

异步的sleep

image.png

上从图可以看出异步的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 ... ...

实际的结果

image.png

无论点击哪个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

时序图

image.png

异步在哪里

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)
    }
}

image.png

为什么let就可以

image.png

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:有结果的意思就是本身没有写错代码/报语法错误,拿到了结果,不管是成功还是失败 )

比喻: 有很多责任人来处理异孩这件事

image.png

存在的情况

  1. success1 和 error1 一定有一个被调用
  2. 第一责任人只要『没有报错』『完整的处理』了success1或 error1(指的是本身语法没有错误),就都会走到success2
  3. 如果是success2 或者 erro2 报错了(本身写的语法有问题),就都会走到 error2
  4. 第二责任人不管是调用 success2 还是 调用 error2,只要『没有报错』『完整的处理』了success2 或 error2(指的是本身语法没有错误),就会走到success3
  5. 如果success2或者erro2 报错了(本身写的语法有问题),就都会走到 error3
  6. 第三责任人...

特殊情况

如果第一责任人处理不好这个结果,但是本身又不想『被动的报错』(语法的错误),它可以选择『主动的报错』,通过返回一个错误的结果(抛出异常)

return Promise.reject('这个活我干不鸟,第二责任人你帮我处理吧')

如何处理异常

  1. 如果出现异常就会返回给下一个then的error回调,如果下一个then的error回调中没有处理这个异常,那么这个异常就会在控制台抛给开发者去处理

  2. 通过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('失败')})

image.png

约定俗成的名字

参数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

image.png

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配套使用

image.png

我们发现这样不行,因为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()

image.png

我们会发现报错了

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)
}

image.png

如果不处理,那么控制台就直接报错了