读完《你不知道的javaScript》系列的中卷中类型和语法部分,发现对于基础知识的理解更加透彻了。
之前计划是将中卷的两部分内容全部记录完毕,最后发现内容实在太多。
所以不得不将中卷分成两部分记录,现在我们开始异步和性能部分。
异步和性能
异步:现在和将来
分块的程序
JavaScript程序最常见的块单位是函数。
任何时候,只要把一段代码包装成一个函数,并指定它在响应某个事件(定时器、鼠标点击、Ajax响应等)时执行,你就是在代码中创建了一个将来执行的块,也由此在这个程序中引入了异步机制。
异步控制台
如果在调试的过程中遇到对象在console.log(..)
语句之后被修改,可你却看到了意料之外的结果,要意识到这可能是这种I/O的异步化造成的。
事件循环
首先通过一段极度简化的伪代码,来了解一下事件循环这个概念。
1 | // eventLoop 是一个用作队列的数组 |
并行线程
异步是关于现在和将来的时间间隙,而并行是关于能够同时发生的事情。
完整运行
由于JavaScript的单线程特性,foo()(以及bar())中的代码具有原子性。
也就是说,一旦foo()开始运行,它的所有代码都会在bar()中的任意代码运行之前完成,或者相反。
这称为完整运行(run-to-completion)特性。
1 | let a = 1 |
并发
单线程事件循环是并发的一种形式
非交互
两个或多个“进程”在同一个程序内并发地交替运行它们的步骤/事件时,如果这些任务彼此不相关,就不一定需要交互。
如果进程间没有相互影响的话,不确定性是完全可以接受的。
交互
1 | let a,b |
包裹baz()调用的条件判断if (a && b)传统上称为门(gate),我们虽然不能确定a和b到达的顺序,但是会等到它们两个都准备好再进一步打开门(调用baz())
1 | let a |
条件判断if (! a)使得只有foo()和bar()中的第一个可以通过,第二个(实际上是任何后续的)调用会被忽略。也就是说,第二名没有任何意义!
协作
如果有像1000万条记录的话,就可能需要运行相当一段时间了(在高性能笔记本上需要几秒钟,在移动设备上需要更长时间,等等)。
这样的“进程”运行时,页面上的其他代码都不能运行,包括不能有其他的response(..)调用或UI刷新,甚至是像滚动、输入、按钮点击这样的用户事件。这是相当痛苦的。
所以,要创建一个协作性更强更友好且不会霸占事件循环队列的并发系统,你可以异步地批处理这些结果。每次处理之后返回事件循环,让其他等待事件有机会运行。
1 | var res = [] |
任务
任务队列就是,它是挂在事件循环队列的每个tick之后的一个队列。
在事件循环的每个tick中,可能出现的异步动作不会导致一个完整的新事件添加到事件循环队列中,而会在当前tick的任务队列末尾添加一个项目(一个任务)。
回调
continuation
“执行A,然后设定一个延时等待1000毫秒,到时后马上执行C”
“执行A,设定延时1000毫秒,然后执行B,然后定时到时后执行C”
尽管第二个版本更精确一些,但是在匹配大脑对这段代码的理解和代码对于JavaScript引擎的意义方面,两个版本对这段代码的解释都有不足。
这种不匹配既微妙又显著,也正是理解回调作为异步表达和管理方式的缺陷的关键所在。
顺序的大脑
执行与计划
因为这并不是我们大脑进行计划的运作方式,所以精确编写和追踪使用回调的异步JavaScript代码如此之难
嵌套回调与链式回调
嵌套和缩进基本上只是转移注意力的枝节而已。
回调方式最主要的缺陷:对于它们在代码中表达异步的方式,我们的大脑需要努力才能同步得上。
信任问题
顺序的人脑计划和回调驱动的异步JavaScript代码之间的不匹配只是回调问题的一部分。
还有一些更深入的问题需要考虑。
实际上,这是回调驱动设计最严重(也是最微妙)的问题。
它以这样一个思路为中心:有时候ajax(..)(也就是你交付回调continuation的第三方)不是你编写的代码,也不在你的直接控制下。
多数情况下,它是某个第三方提供的工具。我们把这称为控制反转(inversion of control),也就是把自己程序一部分的执行控制交给某个第三方。
在你的代码和第三方工具(一组你希望有人维护的东西)之间有一份并没有明确表达的契约。
不只是别人的代码
回调不可靠,且回调并没有为我们提供任何东西来支持回调结果验证这一点。
我们不得不自己构建全部的机制,而且通常为每个异步回调重复这样的工作最后都成了负担。
Promise
什么是Promise
未来值
1 | function add(xPromise, yPromise){ |
通过Promise,调用then(..)实际上可以接受两个函数,第一个用于完成情况(如前所示),第二个用于拒绝情况:
1 | add(fetchX(), fetchY()).then( |
一旦Promise决议,它就永远保持在这个状态。此时它就成为了不变值(immutable value),可以根据需求多次查看。
Promise是一种封装和组合未来值的易于复用的机制。
完成事件
1 | function foo(x){ |
相对于面向回调的代码,这里的反转是显而易见的,而且这也是有意为之。
这里没有把回调传给foo(..),而是返回一个名为evt的事件注册对象,由它来接受回调。
具有then方法的鸭子类型
识别Promise(或者行为类似于Promise的东西)就是定义某种称为thenable的东西,将其定义为任何具有then(..)方法的对象和函数。我们认为,任何这样的值就是Promise一致的thenable。
根据一个值的形态(具有哪些属性)对这个值的类型做出一些假定。这种类型检查(type check)一般用术语鸭子类型(duck typing)来表示——“如果它看起来像只鸭子,叫起来像只鸭子,那它一定就是只鸭子”(参见本书的“类型和语法”部分)。
于是,对thenable值的鸭子类型检测就大致类似于:
1 | if( |
Promise信任问题
调用过早
即使是立即完成的Promise(类似于new Promise(function(resolve){ resolve(42); }))也无法被同步观察到。
也就是说,对一个Promise调用then(..)的时候,即使这个Promise已经决议,提供给then(..)的回调也总会被异步调用
调用过晚
一个Promise决议后,这个Promise上所有的通过then(..)注册的回调都会在下一个异步时机点上依次被立即调用。
这些回调中的任意一个都无法影响或延误对其他回调的调用
回调未调用
这个问题很常见,Promise可以通过几种途径解决。
首先,没有任何东西(甚至JavaScript错误)能阻止Promise向你通知它的决议(如果它决议了的话)。如果你对一个Promise注册了一个完成回调和一个拒绝回调,那么Promise在决议时总是会调用其中的一个。
当然,如果你的回调函数本身包含JavaScript错误,那可能就会看不到你期望的结果,但实际上回调还是被调用了。后面我们会介绍如何在回调出错时得到通知,因为就连这些错误也不会被吞掉。
但是,如果Promise本身永远不被决议呢?即使这样,Promise也提供了解决方案,其使用了一种称为竞态的高级抽象机制:
1 | function timeoutPromise(delay){ |
调用次数过少或过多
回调被调用的正确次数应该是1。“过少”的情况就是调用0次,和前面解释过的“未被”调用是同一种情况。
“过多”的情况很容易解释。Promise将只会接受第一次决议,并默默地忽略任何后续调用。
未能传递参数/环境值
如果你没有用任何值显式决议,那么这个值就是undefined,这是JavaScript常见的处理方式。
但不管这个值是什么,无论当前或未来,它都会被传给所有注册的(且适当的完成或拒绝)回调
吞掉错误或异常
如果拒绝一个Promise并给出一个理由(也就是一个出错消息),这个值就会被传给拒绝回调。
是可信任的Promise吗
如果向Promise.resolve(..)传递一个非Promise、非thenable的立即值,就会得到一个用这个值填充的promise。下面这种情况下,promise p1和promise p2的行为是完全一样的:
1 | var p1 = new Promise(function(resolve, reject){ |
而如果向Promise.resolve(..)传递一个真正的Promise,就只会返回同一个promise:
1 | var p1 = Promise.resolve(42) |
更重要的是,如果向Promise.resolve(..)传递了一个非Promise的thenable值,前者就会试图展开这个值,而且展开过程会持续到提取出一个具体的非类Promise的最终值。
Promise.resolve(..)可以接受任何thenable,将其解封为它的非thenable值。从Promise. resolve(..)得到的是一个真正的Promise,是一个可以信任的值。
如果你传入的已经是真正的Promise,那么你得到的就是它本身,所以通过Promise.resolve(..)过滤来获得可信任性完全没有坏处。
建立信任
Promise这种模式通过可信任的语义把回调作为参数传递,使得这种行为更可靠更合理。
通过把回调的控制反转反转回来,我们把控制权放在了一个可信任的系统(Promise)中,这种系统的设计目的就是为了使异步编码更清晰。
链式流
简单总结一下使链式流程控制可行的Promise固有特性。
• 调用Promise的then(..)会自动创建一个新的Promise从调用返回。
• 在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的)Promise就相应地决议。
• 如果完成或拒绝处理函数返回一个Promise,它将会被展开,这样一来,不管它的决议值是什么,都会成为当前then(..)返回的链接Promise的决议值。
术语:决议(resolve)、完成(fulfill)以及拒绝(reject)
错误处理
1 | var p = Promise.resolve(42) |
如果msg.toLowerCase()合法地抛出一个错误(事实确实如此!),为什么我们的错误处理函数没有得到通知呢?正如前面解释过的,这是因为那个错误处理函数是为promise p准备的,而这个promise已经用值42填充了。
promise p是不可变的,所以唯一可以被通知这个错误的promise是从p.then(..)返回的那一个,但我们在此例中没有捕捉
一些开发者表示,Promise链的一个最佳实践就是最后总以一个catch(..)结束,比如:
1 | var p = Promise.resolve(42) |
进入p的错误以及p之后进入其决议(就像msg.toLowerCase())的错误都会传递到最后的handleErrors(..)。
如果handleErrors(..)本身内部也有错误怎么办呢?谁来捕捉它?
处理未捕获的情况
1 | var p = Promise.resolve(42) |
Promise模式
Promise.all([ .. ])
Promise.race([ .. ])
all([ .. ])和race([ .. ])的变体
• none([ .. ])
这个模式类似于all([ .. ]),不过完成和拒绝的情况互换了。所有的Promise都要被拒绝,即拒绝转化为完成值,反之亦然。
• any([ .. ])
这个模式与all([ .. ])类似,但是会忽略拒绝,所以只需要完成一个而不是全部。
• first([ .. ])
这个模式类似于与any([ .. ])的竞争,即只要第一个Promise完成,它就会忽略后续的任何拒绝和完成。
• last([ .. ])
这个模式类似于first([ .. ]),但却是只有最后一个完成胜出。
并发迭代
1 | var p1 = Promise.resolve(21) |
Promise API 概述
new Promise(..)构造器
1 | var p = new Promise(function(resolve, reject){ |
reject(..)就是拒绝这个promise;但resolve(..)既可能完成promise,也可能拒绝,要根据传入参数而定。
如果传给resolve(..)的是一个非Promise、非thenable的立即值,这个promise就会用这个值完成。
但是,如果传给resolve(..)的是一个真正的Promise或thenable值,这个值就会被递归展开,并且(要构造的)promise将取用其最终决议值或状态。
Promise.resolve(..)和Promise.reject(..)
创建一个已被拒绝的Promise的快捷方式是使用Promise.reject(..),所以以下两个promise是等价的:
1 | var p1 = new Promise(function(resolve, reject) { |
Promise.resolve(..)常用于创建一个已完成的Promise,使用方式与Promise.reject(..)类似。
但是,Promise.resolve(..)也会展开thenable值。
在这种情况下,返回的Promise采用传入的这个thenable的最终决议值,可能是完成,也可能是拒绝:
1 | var fulfilledTh = { |
还要记住,如果传入的是真正的Promise, Promise.resolve(..)什么都不会做,只会直接把这个值返回。
所以,对你不了解属性的值调用Promise.resolve(..),如果它恰好是一个真正的Promise,是不会有额外的开销的。
then(..)和catch(..)
then(..) 接受一个或两个参数:第一个用于完成回调,第二个用于拒绝回调。
如果两者中的任何一个被省略或者作为非函数值传入的话,就会替换为相应的默认回调。
默认完成回调只是把消息传递下去,而默认拒绝回调则只是重新抛出(传播)其接收到的出错原因。
catch(..) 只接受一个拒绝回调作为参数,并自动替换默认完成回调。换句话说,它等价于then(null, ..):
Promise.all([ .. ])和Promise.race([ .. ])
1 | var p1 = Promise.resolve(42) |
生成器
打破完整运行
输入和输出
1 | function *foo(x, y){ |
1.迭代消息传递
1 | function *foo(x) { |
2.两个问题的故事
消息是双向传递的——yield..作为一个表达式可以发出消息响应next(..)调用,next(..)也可以向暂停的yield表达式发送值
多个迭代器
1 | function *foo(){ |
- 本文作者: Jambo
- 版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!