JS的异步发展 - callback,Promise,Generator,async/await
JS异步的发展
对JS有所了解的人都知道,JS语言是单线程的,简单来说就是指一次只能完成一件任务。如果同时有许多个任务,就必须排队,前一个任务耗时长,但是后一个任务必须等待前一个任务完成。
单线程
比如,在做饺子时,必须先准备好馅和面团,才能开始包饺子。这就是单线程。
多线程
任务1和任务2可以同时做就是多线程。
因为js是单线程的,所以就有了同步异步的概念。
同步
就是一按顺序一步步执行。
异步
现在我们对单线程多线程和同步异步有了了解,我们来看看异步的发展时间线:
本文从发展时间线对异步的这四个点对异步做了介绍:
- callback
- Promise
- Generator
- async/await
一. callback
- 什么是回调函数?
A callback is a function that is passed as an argument to another
function and is executed after its parent function has completed.
callback
函数是一个以参数形式传递给另一个函数的函数,并且callback
函数必须等另一个函数执行完才会被调用!(当被调用时,另一个函数就是callback
函数的父函数)。
- 回调函数中的异步
- 首先,我们来了解一下回调
调用函数
b
时,执行顺序就b
函数中从上到下的顺序,先执行语句1、2
,而执行到callback
时回调回去先去调用a
函数,当a
函数执行完毕后,继续执行语句3、4
。callback()
语句放在任何一个语句后面都遵循上述原则。
- 回调的例子(无异步
//最简单的例子
function canYueHui(callback) {
console.log('我可以去约会吗?');
callback();
}
function goTo() {
console.log('sure!快去吧~别让约会对象等急了');
}
function canNotGo() {
console.log('不可以哦~因为你长的不好看');
}
canYueHui(goTo);
//输出
//我可以去约会吗?
// sure!快去吧~别让约会对象等急了
canYueHui(canNotGo);
//输出
// 我可以去约会吗?
// 不可以哦~因为你长的不好看
- 上面就是一个简单的例子,传入一个函数作为参数,然后在函数体中调用这个函数,就是一种
callback
的回调- 然后在
canYueHui
函数的内部调用goTo
函数来得到结果。
- callback中完成异步调用
下面代码,getArray
函数用来打印数组,而addArray
函数用来给数组添加元素。
// 复杂一些的例子
const array = ['我','是','一','个'];
function getArray() {
setTimeout(()=>{
array.forEach((ele)=>{
console.log(ele);
},1000);
})
}
// 无回调函数callback的时候 不可以添加函数
function addArray(element){
setTimeout(()=>{
array.push(element);
},2000);
}
getArray();
addArray('回调函数');
// 输出
// 我 是 一 个
上述代码中的
addArray
是没有使用callback的情况。先打印了array中的内容,然后在进行添加元素。所以输出中没有回调函数
四个字。延迟也只有一秒钟。
//先进行函数之后在运行回调函数 即 先运行addArray之后 在调用getArray函数
function addArray(element,callback){
setTimeout(()=>{
array.push(element);
callback();
},2000);
}
addArray('回调函数',getArray);
// 输出
// 我 是 一 个 回调函数
上述代码是使用callback调用
addArray
的情况。给addArray
函数传了getArray
作为参数,并在函数中调用该函数。使得打印是在添加元素之后进行,输出延迟了三秒钟,先进行函数之后在运行回调函数,即在setTimeout
中先运行了.push()
,在运行getArray
函数。
- 优点
解决了
同步问题
(即解决了一个任务时间长时,后面的任务排队,耗时太久,使程序的执行变慢问题)
- 缺点
- 回调地狱(多层级嵌套)
- 会导致逻辑混乱
- 耦合性高,改动一处就会导致全部变动
- 可读性差
- 比如: 回调地狱 可以一直回调下去
function sayNum(name, callback) {
setTimeout(() => {
console.log(name);
callback()
}, 1000)
}
sayNum('one', function () {
sayNum('two', function () {
sayNum('three', function () {
sayNum('four', function () {
console.log('结束了');
})
})
})
})
//打印结果 one two three four 结束了
JavaScript中Promise
的提出提出解决了callback中的回调地狱问题
二. Promise
使用Promise解决上面的代码
function f(num) {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log(num);
resolve(num);// 成功执行时返回正确的值。
}, 1000)
})
}
var p1 = f('one')
p1.then((data) => {
return f('two')
})
.then((data) => {
return f('three')
})
.then((data) => {
return f('four')
})
// 打印结果 one two three four
代码清晰可观,解决了逻辑混乱的问题
- 概念
- Promise 就是一个对象,用来传递异步操作的信息。
- 主要用于异步计算
- 可以将异步操作队列化,按照期望的顺序执行,返回符合预期的结果
- 可以在对象之间传递和操作promise,帮助我们处理队列
- Promise对象
2.1 三个状态之间的 转化
2.2 对象创建的介绍
Promise
构建函数接受一个函数作为参数,一个是resolve
,一个reject
.resolve
的是将pending
状态转为fulfilled
状态,而reject
是转为rejected
状态
- .then和.catch
- then
- 实例生成后,可以使用then方法分别来指定这两个状态的回调函数.
then
方法可以接受两个回调作为参数then
返回的是一个新的Promise
对象
resolve
的返回值作为then中的参数1传入reject
的返回值作为then中的参数2传入
let testPromise = new Promise((resolve,reject)=>{
let a = true;
if (a){
resolve('这是正确返回');
}else{
reject('这是一个报错');
}
});
testPromise.then((a)=>{
console.log(a);
},(err)=>{
console.log(err);
})
// a = false时 输出 这是一个报错
// a = true时 输出 这是正确返回
- 上述代码就是简单的例子,首先创建一个
Promise
对象,然后在a为true时调用resolve
回调函数,反之,则调用reject
回调函数。
- catch
catch
是用来接受在Promise
对象中抛出的错误的
const testCatch = new Promise((resolve,reject)=>{
throw new Error('只是一个简单的报错');
}).catch((err)=>{
console.log(err);
})
// 输出 只是一个简单的报错
- 上述代码就展示了抛出一个错误时,会调用
catch
- 当然也可以在
Promise
的参数函数中使用try...catch
来接收这个错误
- 优点
- 一旦状态
改变
,就不会再变
,任何时候
都可以得到这个结果- 可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数
- 一定程度上
解决了回调地狱
的问题
- 缺点
- 无法取消
Promise
- 当处于
pending
状态时,无法得知目前进展到哪一个阶段- 代码冗余,有一堆任务时也会使语义不清晰
- 每一个
then
方法内部是一个独立的作用域,要是想共享数据,就要将部分数据暴露在最外层,在then
内部赋值一次
三. Generator
我们来看看generator中异步的使用
- 概念
Generator
函数是ES6中提供的一种异步编程解决方案。语法上,首先可以把它理解成,Generator
函数是一个状态机,封装了多个内部状态,需要使用next()
函数来继续执行下面的代码。
- 两个特征
function
与函数名之间带有(*)
- 函数体内部使用
yield
表达式,函数执行遇到yield
就返回。
- 例子
- 下面是一个简单的
Generator
举例(无异步情况
const helloGenerator = function* (){
yield 'hello';
yield 'Generator';
return 'I Love You';
}
const testGenerator = helloGenerator();
console.log(testGenerator);
//Object [Generator] {}
console.log(testGenerator.next());
//{ value: 'hello', done: false }
console.log(testGenerator.next());
//{ value: 'Generator', done: false }
console.log(testGenerator.next());
//{ value: 'I Love You', done: true }
console.log(testGenerator.next());
//{ value: undefined, done: true }
- 上述代码就创建了一个名为
helloGenerator
的Generator
函数,代码中有两个yield
,一个return
。- 第一次调用
.next()
输出的是遇到第一个yield
时返回后面的hello
,而返回的对象就是value
和done
。value
返回的就是yield
后面的值,而done
是false
,则表示遍历还没有结束。- 第二次调用
.next()
返回的是第二个yield
后面的Generator
- 第三次调用
.next()
返回的则是return之后的内容。如果遇到return之后,就不会在继续执行下面的内容了,也就是说到return
之后就结束了done
变为true
.- 第四次调用
.next()
函数的value
都是undefined
,且done
都为true
.
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
依据
Promise
的原理来实现异步,配合.then
和Generator函数
的.next
来实现异步。
- 优点
可以控制函数的执行,可以配合
co函数库
使用
- 缺点
- 流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)
- 需要手动使用
next()
语句
async/await
是对Generator
的包装,是它的提升。
四. async/await
与Generator
异步的比较
async function gainSth(){
var result1 = await fetch(url);
var result2 = await fetch(url);
var result3 = await fetch(url);
var result4 = await fetch(url);
}
下面的三个await
会在第一个fetch函数
执行完毕之后才继续执行。简单的说,将异步改为了“同步”。
- 概念
也是用来处理异步的,是
Generator
函数的改进,背后的原理是Promise
。稍微与上面比较一下,可以发现async
函数就是将Generator
函数的星号(*)
替换成async
,将yield
替换成了await
。
- 相比于
Generator
函数的四个改进
- 内置执行器。意味着不需要像
Generator
一样调用next
函数或co模块。- 语义更好。使用
async
表示函数里有异步操作,await
表示紧跟在后面的表达式需要等待结果- 更广的适用性。
async
和await
后面跟的都是Promise
函数,原始数据类型会被转为Promise
。- 返回值是
Promise
。async
返回值是Promise
。可以直接使用.then
和.catch
- 简单举例
fun1
函数返回resolve
时
// 单独 async的使用
async function fun1() {
return 'ni';
// 等价于
return new Promise((resolve) => { resolve('ni'); });
// 等价于
return Promise.resolve('ni');
}
console.log(fun1());
// 输出
// Promise { 'ni' }
上述代码只使用了
async
建立了一个函数fun1
,内部没有使用await
,运行一下可以发现返回的是一个Promise
对象。上述三种return
的写法都是等价的。
//asyns/await结合使用
async function testAwait() {
var knowAwait = await fun1();
console.log('123');
console.log(knowAwait);
}
testAwait()
// 123
// ni
上面代码结合了
async
和await
建立了一个函数testAwait
.函数头加上async
,在函数内部使用await
。遇到await
时,会阻塞后面的代码先返回,等异步操作执行完成时再来继续执行后面的语句。
fun1
返回reject
时
async function fun2(){
return Promise.reject('NO');
}
async function testReject() {
try{
var isReject = await fun2();
}catch(error){
console.log(error);
}
console.log('123');
}
testReject();
// 输出
// NO
// 123
如果返回的时
reject
时,后面的代码就不会在执行,并产生错误,原因就是NO
,要加上try...catch
才可以执行
- 优点
- 相比于
Promise
,简化了then
的链式写法- 对
Generator
函数的封装,实现最简洁、语义话
- 缺点
await
是阻塞后面的代码,写多了容易造成性能问题
五. 异步总结
- JS异步发展从
callback--->Promise--->Generator--->async/await
。
异步发展 | 优点 | 缺点 |
---|---|---|
callback | 解决了同步问题 | 1. 回调地狱 1.1 导致逻辑混乱1.2 耦合性高,牵一发而动全身 2. 可读性差 |
promise | 1. 解决了回调地狱 2. 可读性好 3. 状态一经改变,不会再变 | 1. 无法取消promise 2. 处于pending状态时,无法得知目前到哪一个阶段 3. 代码冗余 语义不清 4. 每一个 then 方法内部是一个独立的作用域,要是想共享数据,就要将部分数据暴露在最外层,在then 内部赋值一次 |
Generator | 可以控制函数的执行,可以配合co函数库使用 | 1. 流程管理却不方便(即何时执行第一阶段、何时执行第二阶段) 2. 需要手动使用next()语句 |
async/await | 对Generator函数的封装,实现最简洁、语义话 | 阻塞后面的代码,可能导致性能问题 |
async/await 与 callback
- 相比较于callback,async/await也解决了同步问题,而且不会有回调地狱,代码耦合性弱,可读性好。
async/await 与 Promise
- 写码时可以非常优雅地处理异步函数,彻底告别回调恶梦和无数的 then 方法
- 每一个
then
方法内部是一个独立的作用域,要是想共享数据,就要将部分数据暴露在最外层,在then
内部赋值一次
async/await 与 Generator
async/await
是Generator函数
的一个语法糖
,是对Generator函数
的改进async/await
自带执行机,只需要执行一次async和await
,比起(*)
和yield
,语义更清楚了。async
表示函数里有异步操作,await
表示紧跟在后面的表达式需要等待结果- 返回的是
Promise
。async函数
完全可以看作多个异步操作,包装成的一个Promise 对象
,而await
命令就是内部then
命令的语法糖
所以目前来说,async/await
是最好的异步解决方案
了。
参考文献
- 异步的发展相关
- callback相关
- Promise相关
- Generator相关
- async/await相关
纯手画手写,如果有问题希望指出,第一次写这么长的技术文档,可能有点偏离了异步的概念。
谢谢观看,有问题请指出!!