你不知道的JavaScript(中卷)二

六、异步:现在与将来

程序现在运行的部分和将来运行的部分之间的关系就是异步编程的核心

A.分块的程序

1.最常见的块单位是函数。从现在到将来的“等待”,最简单的方法(但绝不是唯一的,甚至也不是最好的)是使用一个通常称为回调函数的函数

2.任何时候,只要把一段代码包装成一个函数,并指定它在响应某个事件(定时器、鼠标点击、Ajax响应等)时执行,你就是在代码中创建了一个将来执行的块,也由此在这个程序中引入了异步机制

3.在某些条件下,某些浏览器的console.log()并不会把传入的内容立即输出。原因是,在许多程序(不只是JS)中,I/O是非常低带的阻塞部分。所以(从页面和UI角度来说)浏览器在后台异步处理控制台I/O能够提高性能。

B.事件循环

1.所有环境都有一个共同“点”(thread,也指线程),即它们都提供了一种机制来处理程序 中多个块的执行,且执行每块时调用JS引擎,这种机制被称为事件循环。换句话说,JS引擎本身并没有时间的概念,只是一个按需执行JS做生意代码片段的环境。“事件”(JS代码执行)调度总是由包含它的环境进行。

2.程序通常被分成了很多小块,在事件循环队列中一个接一个地执行。严格地说,和你的程序不直接相关的其他事件也可能会插入到队列中

3.setTimeout()并没有把回调函数拍在事件循环队列中,但是设置了一个定时器,当到时后,环境会把你的回调函数放到事件循环中去,所以setTimeout()的精度可能不高

C.并行线程

1.异步是关于现在和将来的时间间隙,而并行是关于能够同时发生的事情。

2.并行计算最觉的工具就是进程和线程。进程和线程独立运行,并可能同时运行:在不同的处理器,甚至不同的计算机上,但多个线程能够共享单个进程的内存

3.事件循环把自身的工作分成一个个任务并顺序执行,不允许对共享内存的并行访问和修改。通过分立线程中彼此合作的事件循环,并行和顺序执行可以共存

4.JS从不跨线程共享数据

5.由于JS的单线程特性,函数中的代码具有原子性,一个函数开始运行,它的所有代码都会在另一个函数的做生意代码运行前完成,或者相反,这称为完事运行(run-to-completion)特性

6.同一段代码有两个可能输出意味着存在不确定性,这种不确定性是在函数(事件)顺序级别上,而不是多线程情况下的语句顺序级别,这种称为竞态条件(race condition)

D.并发

1.两个“进程”同时执行就出现了并发,不管组成它们的单个运算是否并行执行(在独立的处理器或处理器核心上同时运行)。可以把并发看作“进程”级(或者任务级)的并行,与运算级的并行(不同处理器上的线程)相对

2.单线程事件循环是并发的一种形式

3.非交互:两个或多个“进程”在同一个程序内并发地交替运行它们的步骤/事件时,如果这些任务彼此不相关,就不一定需要交线。如果进程间没有相互影响的话,不确定性是完全可以接受的

4.交互

• 针对修改调用相同变量可以协调交互顺序来处理竞态条件

• 针对调用相同方法可以设置门(gate),当所变量或条件都准备好后再打开门调用方法

• 当修改同一个变量时还可以使用门闩,“只有第一个取胜”,判断变量是否已被赋值这种

5.协作:目标是取到一个长期运行的“进程”,并将其分割成多个步骤或多批任务,使得其他并发“进程”有机会将自己的运算插入到事件循环队列中交替运行,例如使用setTimeout()分割耗时操作

E.任务

1.任务队列(job queue):ES6新增,它是挂在事件特殊队列每个tick之后的一个队列。在事件循环的每个tick中,可能出现 的异步动作不会导致一个完事的新带伤添加到事件循环队列中,而会在当前 tick的任务队列末尾添加一个项目(任务)

2.与setTimeout(..0)hack的思路类似,但是其实现方式的定义更加良好,对顺序的保证性更强:尽可能早的将来

F.语句顺序

1.代码中语句的顺序和JS引擎执行语句的顺序并不一定要一致

七、回调

A. continuation

1.回调函数包裹或者说封装了程序的延续(continuation)

B.顺序的大脑

1.代码(通过回调)表达异步的方式并不能很好地映射到同步的大脑计划行为

2.三个函数嵌套在一起构成的链,其中每个函数代表异步序列(任务,“进程”)中的一个步骤。这种代码被称为回调地狱(callback hell)或毁灭多字塔(pyramid of domm)

C.信任问题

1.控制反转(inversion of control),也就是把自己程序一部分的执行控制交给某个第三方。在你的代码和第三方工具(一组你希望有人维护的东西)之间有一份并没有明确表达的契约

八、Promise

A.什么是Promise

1.从外部看,Promise封装了依赖于时间的状态——等待底层值的完成或拒绝,所以Promise本身是与时间无关的。因此,Promise可以按照可预测的方式组成(组合),而不用关心时序或底层结构

2.一旦Promise决议,它就永远保持在这个状态。此时它就成为了不变值(immutable value),可以根据需求多次查看

3.Promise决议后就是外部不可变的值,我们可以安全地把这个值传递给第三方,并砍它不会被有意无意地修改。特别是对于多方查看同一个Promise决议的情况,尤其如此。一方不可能影响到另一方对Promise决议的观察结果。

4.对回调模式的反转实际上是对反转的反转,或者称为反控制反转

5.new Promise(function(){});模式通常称为revealing constructor。传入的函数会立即执行(不会像then()中的回调一样异步延迟),它有两个参数,一个标识完成,一个标识拒绝

B.具有then方法的鸭子类型

1.识别Promise(或者行为类似于Promise的东西)就是定义某种称为thenable的东西,将其定义为任何具有then()方法的对象和函数。

2.根据一个值的形态(具有哪些属性)对这个值的类型做出一些假定。这种类型检查(type check)一般用术语鸭子类型(duck typing)来表示——“如果它看起来像只鸭子,叫起来像只鸭子,那它一定就是只鸭子”

3.对thenable的鸭子类型检测:if(p!=null&&(typeof p ===“object” || typeof p === “function”)&&typeof p.then ===“function”)

4.注意这种检测可能会产生误判

C.Promise信任问题

1.调用过早:Promise即使是立即完成的Promise也无法被同步观察到,也就是说,对一个Promise调用then()的时候,即使这个Promise已经决议,提供给then()的回调也总会被异步调用

2.调用过晚:一个promise决议后,这个Promise上所有的通过then()注册的回调都会在下一个异步时机点依次被立即调用。这些回调中的做任意一个都无法影响或延误对其他回调的调用

• Promise调度技巧:永远都不应该依赖于不同Promise间回调的顺序和调度。实际上,好的编码实践方案根本不会让多个回调的顺序有丝毫影响,可能的话就要避免

3.回调未调用:没有任何东西(甚至JS错误)能阻止Promise向你通知它的决议(如果它决议了的话)。如果你对一个Promise注册了一个完成回调和一个拒绝回调,那么Promise在决议时总是会调用其中的一个

• 如果Promise本身永远不被决议,Promise使用了一种称为竞态的高级抽象机制

4.调用次数过少或过多:Promise定义的方式使得它只能被决议一次。如果出于某种原因,Promise创建代码试图调用resolve()或reject()多次,或者试图两者都调用,那么这个Promise将只会接受第一次决议,并默默地忽略任何后续调用。

• 任何通过then()注册的(每个)回调只会被调用一次,如果把同一个回调注册了不止一次,那它被调用的次数就会和注册次数相同。

5.未能传递参数/环境值:如果你没有用任何值显式决议,那么这个值就是undefined,这是JS常见的处理方式。但不管这个值是什么,无论当前或未来,它都会传给所有注册的(且适当的完成或拒绝)回调

• 如果使用多个参数调用resovle()或者reject(),第一个参数之后的所有参数都会被默默忽略。

6.吞掉错误或异常:如果拒绝一个Promise并给出一个理由(也就是一个出错消息),这个值就会被传给拒绝回调

• 如果在Promise的创建过程中或在查看其决议结果过程中的任何时间点上出现了一个JS异常错误,比如一个TypeError或ReferenceError,那这个异常就会被捕捉,并且会使这个Promise被拒绝

• Promise甚至把JS异常也变成了异步行为,进而极大降低了竞态条件出现的可能

7.是可以信任的Promise吗:

• Promise并没有完全摆脱回调,它们只是改变了传递回调的位置

• 如果向Promise.resolve()传递一个非Promise,非thenable的立即值,就会得到一个用这个值填充的promise

• Promise.resolve()可以接受任何tenable,将其解封为它的非thenable值。从Promise.resolve()得到的是一个真正的Promise,是一个可以信任的值。

8.建立信任:Promise这种模式通过可信任的语义把回调作为参数传递,使得这种行为更可选更合理。通过把回调的控制反转反转回来,我们把控制权放在了一个可信任的系统(Promise)中,这种系统的设计目的就是为了使异步编码更清晰

D.链式流

1.Promise并不只是一个单步执行this-then-that操作的机制。当然,那是构成部件,但是我们可以把多个Promise连接到一起以表示一系列异步步骤,关键在于以下两个Promise固有行为特性:

• 每次你对Promise调用then(),它都会创建并返回一个新的Promise,我们可以将其链接起来

• 不管从then()调用的完成回调(第一个参数)返回的值是什么,它都会被自动设置为被链接Promise(第一点中的)的完成

• 如果你调用promise的then(),并且只传入一个完成处理函数,一个默认拒绝处理函数就会顶替上来

2.链式流程控制可行的Promise固有特性:

• 调用Promise的then()会自动创建一个新的Promise从调用返回

• 在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的)Promise就相应地决议

• 如果完成或拒绝处理函数返回一个Promise,它将会被展开,这样一来,不管它的决议值是什么,都会成为当前then()返回的链接 Promise的决议值

3.对链式流程控制最精确的看法是把它看作Promise组合到一起的一个附加益处,而不是主要目的。

4.术语:决议、完成以及拒绝

var p = new Promise( function(X,Y){

//X()用于完成

//Y()用于拒绝

} );

• 构造器建议选用:resolve()和rejected()

• .then()建议选用:fulfilled()和rejected()

E.错误处理

1.try……catch只能是同步的,无法用于异步代码模式

2.Promise没有采用流行的error-first回调设计风格,而是使用了分离回调(split-callback)风格。一个回调用于完成情况,一个回调用于拒绝情况

3.处理未捕获的情况:

• 一种常见的看法是:Promise应该添加一个done()函数,从本质上标识Promise链的结束。done()不会创建和返回Promise,所以传递给done()的回调显然不会报告一个并不存在的链接Promise的问题

• 浏览器有一个特有的功能:它们 可以跟踪并了解所有对象被丢弃以及被垃圾回收的时机。

4.成功的坑:

• 默认情况下,Promise在下一个任务或时间循环tick上(向开发者终端)报告所有拒绝,如果在这个时间点上该Promise上还没有注册错误处理函数

• 如果想要一个被拒绝的Promise在查看之前的某个家附近姐段内保持被拒绝状态,可以调用defer(),这个函数优先级高于该Promise的自动错误报告

F.Promise模式

1.Promise.all([..])

• 在异步序列中(Promise链),任意时刻都只能有一个异步任务正在执行——步骤2只能在步骤1之后,步骤3只能在步骤2之后

• 在经典的编程术语中,门(gate)是这样一种机制要等待两个或更多并行/并发的任务都完成才能继续。在Promise API中,这种模式为all([])

2.Promise.race([..])

• “只有第一个到达终点的才算胜利”,一旦任何一个Promise决议为完成,Promise.race([..])就会完成,一旦有任何一个Promise决议为拒绝,它就会拒绝

3.all()和race()的变体:none(),any(),first(),last()

G.Promise API概述

1.new Promise()构造器

2.Promise.resolve()和Promise.reject()

3.then()和catch():决议后立即全调用,then()两个参数,第一个用于完成回调,第二个用于拒绝回调;catch()只接受一个拒绝回调作为参数,并自动替换默认完成回调,等价于then(null,……);

4.Promise.all([])和Promise.race([])

H.Promise局限性

1.顺序错误处理

• 如果构建了一个没有错误处理函数的Promise链,链中任何地方的任何错误都会在链中一直传播下去,直到被查看。

• 可以注册一个catch,对于链中任何位置出现的任何错误,这个处理函数都会得到通知

2.单一值

• 一般的建议是构造一个值封装

• 可以使用Promise.all

• 利用ES6的解构

3.无法取消的Promise:一旦创建了一个Promise并为其注册了完成和/或拒绝处理函数,如果出现某种情况使得这个任务悬而未决的话,也没有办法从外部停止它的进程

https://github.com/zhangyue0503/html5js/blob/master/你不知道的JS中/7.html

九、生成器

A.打破完整运行

1.生成器是一类特殊的函数,可以一次或多次启动和停止,并不一定非得要完成。

2.迭代消息传递:消息是双向传递的——yield..作为一个表达式可以发出消息响应next(..)调用,next(..)也可以向暂停的yield表达式发送值

B.异步迭代生成器

1.可以在生成器内部有了看似完全同步的代码(调用Ajax时)

2.可以同步错误处理

https://github.com/zhangyue0503/html5js/blob/master/你不知道的JS中/7.html

十、程序性能

A.Web Worker

1.通常应用于:处理密集型数学计算、大数据集排序、数据处理(压缩、音频分析、图像处理等)、高流量网络通信

B.SIMD

1.单指令多数据(SIMD)是一种数据并行(data parallelism)方式,与Web Worker的任务并行(task parallelism)相对。

十一、性能测试与调优

A.性能测试

1.Benchmark.js

原文发布于微信公众号 - 硬核项目经理(fullstackpm)

原文发表时间:2017-04-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券