[译] 深入理解 Promise 五部曲:1. 异步问题

30

原文地址:http://blog.getify.com/promis...

在微博上看到有人分享LabJS作者写的关于Promise的博客,看了下觉得写得很好,分五个部分讲解了Promise的来龙去脉。从这篇文章开始,我会陆续把五篇博客翻译出来跟大家分享,在大牛的带领下真正理解Promise。卖个关子,作者看待Promise的角度跟我一直以来看到的讲解Promise的角度完全不一样,不只是定留在解决回调金字塔上,至少我没想到Promise竟然有这么重要的意义。先上第一篇。

在这篇文章中,我会解释我们为什么需要使用一个更好的方式(比如Promise)来进行异步流程的编写。

异步

你肯定听说过Javascript中的异步编程,但是它到底是什么呢?

比如当你发生一个Ajax请求,你通常会提供一个回调函数,这个回调函数会在请求返回的时候被调用。但是你是否思考过你的回调函数在其他代码也需要运行的时候是如何被调用的呢?如果两个回调函数同时都要运行会怎样呢?JS引擎会如何处理这个问题呢?

为了理解异步到底是什么,你首先需要理解一个问题:JS引擎是单线程的。这意味着在任何环境中,只有一段JS代码会被执行。但是什么叫一段JS代码呢?总的来说,每个函数是一个不可分割的片段或者代码块。当JS引擎开始执行一个函数(比如回调函数)时,它就会把这个函数执行完,也就是说只有执行完这段代码才会继续执行后面的代码。

换句话说,JS引擎就像一个主题公园中的游乐项目,这个项目每次只能一个人玩儿,人们会排成一个长长的队。大家一个个上去玩儿,下来一个然后再上去一个。如果你要玩儿这个项目你只能在队尾排队等待。幸运的是,每个人都很快就下来了,所以这个队伍移动得很快。

上面说的队伍在技术上被叫做事件轮询。它尽可能快的进行轮询,如果事件队列中有代码需要执行,它会让JS引擎执行这段代码,然后移到下一个需要执行的代码,或者等待新的代码进来。

并发

如果程序在一个时间只有一个任务在执行,这样明显是低效而且有限制性的。如果你点击一个按钮提交一个表单,然后你的鼠标就会被冻结并且你不能滚动页面,这个情况会持续几秒直到请求返回,这样肯定会带来很差的用户体验。

这就是为什么真实的程序会有很多任务在运行而不是就只有一个任务,但是JS引擎是怎么在单线程的环境下实现的呢?

你应该想到每个代码块运行只要很短的时间,通常不到1毫秒。你一眨眼的时间,JS引擎会执行上千百个这样的代码块。但是并不是所有的代码块都是为了执行同一个任务。比如,当你点击提交按钮之后,你也可以点击导航或者滚动页面等等。每个任务都会被分为很多个原子操作,执行这些原子操作会非常快。

比如:

Task A

  • step1

  • step2

  • step3

  • step4

Task B

  • step1

  • step2

JS引擎肯定不能在执行A:1步骤的同时执行B:1。但是Task B不需要等到Task A执行完后再执行,因为引擎可以在每个独立的原子操作之间快速的切换,可能是按下面的顺序执行的:

  • A:1

  • B:1

  • A:2

  • B:2(Task B完成)

  • A:3

  • A:4(Task A完成)

所以,事实上Task A和Task B是可以"同时"运行的,通过穿插地执行它们的每个原子操作,这叫做并发,换句话说,Task A和Task B是并发的。

我们很容易就会把并发和并行弄混。在真正并行的系统中,你会有多个线程,可能一个线程执行Task A同时另一个线程执行Task B。这也意味着,A:1的运行不会阻塞B:1的运行。这就好像有主题公园中有两个分开的游乐项目,会有两队人在排队,它们互相不影响。

JS事件轮询是一个简单的并发模型。它只允许把每个事件添加到事件队列的队尾,而这个队列是先进先出的。当条件允许时,回调函数就会被运行。

同步情况下的异步

在JS中编写异步代码一个巧妙但是烦恼的问题是JS引擎实际执行代码的方式跟我们看上去不大一样。例如:

makeAjaxRequest(url,function(response){
    alert("Response:" + response) ;
}) ;

你会怎么描述这段代码的流程呢?大多数开发者大概会这么说:

  1. 发送Ajax请求

  2. 等到请求完成的时候,弹出提示框

但是这跟JS引擎实际的执行情况相比还不够准确。这个问题主要是因为我们大脑习惯同步的方式。在上面这个描述中,我们使用“等到。。。的时候”来解释,这就也是说我们会阻塞等待Ajax请求,然后继续执行后面的程序。

JS在步骤1和步骤2之间不会阻塞。一个更准确的描述上面这段代码的方式是:

  1. 发送Ajax请求

  2. 注册回调函数

  3. 继续向下执行

  4. 在未来某个时间点,惊呼“Oh,我刚才得到一个返回!”。现在,返回去执行注册的那个回调函数。

这两个解释的区别似乎没什么大不了的,但是我们跳过第三步的思考方式是一个大问题。

源代码是给开发者的而不是计算机的。计算机只关心1和0.有无限种程序能产生一样的1和0序列。我们编写源代码为了使得我们能够以一种有含义并且准确的方式理解代码是干嘛的。由于我们的大脑很难处理异步,所以我们需要找出一种更加同步的方式来编写异步代码,隐藏具体的异步实现。

例如,如果下面这段代码能像我们需要的那样运行并且不会阻塞,那么它是不是更好理解了呢?

response = makeAjaxRequest(url) ;
alert("Response:" + response) ;

如果我们可以像这样编码,那么我们就可以隐藏或者抽象makeAjaxRequest()的异步本质,不需要担心具体细节。
换句话说,我们能使得异步代码只出现在具体的实现上,把这些烦人的东西埋在属于它的地方。

总结

我们还没有解决问题。但是至少我们知道了问题是什么:用异步的方式来表达异步的代码是艰难的,甚至很难用我们的大脑来理解。

我们需要的只是一种以同步的代码来尽可能隐藏具体的异步实现的方式,这样我们的大脑更好理解。我们的目标是以同步的方式来编码而不需要关系它的实现的同步还是异步。

在第二部分:转换的问题中,我会着手处理“回调地狱”来解释这些问题,我们也将看到Promises是如何搞定它的。

深入理解Promise五部曲--1.异步问题
深入理解Promise五部曲--2.转换问题
深入理解Promise五部曲--3.可靠性问题
深入理解Promise五部曲--4.扩展性问题
深入理解Promise五部曲--5.乐高问题

最后,安利下我的个人博客,欢迎访问:http://bin-playground.top

你可能感兴趣的

yuezheng · 2015年11月23日

翻译的很好,感谢楼主。

+1 回复

苏生不惑 · 2014年06月29日

太赞了,期待后文

回复

pod2g · 2015年07月31日

作者说了,一个代码段(比如函数)是不可分割的,必须要执行完才能运行,但是在并发这个章节又说,可以把代码段分割成原子任务,穿插执行,这到底是怎么一回事?

回复

pod2g · 2015年07月31日

什么玩意儿啊,本来有自己的理解,现在看看全打乱了,作者说的都一笔带过,没有对能让人产生歧义的地方进行说明,对一些能让人醍醐灌顶的细节避而不谈,我只能说这种文章还不如不写

回复

pod2g · 2015年07月31日

如果楼主知道是怎么回事,请不吝赐教

回复

lakb248 作者 · 2015年07月31日

原文说函数是一个不可分割的代码段,并没有说代码段不可分割,一个代码段可能由多个函数组成。这里所说的并发其实就是JS事件轮询的问题,如果对JS的事件轮询有什么问题的话,建议看这个https://www.youtube.com/watch?v=8aGhZQkoFbQ,需要翻墙

回复

lakb248 作者 · 2015年07月31日

这篇文章主要是为了介绍JS的异步模型是怎么回事,它主要通过事件轮询来实现,然后说了异步的编程方式和人脑理解处理代码的方式有差异,这也是promise的一个作用,通过promise写出来的异步代码对于人脑来说更容易理解

回复

流浪大法师 · 2015年12月24日

function foo(){console.log("hello world");} foo();console.log("我是在函数foo执行后才执行的");在宏观上看语句执行是按顺序执行,从上往下。但是从微观上来看,js就一个线程(一个线程就就有一个cpu),但是存在一个代码队列控制宏观代码语句的执行,一般来说宏观的语句怎么写,微观的代码队列存放的元素顺序也一般是按照宏观的顺序加入的,所以说加入代码队列的时间是很关键的,在微观上的调度感觉和时间片轮转类似,所以存在原子任务交替的情况

回复

载入中...