从小知识点出发,彻底弄懂promise、async、await

前言

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的链式调用,传参更加方便,异步顺序更加清晰;

猜你喜欢

转载自blog.csdn.net/srj15110129498/article/details/127690288