为什么
关于回调、异步与生成器,网上的文章已经很多了,而且很久以前我也写过类似的一篇 #49 。
为什么现在还要写这个呢?
原因:最近我在看《你不知道的JavaScript中卷》,发现书中一些少有且独特的观点,是我以往所不知道的,也是已有文章很少提到的,所以便有了此文。
(注:本文观点绝大多数来自于《你不知道的JavaScript中卷》第二部分的第1、2、4章,经二次演绎而成。强烈推荐阅读《你不知道的JavaScript》系列书,绝对的不容错过。)
阅读前请确保熟悉以下概念:
回调不仅仅是代码缩进
长久以来,JS实现异步只能用回调这一种方式。随着应用的渐趋复杂,过度嵌套回调的弊端渐渐显现,最为人们所诟病的就是层层嵌套导致的代码缩进,俗称回调金字塔。
我一开始对回调弊端的认识也仅限于此。然而,我发现我错了。因为代码的缩进问题可以通过工厂模式抽象来缓解,这并非很严重的问题。那么,过度的回调嵌套还有什么更严重的问题吗?
顺序的大脑
请观察下面的伪代码。
doA(function(){
doB();
doC(function() {
doD();
});
doE();
});
doF();
无论多么熟悉JS异步的人,要完全搞懂这段代码实际的运行顺序,恐怕也得思考一番。
为什么会这样? → 因为人的大脑是顺序的,天生适合顺序的思考,难以理解(不是不能理解)非顺序的东西。
无论是在书写还是在阅读这段代码的时候,我们的大脑都会下意识地以为这段代码的执行逻辑是这样的doA→doB→doC→doD→doE→doF
,然而实际运行逻辑很可能(假设)是这样的doA→doF→doB→doC→doE→doD
。
这是回调嵌套另一个严重的问题。还有其他问题吗?
下一步该做什么
让我们来进行一个思想实验:两种游戏。
- 第一种游戏,举办方提前跟你说:“这游戏总共有X关。第一关你应该做…..然后在….(地方)进入第二关。第二关你应该做….然后在….(地方)进入第三关。……"。我称之为呆板的游戏。
- 第二种游戏,举办方提前跟你说:”你只管从这个门口进去,等你快到下一关的时候,自然会有人出来给你提示。“我称之为灵活的游戏。
我个人更喜欢玩后者,也就是灵活的游戏。因为它有两个特点:
- 游戏很灵活。我根本不知道下一关会是什么,这充满未知的期待。
- 我不需要顾虑在哪儿进入下一关,因为到时候会有人给我提示。我只需要专心完成当前这一关就好了。
对应到代码当中,我们便能发现回调的另一个严重问题:硬编码。
前后的操作被回调强制硬编码绑定到一起了。在调用函数A的时候,你必须指定A结束之后该干什么,并且显式地传递进去。这样,其实你已经指定了所有的可能事件和路径,代码将变得僵硬且难以维护。同时,在阅读代码的时候,由于必须记住前后的关联操作,这也加重了大脑记忆的负担。
so,让我们总结一下回调的弊端:
- 代码缩进造成金字塔(小问题)
- 嵌套的书写方式与人类顺序大脑思考方式相违背(大问题)
- 前后操作被硬编码绑定在一起,代码变得僵硬,难以维护。(大问题)
为了解决过度回调导致的各种问题,无数卓绝的先驱创造了一个又一个的方法:promise、generator、co、async等等。在此,我不打算详细将讲这些,因为已经有很多文章讲得很好了,例如这个。下面我们继续来探索一下异步的本质。
谁的异步
以Ajax为例。我们都知道,在Ajax执行成功之后,指定的回调函数会被放入”任务队列“中。JS执行引擎在主线程空闲的时候便会轮询任务队列,执行其中的任务。
我们仔细想想,是不是漏了一个关键点:”我知道最终是JS引擎执行了这个回调函数。但是,到底是谁调度这个回调函数的?到底是谁在特定的时间点把这个回调函数放入任务队列中去?“
答案是宿主环境,在本例中也就是浏览器。是浏览器检测到Ajax已经成功返回,是浏览器主动将指定的回调函数放到”任务队列”中,JS引擎只不过是执行而已。
由此,我们澄清了一件(可能令人震惊)的事情: 在回调时代,尽管你已经能够编写异步代码了。但是,其实JS本身,从来没有真正內建直接的异步概念,直到ES6的出现。
事实就是如此。JS引擎本身所做的只不过是在不断轮询任务队列,然后执行其中的任务。JS引擎根本不能做到自己主动把任务放到任务队列中,任务的调度从来都是宿主完成的。举个形象的例子就是:“JS引擎就像是流水线上的工人,宿主就像是派活的老板。工人只知道不断地干活,不断地完成流水线上出现的任务,这些任务都是老板给工人指定的。工人从来没有(也不能)自己给自己派活,自己给自己的流水线上放任务。”
所以,这是JS引擎与宿主之争。ES6从本质上改变了在哪里管理事件循环,这意味着在技术上将其纳入了JavaScript引擎的势力范围,而不再是由宿主来管理。
===2016.11.4更新===
经@riskers提醒,补充此处内容。
promise本质上与setTimeout等不同,他们是两个不同的队列,有先后执行的顺序关系。
此处涉及概念颇为复杂,我并未完全理解。所以,关于这个的更多内容,请参考这个链接。
顾名思义
问题:generator不是用来处理异步的吗?那为什么要叫这个名字呢?
答案:generator是可以用来处理异步,但是它不仅仅是用来处理异步。或者说,本质上,generator是一个函数,它执行的结果是一个iterator迭代器,每一次调用迭代器的next方法,就会产生一个新的值。迭代器本身就是用来生成一系列值的,同时也广泛应用于扩展运算符...
、解构赋值和for...of
循环遍历等地方。
问题:为什么要用yield作为关键字?
答案:在英语中,yield有两层含义:让位与产出。
- 让位是什么意思?就是交出程序的执行权。在ES6执行,JS的函数都是一次性执行完成的。也就是说,函数一旦开始执行,就根本停不下来,直到全部执行完。生成器的引入打破了这一局面。每次调用next,执行到yield,函数便会交出执行权,让其他代码得以运行,它自己则等待下一次next指令的到来。
- 产出是什么意思?产出对应于迭代器。每次yield都会返回一个结果,传递到迭代器的res.value中去。同时,在调用next方法的时候也可以传递新的参数进去。就是这样一个不断输入、输出的过程,而且这个过程是可随意终端、重启的。
如果你能理解下面的例子,那么算是对生成器基本入门了。
function* foo(x){
let y = x * (yield);
return y;
}
let it = foo(6);
let res = it.next(); // res是什么
res = it.next(7); // res是什么
异步的演化
最后,我写了一个实际的列子,分别采取回调、promise、generator与co、async这四种方法,演示了JS实现异步的演化进程。
本例所要完成的功能是:按顺序执行三次setTimeout,并且在指定的时间之后打印出当前的时间。
(注:下面的例子均可直接运行。推荐一个插件,通过它可以直接在chrome中运行ES6的代码,再也不用自己去折腾babel那些东西了,非常的方便。)
回调
setTimeout(() ={
console.log(1, new Date());
setTimeout(() ={
console.log(2, new Date());
setTimeout(() ={
console.log(3, new Date());
},2000)
}, 1000);
},1000);
promise
function p(time){
return new Promise((resolve, reject) ={
setTimeout(() ={
resolve(new Date());
}, time)
});
}
p(1000).then((data) ={
console.log(1, data);
return p(1000);
}).then((data) ={
console.log(2, data);
return p(2000);
}).then((data) ={
console.log(3, data);
})
generator与co
function p(time) {
return new Promise((resolve, reject) ={
setTimeout(() ={
resolve(new Date());
}, time)
});
}
function* delay(){
let time1 = yield p(1000);
console.log(1, time1);
let time2 = yield p(1000);
console.log(2, time2)
let time3 = yield p(2000);
console.log(3, time3);
}
function co(gen){
let it = gen();
next();
function next(arg){
let ret = it.next(arg);
if(ret.done) return;
ret.value.then((data) ={
next(data)
})
}
}
co(delay);
async
function p(time) {
return new Promise((resolve, reject) ={
setTimeout(() ={
resolve(new Date());
}, time)
});
}
(async function(){
let time1 = await p(1000);
console.log(1, time1);
let time2 = await p(1000);
console.log(2, time2)
let time3 = await p(2000);
console.log(3, time3);
})()
看完两件小事
如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:
- 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
- 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程
本文来源于网络,其版权属原作者所有,如有侵权,请与小编联系,谢谢!
转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com
链接:https://www.javascriptc.com/1215.html
原文链接:https://github.com/youngwind/blog/issues/96