异步

2020-08-06  本文已影响0人  泡杯感冒灵

问题

// 循环运行期间,JS执行和DOM渲染暂时卡顿
var i, sum = 0;
for (i = 0;i < 100000000; i++){
  sum += i
}
console.log(sum)

// alert不处理,JS执行和DOM渲染暂时卡顿
console.log(1)
alert('hello')
console.log(2)

原因:JS可以修改DOM结构,浏览器要渲染DOM,单线程就是为了避免DOM渲染的冲突

浏览器需要渲染DOM
JS可以修改DOM
JS执行的时候,浏览器DOM渲染会暂停
两段JS不能同时执行(都修改DOM,就冲突了)
webworker支持多线程,但不能访问DOM

解决方案:异步
异步是一个很无奈的解决方案,虽然有很多问题

console.log(100)
setTimeout(function () {
  console.log(200)
},1000)
console.log(300)
console.log(400)
// 先打印100,然后打印300,400,1s后才打印200

console.log(100)
$.ajax({
  url: 'xxxxx',
  success: function (result) {
    console.log(result)
  }
})
console.log(300)
console.log(400)
// 先打印100,然后打印300,400,ajax请求成功后才打印result

异步存在的问题

问题一:没有按照书写的顺序执行,可读性差
问题二:callback中,不容易模块化
同步代码,直接执行
异步函数先放在异步队列中
待同步函数执行完毕,轮询执行异步队列的函数

//实例解析
// setTimeout明显异步,这个时候就把function () {console.log(100)}先放入异步队列,先不管它
// console.log(200)是同步函数,会放入主进程中,立刻执行。
// 主进程中的代码执行完后,回头去看异步队列有没有,发现里边是有要执行的函数的,就把它取出来放入主进程,立即执行,执行完后再去异步队列里找。
// 如此反复循环,直到主进程和异步队列都为空
setTimeout(function () {
  console.log(100)
},0)
console.log(200)



// 执行第一个setTimeout会告诉计算机,100毫秒后,再把function () {console.log(1)}放入异步队列
// 然后执行第二个setTimeout,会立刻把function () {console.log(2)}放入异步队列中
//  console.log(3) 会直接放入主进程,立即执行,执行完后,会进入异步队列,发现有要执行的函数
// 然后把function () {console.log(2)} 放入主进程中立即执行,这个时候function () {console.log(1)}还没有放入异步队列
// 此时异步队列为空,JS引擎会继续轮询主进程和异步队列
// 100毫秒对于计算机来说,算是一个比较长的时间,等100毫秒后function () {console.log(1)}放入了异步队列
// JS引擎,就会把它放入主进程中立即执行。

setTimeout(function () {
  console.log(1)
}, 100)
setTimeout(function () {
  console.log(2)
})
console.log(3)
//  执行结果  3,2,1
// ajax 请求成功后,才会把function(result){console.log(result)} 放入异步队列中

$.ajax({
  url: 'xxxxx',
  success: function (result) {
    console.log(result)
  }
})

// 100毫秒后,function () {console.log(1)} 会被放入异步队列中
setTimeout(function () {
  console.log(1)
}, 100)


// 立即放入异步队列中
setTimeout(function () {
  console.log(2)
})

// 放入主进程,立即执行
console.log(3)

// 执行结果可能有2种
ajax请求超过100毫秒,那么执行结果就是 3,2,1,result
ajax请求不到100毫秒,那么执行结果就是 3,2,result,1
    // jQuery1.5之前
    var ajax = $.ajax({
      url:'./data.json',
      success:function(){
        console.log('success 1')
        console.log('success 2')
        console.log('success 3')
      },
      error:function(){
        console.log('error')
      }
    })
    console.log(ajax)  // 返回一个XHR对象

    // jQuery1.5之后
    var ajax = $.ajax('./data.json')
    ajax.done(function(){
      console.log('success 1')
    }).fail(function(){
      console.log('fail1')
    }).done(function(){
      console.log('success 2')
    }).fail(function(){
      console.log('fail2')
    }).done(function(){
      console.log('success 3')
    }).fail(function(){
      console.log('fail3')
    })
    console.log(ajax)  // 返回一个deferred对象

// 这种写法的改变,无法改变JS异步和单线程的本质
// 只能从写方法上杜绝callback的形式
// 它是一种语法糖形式,但是解耦了代码,之前是callback中一下子全部写完。现在是分配到好多个函数中,而且这些函数是按顺序执行的
// 很好的体现了开发封闭原装 (对扩展开发,对修改封闭)
// 1.5之前的写法,如果我们想成功时再打印 success 4,那我们只能修改success方法。因为没有地方可扩展。
// 而1.5之后的写法,如果我们想成功时再打印 success 4,我们再加一个.done(console.log(success 4))就可以了
// 这种请求就是对扩展开发,对修改封闭,我们不需要去修改原先的函数
// 这种方法,在实际开发中,多人协作很有帮助,各人负责一个函数,互不干扰;需要新功能了,就扩展
// 而1.5之前那种方式,多人维护一坨代码。很容易搞混
// 而且测试的时候,1.5之前的那种,改了代码之后,之前的所有的功能都要测试一一遍
// 而1.5之后的这种写法,不需要,因为之前的代码是没有变的 ,只是扩展了一个方法而已,只需要测试扩展的功能就可以了


  // 很像promise的写法
  // 这个时候then接收两个参数,一个是成功时执行的函数,一个是失败时执行的函数
  // promise是2015年ES6出来的时候才有的,而jQuery这种.then的写法很早就有了。
  // 所以这种异步解决方案并不是promise出来才有的,只不过是ES6的时候把它变成了一种标准
    var ajax = $.ajax('./data.json')
    ajax.then(function(){
      console.log('success 1')
    },function(){
      console.log('error 1')
    })
    ajax.then(function(){
      console.log('success 2')
    },function(){
      console.log('error 2')
    })


使用jQuery deferred

    // 给出一段非常简单的异步操作代码,使用setTimeout函数
    var wait = function (){
      var task = function(){
        console.log('执行完成')
      }
      setTimeout(task,2000)
    }
    wait()

    // 新增需求:要求执行完成后进行某些特别复杂的操作,代码可能会很多,而且分好几个步骤
    function waithandle(){
      var dtd = $.Deferred()  // 创建一个deferred对象

      var wait = function(dtd){
        var task = function(){
          console.log('执行完成')
          dtd.resolve()  //表示异步任务已经完成
          // dtd.reject()  //表示异步任务失败或出错
        }
        setTimeout(task,2000)
        return dtd  //要求返回deferred 对象
      }

      //注意这里一定要有返回值
      return wait(dtd)   //这里表示 waithandle 执行结果也是返回dtd,只不过式放入wait函数加工后的dtd
    } 

    var w = waithandle()
    var w = waithandle()
    w.then(function(){
      console.log('ok 1')
    },function(){
      console.log('err 1')
    })
    w.then(function(){
      console.log('ok 2')
    },function(){
      console.log('err 2')
    })

    // 还有 w.done() w.fail()
总结
dtd的api,可分成两类,用意不同
第一类 dtd.resolve(要成功)  dtd.reject(要失败)  他们俩是一个需要主动去执行的角色
第二类 dtd.then dtd.done dtd.fail ;  dtd.then是一个监听的角色,如果成功了就执行then的第一个参数,如果失败了就执行then的第二个参数
这两类应该分开,否则后果很严重,比如我们在w.then前,先执行w.reject()就会导致输出顺序的错乱

初步引入promise概念

// deferred.promise
   function waithandle(){
      var dtd = $.Deferred()  // 创建一个deferred对象

      var wait = function(dtd){
        var task = function(){
          console.log('执行完成')
          dtd.resolve()  //表示异步任务已经完成
          // dtd.reject()  //表示异步任务失败或出错
        }
        setTimeout(task,2000)
        return dtd.promise()  //注意这里不再返回deferred 对象,而是返回promise对象
      }

      //注意这里一定要有返回值
      return wait(dtd)
    } 

    var w = waithandle()   // 这里是promise对象
    w.reject()  // 这个时候,如果再调用reject就会报错
    $.when(w).then(function(){
      console.log('ok 1')
    },function(){
      console.log('err 1')
    })

// promise对象,可以调用.then这种监听式的方法,但是不能调用resolve,reject这种主动触发式的方法
// 是成功还是失败,是内部封装说了算,但是监听是使用者唯一的权力,不能去干预promise是成功还是失败
// 这个是最初提的promise概念,也就是在这个概念的基础上,慢慢发展出了ES6标准的promise

promise和deferred的区别

deferred对象有resolve,reject这类可以主动触发的函数,还有then,done,fail这种被动监听式的函数。这些函数都混在一起式不行的,容易被篡改
这个时候我们可以通过生成一个promise对象来进行隔离,promise对象只能被动监听,不能主动修改。这个promise算是ES6标准的promise的前世吧 
    function loadImg(src){
      const promise = new Promise(function(resolve,reject){
        var img = new Image()
        img.onload = function(){
          resolve(img)
        }
        img.onerror = function(){
          reject()
        }
        img.src = src
      })
      return promise
    }

    var src = 'https://www.imooc.com/static/img/index/logo-recommended.png'
    var result = loadImg(src)
    result.then(function(img){
      console.log(img.width)
      return img
    },function(){
      console.log('failed')
    }).then(function(img){
      console.log(img.height)
    })

异常捕获

// 正常情况下,then接收两个参数,第一个是成功执行的回调函数,第二个参数是失败后执行的回调函数。
// 但是如果我们要异常捕获的话,就不能传两个参数了,每个then只接收一个成功的回调函数
// 失败的回调函数,统一用catch接收,也就是统一用catch捕获异常
    result.then(function(img){
      console.log(img.width)
      return img
    }).then(function(img){
      console.log(img.height)
    }).catch(function(ex){
      // 统一捕获异常,ex为异常对象
      console.log(ex)
    })

我们加一个自定义错误,看下是否能捕获

    function loadImg(src){
      const promise = new Promise(function(resolve,reject){
        var img = new Image()
        throw new Error('自定义错误')
        img.onload = function(){
          resolve(img)
        }
        img.onerror = function(){
          reject()
        }
        img.src = src
      })
      return promise
    }

    var src = 'https://www.imooc.com/static/img/index/logo-recommended.png'
    var result = loadImg(src)
    result.then(function(img){
      console.log(img.width)
      return img
    }).then(function(img){
      console.log(img.height)
    }).catch(function(ex){
      console.log(ex)
    })

捕获到了 自定义错误


image.png

这个捕获异常的方式,跟我们之前用的 try catch的捕获异常的方式是一样的原理, 只不过try被promie里封装起来了

try {
  xxx
}catch(ex){
  xxx
}

刚才我们通过throw new Errow的方式,模拟了逻辑之外的,语法的错误。然后通过catch捕获到了。但是我们希望的是catch不仅仅捕获逻辑之外的语法的错误。还可以捕获逻辑之内的错误。比如我们的图片如果加载失败了,就会走onerror的回调,然后reject会传递错误信息。如果想要catch捕获,我们给reject传递错误信息

// 我们可以给src赋值一个不存在的图片地址,然后就会触发onerror
 img.onerror = function(){
      reject('图片加载失败')
 }
image.png

多个串联
工作中,我们经常会遇到这样一种需求场景,比如我们要先通过接口获取到用户的ID,手机号等信息,然后再根据用户ID获取到用户的列表等数据。那么有先后顺序的这种需求,就需要用到promise的串联来解决。
下边我们用加载两张图片来模拟这个过程。

var src1 = 'https://www.imooc.com/static/img/index/logo-recommended.png'
var result1 = loadImg(src1)

var src2 = 'https://www.imooc.com/static/img/course/logo-course.png'
var result2 = loadImg(src2)

// 链式操作
result1.then(function(img){
    console.log('第一个图片加载完成')
    return result2  // 注意这里,比如要return result2,下边才是result2的.then
}).then(function(img){
    console.log('第二个图片加载完成')
}).catch(function(ex){
    // 统一捕获异常
    console.log(ex)
})

promise.all() promise.race()

var src1 = 'https://www.imooc.com/static/img/index/logo-recommended.png'
var result1 = loadImg(src1)

var src2 = 'https://www.imooc.com/static/img/course/logo-course.png'
var result2 = loadImg(src2)

// 注意 此时的Promise开头大写,是window对象的属性
// Promise.all 接受一个promise对象的数组
// 待全部完成之后,统一执行 success
Promise.all([result1,result2]).then(datas => {
    // 接受到的datas是一个数组,包含多个promise返回的内容
    console.log(datas[0])  // 这里打印的都是img标签,因为图片加载成功的时候,我们resolve的是img标签
    console.log(datas[1])
})

// Promise.race 接受一个包含多个promise对象的数组
// 只要有一个完成就执行success
Promise.race([result1, result2]).then(data => {
    // 接受到的data是最先执行完成的promise的返回值
    console.log(data)
})

// 打印结果,肯定是先打印Promise.race 里的data,因为它是有一个promise完成就会执行success.
// 而Promise.all因为要等所有的promise都执行完,所以花费的时间,要比Promise.race长

promise标准

任何技术推广使用,都需要一套标准来支撑
如html,js,css,http,无规矩不成方圆
任何不符合标准的东西,终将会被用户抛弃
不要挑战标准,不要自造标准

promise标准 状态变化

三种状态 pending,fulfilled(成功),rejected(失败)

初始状态是 pending(比如我们之前加载图片的函数,一开始传入src还没有开始加载,就是pendging)

pending变为 fulfilled(图片加载成功 执行了resolve后,状态改为fulfilled),或fulfilled变为 rejected(图片加载失败,执行了reject后,状态改为rejected)

状态是不可逆的,只能是初始变为成功,或初始变为失败

promise标准 then

Promise实例,必须实现then这个方法
then()必须接受两个函数作为参数,第一个函数是成功之后的回调,第二个参数是失败后的回调
then()返回的必须是一个Promise实例
如果then里边的函数,没有明文返回一个Promise实例,那么这个then返回的就是本身的Promise实例,所以可以继续链式调用.then
 var w = waithandle()
    w.then(function(){
      console.log('ok 1')
    },function(){
      console.log('err 1')
    })
    w.then(function(){
      console.log('ok 2')
    },function(){
      console.log('err 2')
    })

async/await是最直接的同步写法

// 语法,
// 一个函数如果想要在函数体里用 await, 必须要再函数前加async标识
// await 后必须跟一个 Promise实例
// 需要用 babel-polyfill
    const load = async function(){
      const result1 = await loadImg(src1)
      console.log(result1)
      const result2 = await loadImg(src2)
      console.log(result2)
    }
    load()

async/await总结

// 首先是函数要加async标识,才可以在函数内部使用await,函数可以当作普通函数调用
// 使用了promise,并没有和promise冲突
// promise算是对异步调用的封装,如果单纯使用promise 只能通过.then这种方式来使用,而async/await算是对promise的扩展,
// async/await以同步的方式使用promise,再也没有回调函数
// 但是改变不了JS单线程,异步的本质
jQuery deferred
ES6 Promise
Async/Await
上一篇下一篇

猜你喜欢

热点阅读