javaScript异步编程

同步和异步

相信大家对同步异步的概念不陌生了。javaScript是单线程的,在同一时间只能处理一个任务,所有的任务都需要排队。

同步任务:指在主线程上排队执行的任务,是按顺序一步一步的执行。只有上个任务执行完成后,下一个才能开始执行,如果上一个任务一直没有执行完成(或者出错)则下一个就一直处于等待状态(阻塞)。

异步任务:指不进入主线程,而进入任务队列的任务。只有等主线程任务执行完毕,任务队列通知主线程,请求执行任务,该任务才会进入主线程执行。

同步和异步的差别就在于是否能改变javaScript这个单线程上各个任务的执行顺序。异步操作是可以改变任务正常的执行顺序。

目前主流的几种异步过程控制方法

Callback

首先callback和异步没有必然的联系,callback本质就是类型为function的函数参数,callback是同步还是异步执行取决于函数本身。

虽然callback常用于异步方法的回调,但其实有不少同步方法也可以传入callback,比如最常见的数组的 forEach方法:

const arr = [1, 2, 3];
arr.forEach(function (element) {
    console.log(element);
});
console.log('finish');
// 打印结果:1, 2, 3, finish

1.1 异步callback:

常见的异步callback 如:setTimeout 、setInterval

setTimeout(function (){
console.log('哈哈');
}, 200);
console.log('finish');
// 打印结果:finish  哈哈


setInterval(function (){
console.log('循环哈哈');
}, 200);
console.log('finish');
// 打印结果:finish  循环哈哈(每隔2毫秒打印一次)

1.2 Callback Hell (回调地狱):

在实际项目中我们经常会遇到这样的问题:

一个异步函数的操作结果是下一个异步函数的入参,下一个异步函数的请求结果又是下下个异步函数的入参......这样递进的层级多了就会形成很多层的callback嵌套,导致代码的可读性和可维护性变得很差,形成所谓的Calback Hell。

类似这样:

step1(param, function (result1) {
    step2(result1, function (result2) {
        step3(result2, function(result3) {
            step4(result3, function(result4) {
                console.log(result4);
            });
        });
    });
});

当然在不放弃使用callback的前提下,上面的代码还是有优化空间的,把函数拆出来:

step1( param, callback1 );

function callback1(result1) {
    step2(result1, callback2);
}
function callback2(result2) {
    step3(result2, callback3);
}
function callback3(result3) {
    step4(result3, callback4);
}
function callback4(result4) {
    console.log(result4);
}

这样写更接近我们平时习惯的从上到下的同步调用,但是代码的复杂度没有变化,只是变的更清晰了,缺点就是需要定义额外的函数,变量。

Promise

随着CommonJS规范出现,其中提出了Promise规范,Promise完全改变了js异步编程的写法,让异步编程变得十分的易于理解,

同时Promise也已经纳入了ES6,而且高版本的chrome、firefox浏览器都已经原生实现了Promise,只不过和现如今流行的类 Promise类库相比少些API。

2.1.基本概念:

promise翻译为“承诺”,从字面意思可以看出,它表达的是将来一定会执行的操作。

promise其实是一个构造函数,自身有 reject 、resolve、all 这几个方法,原型上有 then、catch方法。

Promise对象有一下两个特点:

  • 对象的状态不受外界影响,只有异步操作可以决定当前处于的状态,并且任何其他操作都无法改变这个状态。一共有三种状态:pending(进行中),fulfilled(已成功),rejected(已失败)。

  • 一旦状态改变,就不会再变了。状态改变的过程只可能是 pending->fulfilled 和 pending->rejected。

2.2  resolve 和 then 的用法

resolve 是对promise成功时候的回调,它把promise的状态修改为fulfiled

下面先new 一个Promise

  刷新页面控制台打印出:

没有打印“成功返回出去的数据”是因为我们只是new了一个对象,并没有调用它,

我们传进去的函数已经执行了,所以使用Promise的时候一般是放在一个函数中,在需要的时候去调用就行了。如:

  刷新页面控制台打印出:

 这个时候可以看到我们执行testPromise这个函数得到了一个Promise对象。接下来我们就可以用Promise对象上有的then、catch等方法了。请看看下面的代码:

  刷新页面控制台打印出:

首先testPromise方法被调用执行返回了一个promise对象,然后执行了promise的then方法,then方法接受一个参数-------->resolve返回的数据('成功返回出去的数据')

看到这里应该就能看出,then里面的函数就是和我们*时写的回调函数一个意思,在testPromise这个异步任务执行完成之后被执行。

总结一下:Promise的作用就是能把原来的回调写法分离出来,在异步操作执行完成之后,用链式调用的方式执行回调函数。实质上Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。

使用Promise解决Callback Hell 请看下面代码:

先分析这个过程:

  1. 首先testPromise函数会执行console.log直接打印“执行完成promise”,然后执行then方法接受resolve里面的参数“成功返回出去的数据”,这个参数作为testPromise2函数的如入参。
  2. testPromise2函数会执行console.log打印出“成功返回出去的数据”,然后执行then方法,接受resolve里面的参数“成功返回出去的数据_2”,这个参数作为testPromise3函数的如入参。
  3. testPromise3函数会执行console.log打印出“成功返回出去的数据 _2”,然后执行then方法,接受resolve里面的参数“成功返回出去的数据 _3 ”,最后打印出“成功返回出去的数据__3 结束“。

所以打印的结果是:

执行完成promise / 成功返回出去的数据 / 成功返回出去的数据 _2 / 成功返回出去的数据_3 结束

看看控制台:

2.3.reject 的用法

reject 是对promise失败时候的回调,它把promise的状态修改为rejected,我们可以在then中就可以捕捉到,然后”执行“失败情况的回调。

话不多说上代码:

 看下控制台打印:

总结:then()中可以接收两个参数,第一个是成功的回调函数对应resolve的回调,第二个是失败的回调函数对应reject的回调,并且能在回调函数中拿到成功的数据和失败的原因。

2.4.catch的用法

与promise对象方法then()并行的一个方法是catch,与try catch类似,catch就是用来捕获异常的,也就是和then()方法中接受的第二个参数reject的回调是一样的。

 看看控制台打印:

可以看出效果和在then里面添加第二个参数一样。它还有另外一个作用就是:在执行resolve的回调(then中的第一个参数)时,如果抛出异常了(代码出错),那么并不会报错卡死,而是会进到这个catch()方法中。

如下:

 看看控制台打印结果:

在resolve的回调中,当遇到 console.log(tableDataList) 时 tableDataList这个变量是没有定义的,如果我们不用 catch,代码运行到这里就直接在控制台报错了,不会往下运行了。

在这里能得到上图结果就说明进到catch方法里面去了,而且把错误的原因传到了reason参数中,这样即便是有错误的代码也不会报错。

2.5. all的用法

是与then同级的另一个方法,提供了并行执行异步操作的能力,并且在所有的异步操作执行之后,同时执行结果都是成功的时候才执行then回调。

all 接收的参数是一个数组,里面是需要执行异步操作的所有方法,每个方法里面的值最终返回promise对象。举个例子,代码如下:

 

代码中的三个异步操作是并行执行的,等到他们都执行完成后,同时执行结果都是成功的时候,才会进到then里面。

三个异步操作返回的数据都在then里面,all会把所有异步操作的结果放在一个数组中,作为入参传给then,然后then方法的成功回调将结果接收。

 三个函数的执行顺序跟你在all()中放置的顺序一致,只要生成的随机数大于5就直接进入all 的 catch()方法,其余的将不会进入all的任何回调,即:只有所有的函数都执行成功才能进入all 的成功回调。

2.6. race的用法

all是等所有的异步操作都执行完了再执行then方法,那么race方法就是相反的,谁先执行完成就先执行回调。

先执行完的不管是进行了race的成功过回调还是失败回调,其余的将不会进入race的任何回调。

废话不多说上代码:

看下控制台打印结果:

1秒随机生成数据函数最先完成,完成后就进入race的回调函数(不管是进入成功回调函数还是进入失败回调函数),剩下的异步函数将不会再进入race的任何回调。

race()的使用举个例子看看:

 

这里定义了两个Promise对象,一个是请求后台接口,一个计时10秒,把这两个放到race里面赛跑,看看谁先完成,

如果请求数据先完成就直接进入then成功回调函数中,如果10秒已经到了还没请求完成数据,就提示请求超时。

3.async/await

首先从字面意思来理解,async是”异步“的简写,而 await 可以认为是async wait 的简写。

async用于申明一个function是异步的,而await用于等待一个异步方法执行完成。await 只能出现在 async 函数中。

3.1.async起什么作用

async函数返回的是一个Promise对象 ,如果在函数中return一个直接量,async会把这个直接量通过promise.resolve()封装成Promise对象。

Promise.resolve(x)可以看作是new Promise ( resolve => resolve(x) )的简写。可以用于快速封装字面量对象或者其他对象,将其封装成 promise 实例。

 看看控制台打印的asy 是什么 ---------- 一个promise对象

testAsync().then(result => {
    console.log(result) // Hello asycn
})

如果 async 函数没有返回值,它会返回 Promise.resolve(undefined)

3.2.await 等的是什么

await 等待的是一个表达式,这个表达式的计算结果是promise对象或者其他的值(没有特殊限定)。

因为async函数返回一个promise对象,所以await 可以用于等待一个async函数的返回值,也可以等待任意表达式的结果(await后面实际是可以接普通函数调用或者直接量)。

如果await等到了一个promise对象,await就忙起来了,它会阻塞后面的代码(await 必须用在async函数中,async函数不会阻塞代码,它内部所有的阻塞都被封装在一个promise对象中异步执行),等着promise对象resolve ,然后得到resolve的值,作为await表达式的运算结果。

例如:

在vue 的生命周期中使用 async ,在async 函数中使用 await 目的就是等第一个函数执行完成之后的到结果后再执行第二个函数。

3.3.async/await 的优势在于处理then链 (解决地狱回调)

单一的promise链并不能发现async/await的优势,但是如果需要解决多个promise组成的then链的时候,它的优势就能体现出来了,例如:

4.Generator

generator (生成器) 是 ES6 标准引入的新的数据类型,一个generator看上去像一个函数,但可以返回多次。

generator由 function* 定义,并且除了return 语句,还可以用yield返回多次。

注意:yield 表达式只能用在 Generator 函数里面,用在其他地方都会报错

我们先来看一个简单的例子:

  看看控制台的打印:

  • 可以看出next() 会返回一个对象,这个对象中有两个key 其中 value 表示 yield 语句后面的表达式的值,done是一个布尔值,表示函数体是否已经执行结束。

  • next() 方法会执行函数体,直到遇到的第一个yield语句,然后挂起函数执行,并将紧跟在 yield 后的表达式的值作为返回的对象的 value 值。等待后续调用,当再次执行 g.next() 时,执行流在挂起的地方继续执行,直到遇到第2个yield,依次类推。直到 return 为止,将 return 的值赋值给 value,若无 return 后面的值 value 都为 undefined,此时 done 值为 true。

  • var g = generator(); 进行实例化之后,generator() 里的代码不会主动执行。第一个 next() 永远是用于启动生成器,生成器启动后要想运行到最后,其内部的每个 yield 都会对应一个 next() ,所以 next() 永远都会比 yield 多一个。

再说一下next 传参:next方法的参数表示上一个yield表达式的返回值!!

function* foo(x) {
  let y = 2 * (yield (x + 1));
  let z = yield (y / 3);
  return (x + y + z);
}
 
let b = foo(5);
b.next()   // { value:6, done:false }  遇到第一个yield 将函数挂起,并将yield后的表达式的值返回给 value 5+1=6
b.next(12) // { value:8, done:false } (yield ( x+1) )表达式的值是12 所以  y 的值是 2*12=24, 遇到yield 将函数挂起,并将yield 表达式的值返回给对象的value  24/3=8
b.next(13) // { value:42, done:true }  yield (y/3)表达式的值是13 所以z的值是13, x 的值是5 ,所以x+y+z 等于 5+24+13=42

说了这么多 generator 和异步到底有什么关系呢? 我们来看看Promise + Generator 实现的异步控制。

 和async/await类似,这种实现也将异步方法转化成了同步的写法,实际上这就是 ES7中async/await的实现原理(将genWrap替换为async,将 yield 替换成 await )。

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>