回调函数
f1();
f2();
如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数。
function f1(callback){
setTimeout(function () {
// f1的任务代码
callback();
}, 1000);
}
执行代码就变成下面这样:
f1(f2);
如果有更多的异步处理任务,那么整段代码充满了回调嵌套,代码不仅在纵向扩展,横向也在扩展。我相信,对于任何人来说,调试起来都会很困难,我们不得不从一个函数跳到下一个,再跳到下一个,在整个代码中跳来跳去以查看流程,而最终的结果藏在整段代码的中间位置。真实的JavaScript程序代码可能要混乱的多,使得这种追踪难度会成倍增加。这就是我们常说的回调地狱(Callback Hell)。
控制反转就是把自己程序一部分的执行控制交给某个第三方,在你的代码和第三方工具直接有一份并没有明确表达的契约。 既然是无法控制的第三方在执行你的回调函数,那么就有可能存在以下问题,当然通常情况下是不会发生的:
调用回调过早 调用回调过晚 调用回调次数太多或者太少 未能把所需的参数成功传给你的回调函数 吞掉可能出现的错误或异常 ...... 这种控制反转会导致信任链的完全断裂,如果你没有采取行动来解决这些控制反转导致的信任问题,那么你的代码已经有了隐藏的Bug,尽管我们大多数人都没有这样做。 这里,我们引出了回调函数处理异步的第二个问题:控制反转。
综上,回调函数处理异步流程存在2个问题:
- 缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
- 缺乏可信任性: 控制反转导致的一系列信任问题
Promise
Promise解决的是回调函数处理异步的第2个问题:控制反转。
简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。比如,f1的回调函数f2,可以写成:
f1().then(f2);
这样写的优点在于,回调函数变成了链式写法,无论有再多的业务依赖,通过多个then(...)来获取数据,让代码只在纵向进行扩展,另外一点就是逻辑性更明显了,将异步业务提取成单个函数,整个流程可以看到是一步步向下执行的,依赖层级也很清晰,最后需要的数据是在整个代码的最后一步获得。
Promise是如何解决控制反转带来的信任缺失问题
首先明确一点,Promise可以保证以下情况,引用自JavaScript | MDN:
- 在JavaScript事件队列的当前运行完成之前,回调函数永远不会被调用
- 通过 .then 形式添加的回调函数,甚至都在异步操作完成之后才被添加的函数,都会被调用
- 通过多次调用 .then,可以添加多个回调函数,它们会按照插入顺序并且独立运行
Generator
Generator 介绍
Generator 函数是 ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句(结束执行)。
调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。
下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
上面代码一共调用了四次next方法。
调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。
yield 与 return
yield表达式与return语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式。正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield。从另一个角度看,也可以说Generator生成了一系列的值,这也就是它的名称的来历(英语中,generator 这个词是“生成器”的意思)。
Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。
function* f() {
console.log('执行了!')
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000);
上面代码中,函数f如果是普通函数,在为变量generator赋值时就会执行。但是,函数f是一个 Generator 函数,就变成只有调用next方法时,函数f才会执行。
Generator 实现异步
function getUserId () {
$.ajax({
type: 'get',
url: 'http://localhost:3000/api/userId',
success: function (data) {
userId = data;
it.next(userId);
}
error: function (err) {
console.log(err);
}
});
}
function newBlog (userId) {
$.ajax({
type: 'get',
url: 'http://localhost:3000/api/blog/new',
data: {
userId: userId
},
success: function (data) {
blogData = data;
it.next(newBlog);
}
error: function (err) {
console.log(err);
}
});
}
function getBlogData (blogData) {
$.ajax({
type: 'get',
url: 'http://localhost:3000/api/new/getData',
data: {
blogId: newBlog.blogId,
},
success: function (blogId) {
it.next(blogId);
}
error: function (err) {
console.log(err);
}
});
}
function *main () {
let userId = yield getuserId();
let newBlog = yield newBlog(userId);
let blogData = yield getBlogData(blogData);
console.log('新建博客数据:', blogData);
}
// 生成迭代器实例
var it = main();
// 运行第一步
it.next();
console.log('不影响主线程执行');
我们注意*main()生成器内部的代码,不看yield关键字的话,是完全符合大脑思维习惯的同步书写形式,把异步的流程封装到外面,在成功的回调函数里面调用it.next(),将传回的数据放到任务队列里进行排队,当JavaScript主线程空闲的时候会从任务队列里依次取出回调任务执行。
综上,生成器Generator解决了回调函数处理异步流程的第一个问题:不符合大脑顺序、线性的思维方式。
async/await
上面我们介绍了Promise和Generator,把这两者结合起来,就是Async/Await。
Generator的缺点是还需要我们手动控制next()执行,使用Async/Await的时候,只要await后面跟着一个Promise,它会自动等到Promise决议以后的返回值,resolve(...)或者reject(...)都可以。
什么是Async/Await?
Async - 定义异步函数(async function someName(){...})
- 自动把函数转换为 Promise
- 当调用异步函数时,函数返回值会被 resolve 处理
- 异步函数内部可以使用
await
Await - 暂停异步函数的执行 (var result = await someAsyncCall();)
- 当使用在 Promise 前面时,
await等待 Promise 完成,并返回 Promise 的结果 await只能和 Promise 一起使用,不能和 callback 一起使用await只能用在async函数中
Async/Await 底层依然使用了 Promise。
多个异步函数同时执行时,需要借助 Promise.all
async function getABC() {
let A = await getValueA(); // getValueA 花费 2 秒
let B = await getValueB(); // getValueA 花费 4 秒
let C = await getValueC(); // getValueA 花费 3 秒
return A*B*C;
}
每次遇到 await 关键字时,Promise 都会停下在,一直到运行结束,所以总共花费是 2+4+3 = 9 秒。await 把异步变成了同步。
Async/Await是Generator和Promise的组合,完全解决了基于回调的异步流程存在的两个问题,可能是现在最好的JavaScript处理异步的方式了。
总结
本文通过四个阶段来讲述JavaScript异步编程的发展历程:
-
第一个阶段 - 回调函数,但会导致两个问题:
- 缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
- 缺乏可信任性: 控制反转导致的一系列信任问题
-
第二个阶段 - Promise,Promise是基于PromiseA+规范的实现,它很好的解决了控制反转导致的信任问题,将代码执行的主动权重新拿了回来。
-
第三个阶段 - 生成器函数Generator,使用Generator,可以让我们用同步的方式来书写代码,解决了顺序性的问题,但是需要手动去控制next(...),将回调成功返回的数据送回JavaScript主流程中。
-
第四个阶段 - Async/Await,Async/Await结合了Promise和Generator,在await后面跟一个Promise,它会自动等待Promise的决议值,解决了Generator需要手动控制next(...)执行的问题,真正实现了用同步的方式书写异步代码。
参考: