同步和异步
相信大家对同步异步的概念不陌生了。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 请看下面代码:
先分析这个过程:
- 首先
testPromise
函数会执行console.log
直接打印“执行完成promise”,然后执行then方法接受resolve里面的参数“成功返回出去的数据”,这个参数作为testPromise2
函数的如入参。testPromise2
函数会执行console.log
打印出“成功返回出去的数据”,然后执行then方法,接受resolve里面的参数“成功返回出去的数据_2”,这个参数作为testPromise3
函数的如入参。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 )。