1 什么同步&异步
同步:简单来说,就是顺序执行,指的是同一时间只能做一件事情,只有目前正在执行的事情做完之后,才能做下一件事情。
同步操作的优点在于做任何事情都是依次执行,井然有序,不会存在大家同时抢一个资源的问题。
同步操作的缺点在于会阻塞后续代码的执行。如果当前执行的任务需要花费很长的时间,那么后面的程序就只能一直等待。从而影响效率,对应到前端页面的展示来说,有可能会造成页面渲染的阻塞,大大影响用户体验。
异步:指的是当前代码的执行不影响后面代码的执行。当程序运行到异步的代码时,会将该异步的代码作为任务放进任务队列,而不是推入主线程的调用栈。等主线程执行完之后,再去任务队列里执行对应的任务。
因此,异步操作的优点就是:不会阻塞后续代码的执行。
2 js中异步的应用场景
定时任务:setTimeout、setInterval
网络请求:ajax请求、动态创建img标签的加载
事件监听器:addEventListener
3 实现异步的四种方法
对于setTimeout、setInterval、addEventListener这种异步场景,不需要我们手动实现异步,直接调用即可。
但是对于ajax请求、node.js中操作数据库这种异步,就需要我们自己来实现
1、回调函数
简单理解:函数可以作为一个参数传递到另外一个函数中。
<script>
function add(num1, num2, callback) {
var sum = num1 + num2;
callback(sum);
}
function print(num) {
console.log(num);
}
add(1, 2, print); //3
</script>
这个code就是简单的回调函数,回调函数中执行的是我们请求成功之后要做的进一步操作。 这样就初步实现了异步,但是回调函数有一个非常严重的缺点,那就是回调函数里再发起一个请求时,就需要多层嵌套,就形成了回调地狱的问题,为解决回调地狱问题,后来提出了Promise、async/await、generator的概念
2、Promise
Promise作为典型的微任务之一,它的出现可以使JS达到异步执行的效果。
promise 它会有三个状态
- 持续: pending
- 成功: fulfilled
- 失败: rejected
promise 实例化对象 上有两个方法 .then .catch
一个Promise函数的结构如下列代码如下:
const promise = new Promise((resolve, reject) => {
resolve('a');
});
promise
.then((arg) => { console.log(`执行resolve,参数是${arg}`) })
.catch((arg) => { console.log(`执行reject,参数是${arg}`) })
.finally(() => { console.log('结束promise') });
如果,我们需要嵌套执行异步代码,相比于回调函数来说,Promise的执行方式如下列代码所示:
const promise = new Promise((resolve, reject) => {
resolve(1);
});
promise.then((value) => {
console.log(value);
return value * 2;
}).then((value) => {
console.log(value);
return value * 2;
}).then((value) => {
console.log(value);
}).catch((err) => {
console.log(err);
});
即,通过then来实现多级嵌套(**链式调用
每个Promise都会经历的生命周期是:
进行中(pending) - 此时代码执行尚未结束,所以也叫未处理的(unsettled) 已处理(settled) - 异步代码已执行结束 已处理的代码会进入两种状态中的一种: 已完成(fulfilled) - 表明异步代码执行成功,由resolve()触发 已拒绝(rejected)- 遇到错误,异步代码执行失败 ,由reject()触发 因此,pending,fulfilled, rejected就是Promise中的三种状态
牢记:在Promise中,要么包含resolve()来表示Promise的状态为fulfilled,要么包含reject()来表示Promise的状态为rejected。 不然我们的Promise就会一直处于pending的状态,直至程序崩溃…
除此之外,Promise不仅很好的解决了链式调用的问题,它还有很多神奇的操作:
Promise.all(promises):接收一个包含多个Promise对象的数组,等待所有都完成时,返回存放它们结果的数组。如果任一被拒绝,则立即抛出错误,其他已完成的结果会被忽略
Promise.allSettled(promises): 接收一个包含多个Promise对象的数组,等待所有都已完成或者已拒绝时,返回存放它们结果对象的数组。每个结果对象的结构为{status:‘fulfilled’ // 或 ‘rejected’, value // 或reason}
Promise.race(promises): 接收一个包含多个Promise对象的数组,等待第一个有结果(完成/拒绝)的Promise,并把其result/error作为结果返回
3、Generator
Generator是ES6提出的一种异步编程的方案。因为手动创建一个iterator十分麻烦,因此ES6推出了generator,用于更方便的创建iterator。也就是说,Generator就是一个返回值为iterator对象的函数。
- 什么是iterator
iterator中文名叫迭代器。它为js中各种不同的数据结构(Object、Array、Set、Map)提供统一的访问机制。任何数据结构只要部署了Iterator接口,就可以完成遍历操作。 因此iterator也是一种对象,不过相比于普通对象来说,它有着专为迭代而设计的接口。
iterator 的作用:
为各种数据结构,提供一个统一的、简便的访问接口; 使得数据结构的成员能够按某种次序排列; ES6 创造了一种新的遍历命令for…of循环,Iterator 接口主要供for…of消费 iterator的结构: 它有next方法,该方法返回一个包含value和done两个属性的对象(我们假设叫result)。value是迭代的值,后者是表明迭代是否完成的标志。true表示迭代完成,false表示没有。iterator内部有指向迭代位置的指针,每次调用next,自动移动指针并返回相应的result。 使用iterator遍历对象的写法是
function createIterator(items) {
var i = 0;
return {
next: function () {
var done = (i >= items.length);
var value = !done ? items[i++] : undefined;
return {
done: done,
value: value
};
}
};
}
var iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// 后续的所有调用返回的结果都一样
console.log(iterator.next()); // "{ value: undefined, done: true }"
使用iterator迭代器遍历数组的过程为:
let arr = ['a','b','c'];
let iter = arr[Symbol.iterator]();
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
for … of的循环内部实现机制其实就是iterator,它首先调用被遍历集合对象的 Symbol.iterator 方法,该方法返回一个迭代器对象,迭代器对象是可以拥有.next()方法的任何对象,然后,在 for … of 的每次循环中,都将调用该迭代器对象上的 .next 方法。然后使用for i of打印出来的i也就是调用.next方法后得到的对象上的value属性。
- Generator
Generator 函数是 ES6 提供的一种异步编程解决方案。形式上,Generator 函数是一个普通函数,但是有两个特征:
1 function关键字与函数名之间有一个星号
2 函数体内部使用yield语句,定义不同的内部状态
Generator函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object)
Generator有函数该有的return方法和throw方法
在普通函数中,我们想要一个函数最终的执行结果,一般都是return出来,或者以return作为结束函数的标准。运行函数时也不能被打断,期间也不能从外部再传入值到函数体内。 但在generator中,就打破了这几点,所以generator和普通的函数完全不同。 当以function*的方式声明了一个Generator生成器时,内部是可以有许多状态的,以yield进行断点间隔。期间我们执行调用这个生成的Generator,他会返回一个遍历器对象,用这个对象上的方法,实现获得一个yield后面输出的结果。
function* generator() {
yield 1
yield 2
};
let iterator = generator();
iterator.next() // {value: 1, done: false}
iterator.next() // {value: 2, done: false}
iterator.next() // {value: undefined, done: true}
- yield和return的区别:
1 都能返回紧跟在语句后面的那个表达式的值
2 yield相比于return来说,更像是一个断点。遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。
3 一个函数里面,只能执行一个return语句,但是可以执行多次yield表达式。
4 正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield
语法注意点:
yield表达式只能用在 Generator 函数里面
yield表达式如果用在另一个表达式之中,必须放在圆括号里面
yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
如果 return 语句后面还有 yield 表达式,那么后面的 yield 完全不生效
- 使用Generator的其余注意事项
需要注意的是,yield 不能跨函数。并且yield需要和*配套使用,别处使用无效
箭头函数不能用做 generator
- Generator到底有什么用
因为Generator可以在执行过程中多次返回,所以它看上去就像一个可以记住执行状态的函数,利用这一点,写一个generator就可以实现需要用面向对象才能实现的功能
Generator还有另一个巨大的好处,就是把异步回调代码变成“同步”代码。这个在ajax请求中很有用,避免了回调地狱.
4、async/await
async/await是ES7提出的关于异步的终极解决方案
作用: 能帮助我们把 异步代码, 写的和 同步代码一样
书写: 函数开头必须书写 async 表明内部可以书写 await
await 后边需要跟着 promise
await 表示等到的意思, 执行到 fn() 虽然是异步的,但是因为有 await 关键字, 此时不会往下继续执行,而是等待 fn() 执行完毕, 在往下执行
关于async/await是Generator的语法糖:
所谓Generator语法糖,表明的就是aysnc/await实现的就是generator实现的功能。但是async/await比generator要好用。因为generator执行yield设下的断点采用的方式就是不断的调用iterator方法,这是个手动调用的过程。针对generator的这个缺点,后面提出了co这个库函数来自动执行next,相比于之前的方案,这种方式确实有了进步,但是仍然麻烦。而async配合await得到的就是断点执行后的结果。因此async/await比generator使用更普遍。
- async函数对 Generator函数的改进,主要体现在以下三点:
内置执行器:Generator函数的执行必须靠执行器,因为不能一次性执行完成,所以之后才有了开源的 co函数库。但是,async函数和正常的函数一样执行,也不用 co函数库,也不用使用 next方法,而 async函数自带执行器,会自动执行。
适用性更好:co函数库有条件约束,yield命令后面只能是 Thunk函数或 Promise对象,但是 async函数的 await关键词后面,可以不受约束。
可读性更好:async和 await,比起使用 *号和 yield,语义更清晰明了
- 关于async/await是Promise的语法糖:
如果不使用async/await的话,Promise就需要通过链式调用来依次执行then之后的代码:
function counter(n){
return new Promise((resolve, reject) => {
resolve(n + 1);
});
}
function adder(a, b){
return new Promise((resolve, reject) => {
resolve(a + b);
});
}
function delay(a){
return new Promise((resolve, reject) => {
setTimeout(() => resolve(a), 1000);
});
}
// 链式调用写法
function callAll(){
counter(1)
.then((val) => adder(val, 3))
.then((val) => delay(val))
.then(console.log);
}
callAll();//5
虽然相比于回调地狱来说,链式调用确实顺眼多了。但是其呈现仍然略繁琐了一些。
而async/await的出现,就使得我们可以通过同步代码来达到异步的效果
async function callAll(){
const count = await counter(1);
const sum = await adder(count, 3);
console.log(await delay(sum));
}
callAll();// 5
- async/await 的问题
没有办法捕获到 错误, 只能接受 promise 的成功状态,如果报错, 会中断程序执行
- 解决办法:
1 try...catch
首次执行的时候 会走 try 这个分支, 如果这个位置有报错,他会结束执行 try 分支, 然后走 catch 分支,如果再运行 try 分支的时候, 没有报错, 那么 catch 不会运行
举例:
async function newFn() {
try {
let r1 = await fn()
console.log(r1)
} catch (error) {
console.log(error)
console.log('如果失败了, 执行这行代码, 提示用户网络有问题')
}
}
newFn()
2: 更改 promise 的封装
原因: promise 执行 reject 时 async await 不能捕获到错误,
解决: 让这个 promise 不管什么情况 都返回 resolve,我们通过 返回的 参数, 区分现在时成功还是失败
开发中 对象内的 code 如果为0, 一般代表失败,对象内的 code 如果为1, 一般代表成功
举例:
function fn() {
const p = new Promise(function (reslove, reject) {
//给timer设定一个随机时间
const timer = Math.ceil(Math.random() * 3000) + 1000
setTimeout(() => {
if (timer > 3500) {
//判断timer大于3500毫秒提示网络中断
// reject('你的网络中断')
reslove({
code: 0,
msg: '你的网络中断'
})
} else {
reslove({
code: 1,
msg: '你的网络良好'
})
}
}, timer)
})
return p
}
async function newFn() {
let r1 = await fn()
if (r1.code === 0) {
console.log('您的网络有问题')
} else {
console.log(r1.msg)
}
}
newFn()
总结
Promise让异步执行看起来更清晰明了,通过then让异步执行结果分离出来。
-
async/await其实是基于Promise的。async函数其实是把promise包装了一下。使用async函数可以让代码简洁很多,不需要promise一样需要些then,不需要写匿名函数处理promise的resolve值,也不需要定义多余的data变量,还避免了嵌套代码。
-
async函数是Generator函数的语法糖。async函数的返回值是 promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。同时,我们还可以用await来替代then方法指定下一步的操作。
-
Promise+async的操作最为常见。是因为Generator被async替代了