前言
promise一直在用,但就是不知道该怎么系统的去串到一起,简单易懂的讲给别人听,所以又整理了下相关的小知识点,让我们一起来看看吧~
要彻底弄懂promise、async、await,涉及到3个重要的点:
1. 同步、异步,会涉及到堆栈、消息队列、事件循环的小知识点;
2. 异步的解决方案,主要是回调函数和promise;
3. 异步的终极解决方案:Generator函数 和它的语法糖 async,await;
要完全理解,涉及到非常多的小知识点,还需从js是单线程说起。
js为什么是单线程?
js 诞生于1995年,自诞生起,就是单线程;
浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程,其中浏览器渲染进程(浏览器内核)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等;
其中包含的线程有:GUI 渲染线程(负责渲染页面,解析 HTML,CSS 构成 DOM 树)、JS 引擎线程、事件触发线程、定时器触发线程、http 请求线程等。
主线程:即 js 引擎执行的线程,该线程只有一个,页面渲染、函数处理都在这个主线程上执行。
工作线程:也称幕后线程,这个线程可能存在于浏览器或js引擎内,与主线程是分开的,处理文件读取、网络请求等异步事件。
可以看出,异步操作都是放到事件循环队列里面,等待主执行栈来执行,并没有专门的异步执行线程,所以说js是单线程。
这里提到了进程和线程,那么我们简单介绍一下:
进程和线程
进程:资源分配的最小单位;
线程:CPU调度的最小单位;
一个系统中,有很多进程,它们都会使用内存。为了确保内存不被别人使用,每个进程所能访问的内存都是设定好的。一人一份,互不干扰,进程需要管理好它的资源;线程作为进程的一部分,扮演的角色就是怎么利用中央处理器(CPU)运行代码。这其中牵扯到的最重要资源的是中央处理器(CPU)和其中的寄存器,及线程的栈(stack)。
这里需要强调的是,线程关注的是中央处理器(CPU)的运行,而不是内存等资源的管理。
总结:线程是进程的一部分,线程主抓中央处理器(CPU)执行代码的过程,其余的资源的保护和管理由整个进程去完成。
js为什么需要是单线程的?
js 主要用途是操作DOM,和用户的交互,如果js设计为多线程,那么同时修改和删除同一个dom,浏览器又该如何执行呢? 所以只能是单线程。
下面我们就具体说一下异步和同步是什么
1. 同步、异步
同步:一句一句的执行,严格按照先后顺序执行,第一个操作没有执行完就不会执行下一个操作;而js是单线程,就是同步
这里就涉及到栈的概念,后进先出;
异步:不一定按先后顺序执行,如果碰到耗时操作(如等待网络请求的响应),不会像同步一样阻塞,可以接着执行后面的操作,等响应之后再执行耗时操作的下一步处理;
为提高效率,把浏览器的多个内核都用起来,HTML5提出Web Worker标准,允许javascript创建多个线程,但子线程完全受主线程控制,且不得操作dom;
举个代码例子:
function myFun() {
setTimeout(() => {
console.log('执行了!');
}, 2000);
};
console.log('啦啦啦');
myFun();
console.log('哈哈哈');
执行顺序为:
啦啦啦 、哈哈哈 、 执行了!(2s后)
这里涉及到一个消息队列的概念
消息队列:同步时,浏览器会维护一个执行栈,即调用栈(后进先出);在开启多线程后,浏览器还会维护一个消息列表,除主线程外,其余都是副线程,副线程的集合被称为消息列表;
在上述例子中,console.log('啦啦啦'); console.log('哈哈哈'); 为主线程,setTimeout为副线程。
加上promise后看看执行顺序
setTimeout(() => {
console.log('定时器');
})
new Promise(resolve => {
console.log('promise');
resolve();
}).then(() => {
console.log('then');
})
console.log('主线程');
执行顺序为:
promise、主线程、then、定时器
分析:promise是主线程,先执行,打印“promise”,然后继续执行主线程,打印“主线程”,主线程走完了,开始走消息列表,先执行了promise.then,后执行了setTimeout,因为setTimeout是宏任务,优先级较低,而promise.then是微任务,所以setTimeout会排在promise.then的后面。
涉及到两个新概念:微任务 和 宏任务;
promise.then比定时器先执行,这里又涉及到一个新的概念事件循环(Event Loop);
事件循环:选择副线程中谁先执行的过程;
宏任务、微任务
宏任务:js同步执行的代码块,setTimeout、setInterval、XMLHttprequest、setImmediate、I/O、UI rendering等;
微任务:promise.then、process.nextTick(node环境)、Object.observe, MutationObserver等;
浏览器执行的顺序:
1. 执行主代码块(宏任务);
2. 遇到promise,把then中的内容放至微任务队列中;
3. 遇到setTimeout、setInterval,放宏任务队列;
4. 一个宏任务执行完成后,检查是否有微任务;
5. 有的话执行所有微任务;
6. 执行完之后,开始执行下一个宏任务;
这个选择谁先执行的过程就是事件循环(Event Loop)。
2. 异步的解决方案:回调函数 & promise
回调函数
回调函数是一个函数,将会在另一个函数完成执行后立即执行。
举个例子看一下:
f2 = () => {
console.log('2')
}
f1 = (callback) => {
console.log('1')
setTimeout(() => {
callback();
}, 2000);
console.log('3')
}
f1(f2);
执行顺序为:1、3、2(2s后执行)
主线程执行结束后,会通过回调函数callback调用f2()
读取一个文件,fileReader就是一个异步请求,这个异步请求就是通过回调函数的方式获取的,如果我们需要读取多个文件,读完这个文件读下一个文件,按回调函数来写就是这样:
let reader = new FileReader()
let file1 = input.files[0], file2 = input.files[1], file3 = input.files[2]
reader.readAsText(file1, (err, data) => {
if (err) {
console.log(err)
} else {
console.log(data)
}
reader.readAsText(file2, (err, data) => {
if (err) {
console.log(err)
} else {
console.log(data)
}
reader.readAsText(file3, (err, data) => {
if (err) {
console.log(err)
} else {
console.log(data)
}
})
})
})
但这样写的话,如果文件较多,代码可读性就很差了,这就是我们常说的“回调地狱”。
回调地狱:多级的异步的嵌套调用的问题。
那么,我们要解决这种多级嵌套调用,es6 提供了promise,用来解决这种问题;
Promise
promise有三种状态:成功(fulfilled)、失败(rejected)、等待(pending);
举个例子看看:
let a = 2
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
if (a) {
console.log('a=', a)
resolve(2000)
} else {
reject(2000)
}
}, 1000)
console.log(1)
})
promise.then(res => {
console.log(res)
}, err => {
console.log(err)
})
执行结果: 1、 a= 2、 2000(1s后执行)
1. then 链式操作
Promise对象的then方法返回一个新的Promise对象,可以通过链式调用then();
then() 接收两个函数作为参数,第一个参数是promise执行成功时的回调,第二个参数是promise执行失败时的回调;
延续上面的例子,看下链式调用:
promise.then(res => {
console.log(res)
return res + 1000
}).then(res => {
console.log(res)
return res + 2000
}).then(res => {
console.log(res)
})
输出的结果:2000、 3000、 5000
2. catch捕捉
promise 除了提供then(),还提供了catch(),用来捕捉错误的回调;
let a
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
if (a) {
console.log('a=', a)
resolve(2000)
} else {
reject(`a=${a}`)
}
}, 1000)
console.log(1)
})
promise.catch(res => {
console.log(res)
})
输出结果为:1、 a=undefined
3. all()
如果有3个接口分别是A,B,C,必须在这三个接口成功之后,才能调用D接口,链式调用then(),一层套一层,代码不优雅,
promise提供了all(),接收一个Promise对象组成的数组作为参数,当数组所有的Promise对象的状态都变成resolved或rejected时,才会去调用then(),这样的话,既实现了我们想要的功能,代码上看着也简洁好看很多;
const getA = new Promise((resolve, reject) => {
console.log('执行A')
resolve()
})
const getB = new Promise((resolve, reject) => {
console.log('执行B')
resolve()
})
const getC = new Promise((resolve, reject) => {
console.log('执行C')
resolve()
})
Promise.all([getA, getB, getC]).then(res => {
console.log('A,B,C都已执行完毕')
})
4. race()
如果有3个接口分别是A,B,C,只要有一个接口响应了,就可以调用D接口
与Promise.all() 相似的是,Promise.race() 都是以一个Promise对象组成的数组作为参数,不同的是,只要当数组中的其中一个Promsie状态变成 resolved 或 rejected 时,就可以调用.then()了。
const getA = new Promise((resolve, reject) => {
console.log('执行A')
resolve()
})
const getB = new Promise((resolve, reject) => {
console.log('执行B')
resolve()
})
const getC = new Promise((resolve, reject) => {
console.log('执行C')
resolve()
})
Promise.race([getA, getB, getC]).then(res => {
console.log(res)
})
promise 是ES6 用来解决异步的方法,除此之外,ES6 还提供了async 和 await,async 和 await 是Generator函数的语法糖。
Generator 函数
Generator 是一个迭代生成器,其返回值为迭代器(lterator),是ES6引入的新的数据类型,主要用于异步编程,它借鉴于Python中的generator概念和语法;
Generator 函数内有两个重要方法: 1. yield 表达式; 2. next();
Generator 函数是分段执行的,yield 表达式是暂停执行的标记,而 next方法可以恢复执行;
Generator 函数自己不会执行,而是会返回一个遍历器对象,遍历器对象会通过.next()方法依次调用各个状态;
Generator 函数的基础语法:
function* persition(){
yield '我是generato生成器';
yield '我要开始了';
return '结束'
}
//创建一个句柄,赋值给生成器
var iterator = persition();
//直接调用并不能被立即执行
console.log(iterator)
//需使用next()方法来调用这个生成器 next()方法调用一次,
//并不能将Generator函数内的yield值全部打印出来,需要依次进行调用
console.log(iterator.next())
console.log(iterator.next())
//如果iterator对象内done为true,证明Generator函数执行完毕
console.log(iterator.next())
输出结果:
persition {<suspended>}
{value: '我是generato生成器', done: false}
{value: '我要开始了', done: false}
{value: '结束', done: true}
Generator函数除能控制函数分段执行之外,还有一个重要作用是消息传递(即可以传值);
function *doSomething() {
let x = yield '下班啦'
console.log(x)
return (x * 2)
}
let something = doSomething()
console.log(something.next(1))
console.log(something.next(2))
输出结果:
{value: '下班啦', done: false}
2
{value: 4, done: true}
分析:
第一个next() 是Generator函数的启动器,传值会被忽略,打印的是yield 后面的值,yield 后面的值并不会赋值给 x;
暂停执行的时候,yield 可以接收下一个启动他的next() 传来的值,也就是说,第二个next() 传的参数会把第一个 yield 的值替换掉,这时,x赋值为2,return 2*2;
注:yield 的用法和return 有点像,如果yield后面没值,就是return undefined,最后一个next(),得到的是return 的值,如果没有,就是undefined;
function *doSomething() {
let x = yield '下班啦'
console.log(x)
let y = yield (x + 3)
console.log(y)
let z = yield (y * 3)
return (x * 2)
}
let something = doSomething()
console.log(something.next(1))
console.log(something.next(2))
console.log(something.next())
console.log(something.next())
输出结果:
{value: '下班啦', done: false}
2
{value: 5, done: false}
undefined
{value: NaN, done: false}
{value: 4, done: true}
前两个next() 和上个例子一样,第三个next() 传入的为空,y打印的就是undefined,undefined * 3就是NaN;第四个next() 传入的为空,return 的是x 的值,而在第二个next()里,传入值为2,所以return 的是2*2,即4;
async、await
async、await 是Generator函数的语法糖,原理是通过Generator函数加自动执行器来实现,这样就可以通过 await 来把函数分状态执行,但是又不用一直next,可以自动执行,就很nice。
async 就相当于generator函数中的*,await 相当于yield,async 用于申明一个function 是异步的,而 await 用于等待一个异步方法执行完成。
async 有3个特点:
1. 函数前面会加一个async修饰符,证明该函数是异步函数;
2. await 是一个运算符,用于组成表达式,会阻塞后面代码的执行;
3. await 如果等到的是Promise对象,则得到的是Promise对象的 resolve值;
举个例子看看:
function fn() {
return new Promise(resolve =>{
resolve('下班啦')
})
}
async function getSomething(){
let x = await fn()
console.log('x:', x)
}
getSomething()
输出结果: x: 下班啦
async function getSomething(){
return await '下班啦'
}
console.log(getSomething())
getSomething().then(res => {
console.log(res)
})
输出结果:Promise {<pending>} 、 下班啦
1. async 返回的是一个promise对象,函数内部 return 返回的值,会成为 then 方法回调函数的参数;
2. await 如果等到的不是promise对象,就会得到一个表达式的运算结果;
总结:await 不能工作在顶级作用域中,await 关键字修饰的东西只能用在 async 修饰的函数中。await
可以让 JavaScript 进行等待,直到一个 promise 执行并返回它的结果,JavaScript才会继续往下执行。
async 用来修饰函数,可以用来修饰内部没有 await 的函数。当然,如果这个函数有 return 语句,js 会自动将返回值包装成 resolved 值(promise 对象)。
async function fn() {
return 1
}
等价于:
async function fn() {
return Promise.resolve(1)
}
举一个常用的简单例子看看
async function fn() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('done')
resolve()
}, 1000)
})
await promise // 直到promise返回一个resolve值(*)
console.log('开始执行')
}
fn()
输出结果:
done(1s后执行)
开始执行
总结:从回调函数,到promise,再到generator,再到async / await,这四种分别代表了JavaScript异步编程解决方案的进化路程。async和generator函数主要就是为了解决异步的并发调用,直接将参数从then里取出来,相比promise的链式调用,传参更加方便,异步顺序更加清晰;