Open youngwind opened 7 years ago
ES6从本质上改变了在哪里管理事件循环,这意味着在技术上将其纳入了JavaScript引擎的势力范围,而不再是由宿主来管理
这里没有说清楚,应该还描述一下ES6(比如Promise)是在宿主(比如浏览器)的事件、定时器任务队列的task完成后执行
这里没有说清楚,应该还描述一下ES6(比如Promise)是在宿主(比如浏览器)的事件、定时器任务队列的task完成后执行
@riskers 多谢指出!确实,这个部分我当时并没有想清楚,所以没详细展开。后来我又去研究了一番,主要参考这份资料。个人认可以下Macro-task和Micro-task的观点。
在挂起任务时,JS 引擎会将所有任务按照类别分到这两个队列中,首先在 macrotask 的队列(这个队列也被叫做 task queue)中取出第一个任务,执行完毕后取出 microtask 队列中的所有任务顺序执行;之后再取 macrotask 任务,周而复始,直至两个队列的任务都取完。 两个类别的具体分类如下: macro-task: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering micro-task: process.nextTick, Promises(这里指浏览器实现的原生 Promise), Object.observe, MutationObserver
@youngwind 是的,就是这个意思 👍
这里没有说清楚,应该还描述一下ES6(比如Promise)是在宿主(比如浏览器)的事件、定时器任务队列的task完成后执行
Promise 应该是先执行,不是后执行。
@imasshole,是后执行的。
setTimeout(function(){
console.log(1)
},0)
new Promise(function(resolve, reject){
console.log(2)
resolve('resolve')
}).then(function(){
console.log(3)
})
比如这段代码,依次输出 2 3 1。代码执行顺序是:
总结的很好,可是我还是觉得中卷这部分翻译的很糟糕。
@riskers 是的,我是说第二步的 micrco-task 是在第三步的 macro-task 之前执行, 你说的是第一步的task 是在第二步的mirco-task前执行,这是没错的。
不过第一步的貌似不能叫 macro-task ,就是正常的 task。
@waitinghope 我也觉得中卷读起来没那么流畅,可能是随着内容的渐渐深入,翻译的难度比上卷要大了。
@imasshole task 还是 macro-task ,我看到的资料叫法都不一样。
为什么
关于回调、异步与生成器,网上的文章已经很多了,而且很久以前我也写过类似的一篇 #49 。 为什么现在还要写这个呢? 原因:最近我在看《你不知道的JavaScript中卷》,发现书中一些少有且独特的观点,是我以往所不知道的,也是已有文章很少提到的,所以便有了此文。 (注:本文观点绝大多数来自于《你不知道的JavaScript中卷》第二部分的第1、2、4章,经二次演绎而成。强烈推荐阅读《你不知道的JavaScript》系列书,绝对的不容错过。)
阅读前请确保熟悉以下概念:
回调不仅仅是代码缩进
长久以来,JS实现异步只能用回调这一种方式。随着应用的渐趋复杂,过度嵌套回调的弊端渐渐显现,最为人们所诟病的就是层层嵌套导致的代码缩进,俗称回调金字塔。 我一开始对回调弊端的认识也仅限于此。然而,我发现我错了。因为代码的缩进问题可以通过工厂模式抽象来缓解,这并非很严重的问题。那么,过度的回调嵌套还有什么更严重的问题吗?
顺序的大脑
请观察下面的伪代码。
无论多么熟悉JS异步的人,要完全搞懂这段代码实际的运行顺序,恐怕也得思考一番。 为什么会这样? → 因为人的大脑是顺序的,天生适合顺序的思考,难以理解(不是不能理解)非顺序的东西。 无论是在书写还是在阅读这段代码的时候,我们的大脑都会下意识地以为这段代码的执行逻辑是这样的
doA→doB→doC→doD→doE→doF
,然而实际运行逻辑很可能(假设)是这样的doA→doF→doB→doC→doE→doD
。 这是回调嵌套另一个严重的问题。还有其他问题吗?下一步该做什么
让我们来进行一个思想实验:两种游戏。
我个人更喜欢玩后者,也就是灵活的游戏。因为它有两个特点:
对应到代码当中,我们便能发现回调的另一个严重问题:硬编码。 前后的操作被回调强制硬编码绑定到一起了。在调用函数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有两层含义:让位与产出。
如果你能理解下面的例子,那么算是对生成器基本入门了。
异步的演化
最后,我写了一个实际的列子,分别采取回调、promise、generator与co、async这四种方法,演示了JS实现异步的演化进程。 本例所要完成的功能是:按顺序执行三次setTimeout,并且在指定的时间之后打印出当前的时间。 (注:下面的例子均可直接运行。推荐一个插件,通过它可以直接在chrome中运行ES6的代码,再也不用自己去折腾babel那些东西了,非常的方便。)
回调
promise
generator与co
async
====EOF====