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

[转: 原文地址, https://segmentfault.com/a/1190000000586666]

在微博上看到有人分享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是如何搞定它的。


上一篇:Linux分页机制


下一篇:(原+转)ubuntu中删除文件夹