前言

 上周5在公司作了关于JS异步编程模型的技术分享,可能是内容太干的缘故吧,最后从大家的表情看出“这条粉肠到底在说啥?”的结果:(下面是PPT的讲义,具体的PPT和示例代码在https://github.com/fsjohnhuang/ppt/tree/master/apm_of_js上,有兴趣就上去看看吧!

重申主题

 《异步编程模型》这个名称确实不太直观,其实今天我想和大家分享的就是上面的代码是如何演进成下面的代码而已。

a(function(){
b(function(){
c(function(){
d()
})
})
})

TO

;(async function(){
await a()
await b()
await c()
await d()
}())

写在前面

 我们知道JavaScript是单线程运行的(撇开Web Worker),并且JavaScript线程执行时浏览器GUI渲染线程无法抢占CPU时间片,因此假如我们通过以下代码实现60秒后执行某项操作

const deadline = Date.now() + 60000
while(deadline > Date.now());
console.log('doSomething')

那么浏览器将假死60秒。正常情况下我们采用异步调用的方式来实现

const deadline = Date.now() + 60000
;(function _(){
if (deadline > Date.now()){
setTimeout(_, 100)
}
else{
console.log('doSomething')
}
}())

那到底上述两种方式有什么不同呢?

到这里我有个疑问,那就是到底什么才叫做异步呢?既然有异步,那必然有同步,那同步又是什么呢?谈起同步和异步,那必不可少地要提起阻塞和非阻塞,那它们又是什么意思呢?

谈到它们那必须联系到IO来说了
阻塞: 就是JS线程发起阻塞IO后,JS线程什么都不做就等则阻塞IO响应。
非阻塞: 就是JS线程发起非阻塞IO后,JS线程可以做其他事,然后通过轮询、信号量等方式通知JS线程获取IO响应结果。
也就是说阻塞和非阻塞描述的是发起IO和获取IO响应之间的时间里,JS线程是否可以继续处理其他任务。
而同步和异步则是描述另一个方面。

首先当我们发起网络IO请求时,应用程序会向OS发起系统调用,然后内核会调用驱动程序操作网卡,然后网卡得到的数据会先存放在内核空间中(应用程序是读取不了的),然后将数据从内核空间拷贝到用户空间。抽象一下就是,发起IO请求会涉及到用户空间和内核空间间的数据通信。

同步: 应用程序需要显式地将数据从内核空间拷贝到用户空间中,然后再使用数据。
异步: 将数据从内核空间拷贝到用户空间的操作由系统自动处理,然后通知应用程序直接使用数据即可。

对于如setTimeout等方法而已,本来就存在用户空间和内核空间的数据通信问题,因此异步更多是描述非阻塞这一特性。
那么异步调用的特点就是:
1. 非阻塞
2. 操作结果将于不明确的未来返回

从Callback Hell说起

举个栗子——番茄炒蛋
番茄切块(代号a)
鸡蛋打成蛋液(代号b)
蛋液煮成半熟(代号c)
将蛋切成块(代号d)
番茄与鸡蛋块一起炒熟(代号e)

假设个步骤都是同步IO时

->番茄切块->鸡蛋打成蛋液->蛋液煮成半熟->将蛋切成块->番茄与鸡蛋块一起炒熟

a()
b()
c()
d()
e()

假设个步骤都是异步IO时

 情况1——所有步骤均无状态依赖
->番茄切块
->鸡蛋打成蛋液
->蛋液煮成半熟
->将蛋切成块
->番茄与鸡蛋块一起炒熟

a()
b()
c()
d()
e()

 情况2——步骤间存在线性的状态依赖
->番茄切块->鸡蛋打成蛋液->蛋液煮成半熟->将蛋切成块->番茄与鸡蛋块一起炒熟

a('番茄', function(v番茄块){
b('鸡蛋', function(v蛋液){
c(v蛋液, function(v半熟的鸡蛋){
d(v半熟的鸡蛋, function(v鸡蛋块){
e(v番茄块, v鸡蛋块)
})
})
})
})

这就是Callback Hell了

 情况3——步骤间存在复杂的状态依赖
异步执行:->番茄切块 |->番茄与鸡蛋块一起炒熟
->鸡蛋打成蛋液->蛋液煮成半熟->切成蛋块|

异步调用所带来的问题是

  1. 状态依赖关系难以表达,更无法使用if...else,while等流程控制语句。
  2. 无法提供try...catch异常机制来处理异常

初次尝试——EventProxy

EventProxy作为一个事件系统,通过after、tail等事件订阅方法提供带约束的事件触发机制,“约束”对应“前置条件”,因此我们可以利用这种带约束的事件触发机制来作为异步执行模式下的流程控制表达方式。

const doAsyncIO = (value, cb) => setTimeout(()=>cb(value), Math.random() * 1000)
const ep = new EventProxy() /* 定义任务 */
const a = v番茄 => doAsyncIO('番茄块', ep.emit.bind(ep,'a'))
const b = v鸡蛋 => doAsyncIO('蛋液', ep.emit.bind(ep,'b'))
const c = v蛋液 => doAsyncIO('半熟的鸡蛋', ep.emit.bind(ep,'c'))
const d = v半熟的鸡蛋 => doAsyncIO('鸡蛋块', ep.emit.bind(ep,'d'))
const e = (v番茄块, v鸡蛋块) => doAsyncIO('番茄炒鸡蛋', ep.emit.bind(ep,'e')) /* 定义任务间的状态依赖 */
ep.once('b',c)
ep.once('c',d)
ep.all('a', 'd', e) /* 执行任务 */
a()
b()

另外通过error事件提供对异常机制的支持

ep.on('error', err => {
console.log(err)
})

但由于EventProxy采用事件机制来做流程控制,而事件机制好处是降低模块的耦合度,但从另一个角度来说会使整个系统结构松散难以看出主干模块,因此通过事件机制实现流程控制必然导致代码结构松散和逻辑离散,不过这可以良好的组织形式来让代码结构更紧密一些。

曙光的出现——Promise

这里的Promise指的是已经被ES6纳入囊中的Promises/A+规范及其实现.
Promise相当于我们去麦当劳点餐后得到的小票,在未来某个时间点拿着小票就可以拿到食物。不同的是,只要我们持有Promise实例,无论索取多少次,都能拿到同样的结果。而麦当劳显然只能给你一份食物而已。
代码表现如下

const p1 = new Promise(function(resolve, reject){
/* 工厂函数
* resolve函数表示当前Promise正常结束, 例子: setTimeout(()=>resolve('bingo'), 1000)
* reject函数表示当前Promise发生异常, 例子: setTimeout(()=>reject(Error('OMG!')), 1000)
*/
})
const p2 = p1.then(
function fulfilled(val){
return val + 1
}
, function rejected(err){
/*处理p1工厂函数中调用reject传递来的值*/
}
)
const p3 = p2.then(
function fulfilled(val){
return new Promise(function(resolve){setTimeout(()=>resolve(val+1), 10000)})
}
, function rejected(err){
/*处理p1或p2调用reject或throw error的值*/
}
)
p3.catch(function rejected(err){
/*处理p1或p2或p3调用reject或throw error的值*/
}
)

Promises/A+中规定Promise状态为pending(默认值)、fulfilled或rejected,其中状态仅能从pending->fulfilled或pending->rejected,并且可通过then和catch订阅状态变化事件。状态变化事件的回调函数执行结果会影响Promise链中下一个Promise实例的状态。另外在触发Promise状态变化时是可以携带附加信息的,并且该附加信息将沿着Promise链被一直传递下去直到被某个Promise的事件回调函数接收为止。而且Promise还提供Promise.all和Promise.race两个帮助方法来实现与或的逻辑关系,提供Promsie.resolve来将thenable对象转换为Promise对象。
API:
new Promise(function(resolve, reject){}), 带工厂函数的构造函数
Promise.prototype.then(fulfilled()=>{}, rejected()=>{}),订阅Promise实例状态从pending到fulfilled,和从pending到rejected的变化
Promise.prototype.catch(rejected()=>{}),订阅Promise实例状态从pending到rejected的变化
Promise.resolve(val), 生成一个状态为fulfilled的Promise实例
Promise.reject(val), 生成一个状态为rejected的Promise实例
Promise.all(array), 生成一个Promise实例,当array中所有Promise实例状态均为fulfilled时,该Promise实例的状态将从pending转换为fulfilled,若array中某个Promise实例的状态为rejected,则该实例的状态将从pending转换为rejected.
Promise.race(array), 生成一个Promise实例,当array中某个Promise实例状态发生转换,那么该Promise实例也随之转

const doAsyncIO = value => resolve => setTimeout(()=>resolve(value), Math.random() * 1000)

/* 定义任务 */
const a = v番茄 => new Promise(doAsyncIO('番茄块'))
const b = v鸡蛋 => new Promise(doAsyncIO('蛋液'))
const c = v蛋液 => new Promise(doAsyncIO('半熟的鸡蛋'))
const d = v半熟的鸡蛋 => new Promise(doAsyncIO('鸡蛋块'))
const e = ([v番茄块, v鸡蛋块]) => new Promise(doAsyncIO('番茄炒鸡蛋')) /* 执行任务 */
Promise.all([
a('番茄'),
b('鸡蛋').then(c).then(d)
]).then(e)
.catch(err=>{
console.log(err)
})

最大特点:独立的可存储的异步调用结果
其他特点:fulfilled和rejected函数异步执行

jQuery作为前端必备工具,也为我们提供类似与Promise的工具,那就是jQuery.Deffered

const deffered = $.getJSON('dummy.js')
deffered.then(function(val1){
console.log(val1)
return !val1
},function (err){
console.log(err)
}).then(function(val2){
console.log(val2)
})

但jQuery.Deferred并不是完整的Promise/A+的实现。
如:

  1. jQuery1.8之前上述代码val2的值与val1一样,jQuery1.8及以后上述代码val2的值就是!val1了。
  2. fulfilled和rejected函数采用同步执行

遗留问题!

const a = () => Promise.resolve('a')
const b = (v1) => Promise.resolve('b')
const c = (v2, v1) => console.log(v1) a().then(b).then(c)

真正的光明——Coroutine

 Coroutine中文就是协程,意思就是线程间采用协同合作的方式工作,而不是抢占式的方式工作。由于JS是单线程运行的,所以这里的Coroutine就是一个可以部分执行后退出,后续可在之前退出的地方继续往下执行的函数.

function coroutine(){
yield console.log('c u later!')
console.log('welcome guys!')
}

Generator Function

 其实就是迭代器,跟C#的IEnumrable、IEnumerator和Java的Iterable、Iterator一样。

function* enumerable(){
yield 1
yield 2
}
for (let num of enumerable()){
console.log(num)
}

 现在我们将1,2替换为代码

function *enumerable(msg){
console.log(msg)
var msg1 = yield msg + ' after ' // 断点
console.log(msg1)
var msg2 = yield msg1 + ' after' // 断点
console.log(msg2 + ' over')
}

编译器会将上述代码转换成

const enumerable = function(msg){
var state = -1 return {
next: function(val){
switch(++state){
case 0:
console.log(msg + ' after')
break
case 1:
var msg1 = val
console.log(msg1 + ' after')
break
case 2:
var msg2 = val
console.log(msg2 + ' over')
break
}
}
}
}

通过调用next函数就可以从之前退出的地方继续执行了。(条件控制、循环、迭代、异常捕获处理等就更复杂了)
其实Generator Function实质上就是定义一个有限状态机,然后通过Generator Function实例的next,throw和return方法触发状态迁移。
next(val), 返回{value: val1, done: true|false}
throw(err),在上次执行的位置抛出异常
return(val),状态机的状态迁移至终止态,并返回{value: val, done: true}
现在我们用Gererator Function来做番茄炒蛋

const doAsyncIO = value => (resolve) => setTimeout(()=>resolve(value), Math.random() * 1000)

/* 定义任务 */
const a = v番茄 => new Promise(doAsyncIO('番茄块'))
const b = v鸡蛋 => new Promise(doAsyncIO('蛋液'))
const c = v蛋液 => new Promise(doAsyncIO('半熟的鸡蛋'))
const d = v半熟的鸡蛋 => new Promise(doAsyncIO('鸡蛋块'))
const e = (v番茄块, v鸡蛋块) => new Promise(doAsyncIO('番茄炒鸡蛋')) function* coroutineFunction(){
try{
var p番茄块 = a('番茄')
var v蛋液 = yield b('鸡蛋')
var v半熟的鸡蛋 = yield c(v蛋液)
var v鸡蛋块 = yield d(v半熟的鸡蛋)
var v番茄块 = yield p番茄块
var v番茄抄鸡蛋 = yield e(v番茄块, v鸡蛋块)
}
catch(e){
console.log(e.message)
}
}
const coroutine = coroutineFunction()
throwError = coroutine.throw.bind(coroutine)
coroutine.next().value.then(function(v蛋液){
coroutine.next(v蛋液).then(function(v半熟的鸡蛋){
coroutine.next(v半熟的鸡蛋).then(function(v鸡蛋块){
coroutine.next().then(function(v番茄块){
coroutine.next(v番茄块).then(function(v番茄抄鸡蛋){
coroutine.next(v番茄抄鸡蛋)
}, throwError)
}, throwError)
}, throwError)
}, throwError)
})

 悲催又回到Callback hell.但我们可以发现coroutineFunction其实是以同步代码的风格来定义任务间的执行顺序(状态依赖)而已,执行模块在后面这个让人头痛的Callback hell那里,并且这个Callback Hell是根据coroutineFunction的内容生成,像这种重复有意义的事情自然由机器帮我们处理最为恰当了,于是我们引入个状态管理器得到

const doAsyncIO = value => (resolve) => setTimeout(()=>resolve(value), Math.random() * 1000)

/* 定义任务 */
const a = v番茄 => new Promise(doAsyncIO('番茄块'))
const b = v鸡蛋 => new Promise(doAsyncIO('蛋液'))
const c = v蛋液 => new Promise(doAsyncIO('半熟的鸡蛋'))
const d = v半熟的鸡蛋 => new Promise(doAsyncIO('鸡蛋块'))
const e = (v番茄块, v鸡蛋块) => new Promise(doAsyncIO('番茄炒鸡蛋')) function* coroutineFunction(){
try{
var p番茄块 = a('番茄')
var v蛋液 = yield b('鸡蛋')
var v半熟的鸡蛋 = yield c(v蛋液)
var v鸡蛋块 = yield d(v半熟的鸡蛋)
var v番茄块 = yield p番茄块
var v番茄抄鸡蛋 = yield e(v番茄块, v鸡蛋块)
}
catch(e){
console.log(e.message)
}
}
iPromise(coroutineFunction)

 舒爽多了!

async和await

ES7引入了async和await两个关键字,Node.js7支持这两货。于是Coroutine写法就更酸爽了.

const doAsyncIO = value => (resolve) => setTimeout(()=>resolve(value), Math.random() * 1000)

/* 定义任务 */
const a = v番茄 => new Promise(doAsyncIO('番茄块'))
const b = v鸡蛋 => new Promise(doAsyncIO('蛋液'))
const c = v蛋液 => new Promise(doAsyncIO('半熟的鸡蛋'))
const d = v半熟的鸡蛋 => new Promise(doAsyncIO('鸡蛋块'))
const e = (v番茄块, v鸡蛋块) => new Promise(doAsyncIO('番茄炒鸡蛋')) async function coroutine(){
try{
var p番茄块 = a('番茄')
var v蛋液 = await b('鸡蛋')
var v半熟的鸡蛋 = await c(v蛋液)
var v鸡蛋块 = await d(v半熟的鸡蛋)
var v番茄块 = await p番茄块
var v番茄抄鸡蛋 = await e(v番茄块, v鸡蛋块)
}
catch(e){
console.log(e.message)
}
}
coroutine()

总结

到这里各位应该会想“不就做个西红柿炒鸡蛋吗,搞这么多,至于吗?”。其实我的看法是

  1. 对于状态依赖简单的情况下,callback的方式足矣;
  2. 对于状态依赖复杂(譬如做个佛跳墙等大菜时),Promise或Coroutine显然会让代码更简洁直观,更容易测试因此bug更少,更容易维护因此更易被优化。

我曾梦想有一天所有浏览器都支持Promise,async和await,大家可以不明就里地写出coroutine,完美地处理异步调用的各种问题。直到有一天知道世上又多了Rxjs这货,不说了继续填坑去:)

 

深究JS异步编程模型的更多相关文章

  1. JS魔法堂:深究JS异步编程模型

    前言  上周5在公司作了关于JS异步编程模型的技术分享,可能是内容太干的缘故吧,最后从大家的表情看出"这条粉肠到底在说啥?"的结果:(下面是PPT的讲义,具体的PPT和示例代码在h ...

  2. 深入理解node.js异步编程:基础篇

    ###[本文是基础内容,大神请绕道,才疏学浅,难免纰漏,请各位轻喷] ##1. 概述 目前开源社区最火热的技术当属Node.js莫属了,作为使用Javascript为主要开发语言的服务器端编程技术和平 ...

  3. JS异步编程 (1)

    JS异步编程 (1) 1.1 什么叫异步 异步(async)是相对于同步(sync)而言的,很好理解. 同步就是一件事一件事的执行.只有前一个任务执行完毕,才能执行后一个任务.而异步比如: setTi ...

  4. 多线程之异步编程: 经典和最新的异步编程模型,async与await

    经典的异步编程模型(IAsyncResult) 最新的异步编程模型(async 和 await) 将 IAsyncInfo 转换成 Task 将 Task 转换成 IAsyncInfo 示例1.使用经 ...

  5. 多线程之异步编程: 经典和最新的异步编程模型, IAsyncInfo 与 Task 相互转换

    经典的异步编程模型(IAsyncResult) 最新的异步编程模型(async 和 await) 将 IAsyncInfo 转换成 Task 将 Task 转换成 IAsyncInfo 示例1.使用经 ...

  6. 谈谈c#中异步编程模型的变迁

    大家在编程过程中都会用到一些异步编程的情况.在c#的BCL中,很多api都提供了异步方法,初学者可能对各种不同异步方法的使用感到迷惑,本文主要为大家梳理一下异步方法的变迁以及如何使用异步方法. Beg ...

  7. js异步编程

    前言 以一个煮饭的例子开始,例如有三件事,A是买菜.B是买肉.C是洗米,最终的结果是为了煮一餐饭.为了最后一餐饭,可以三件事一起做,也可以轮流做,也可能C需要最后做(等A.B做完),这三件事是相关的, ...

  8. 重新想象 Windows 8 Store Apps (44) - 多线程之异步编程: 经典和最新的异步编程模型, IAsyncInfo 与 Task 相互转换

    [源码下载] 重新想象 Windows 8 Store Apps (44) - 多线程之异步编程: 经典和最新的异步编程模型, IAsyncInfo 与 Task 相互转换 作者:webabcd 介绍 ...

  9. 【温故知新】c#异步编程模型(APM)--使用委托进行异步编程

    当我们用到C#类许多耗时的函数XXX时,总会存在同名的类似BeginXXX,EndXXX这样的函数. 例如Stream抽象类的Read函数就有 public abstract int Read(byt ...

随机推荐

  1. dba管理

    ORACLE 锁表的解决方法及查找引起锁表SQL语句方法  1. ORACLE中查看当前系统中锁表情况 select * from v$locked_object 可以通过查询v$locked_obj ...

  2. Java制作证书的工具keytool用法总结

    一.keytool的概念 keytool 是个密钥和证书管理工具.它使用户能够管理自己的公钥/私钥对及相关证书,用于(通过数字签名)自我认证(用户向别的用户/服务认证自己)或数据完整性以及认证服务.在 ...

  3. Android广播错误.MainActivity$MyReceiver; no empty constructor

    广播的定义,如果是内部类,必须为静态类. 下面总结一下作为内部类的广播接收者在注册的时候需要注意的地方:   1.清单文件注册广播接收者时,广播接收者的名字格式需要注意.因为是内部类,所以需要在内部类 ...

  4. ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)

    ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) 背景: mys ...

  5. Maven学习之 插件plugin

    Maven本质上是一个执行插件的框架.插件共分两类:build插件和reporting插件. build插件,会在build阶段被执行,应该配置在POM的<build/>元素中. repo ...

  6. 关于Plupload结合上传插件jquery.plupload.queue的使用

    之前使用过很多的上传组件,但对各种浏览器的兼容性太差,不得不放弃!! plupload 是款很强大的上传组件,不得不推荐.plupload 前端根据浏览器不同选择使用Html5. Gears, Sil ...

  7. Android Studio签名打包的两种方式

    签名打包的两种方式: 注:给我们自己开发的app签名,就代表着我自己的版权,以后要进行升级,也必须要使用相同的签名才行.签名就代表着自己的身份(即keystore),多个app可以使用同一个签名. 如 ...

  8. 斯坦福第三课:线性代数回顾(Linear Algebra Review)

    3.1  矩阵和向量 3.2  加法和标量乘法 3.3  矩阵向量乘法 3.4  矩阵乘法 3.5  矩阵乘法的性质 3.6  逆.转置 3.1  矩阵和向量 如图:这个是 4×2 矩阵,即 4 行  ...

  9. Titanium系列--安装Titanium Studio 中的Android SDK,JDK以及环境变量的配置(二)

    Ubuntu安装配置JDK 1.先去 Oracle下载Linux下的JDK压缩包,我下载的是jdk-8u25-linux-x64.tar.gz文件,下好后直接解压 Step1:# 将解压好的jdk1. ...

  10. Cisco ASA intra-interface routing

    LAN1和LAN2的默认路由指向各自的ASA,各ASA中设置对方LAN的静态路由指向ROUTER,打开ASA的intra-interface traffic,关闭LAN1和LAN2地址互相访问的NAT ...