前言

 上周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——步骤间存在复杂的状态依赖

异步执行:->番茄切块 |->番茄与鸡蛋块一起炒熟

->鸡蛋打成蛋液->蛋液煮成半熟->切成蛋块|

/* 定义任务 */
//番茄切块
const a = (v番茄, cb = id) => doAsyncIO(a, '番茄块', cb)
//将鸡蛋打成蛋液
const b = (v鸡蛋, cb = id) => doAsyncIO(b, '蛋液', cb)
// 蛋液煮成半熟
const c = (v蛋液, cb = id) => doAsyncIO(c, '半熟的鸡蛋', cb)
// 切成蛋块
const d = (v半熟的鸡蛋, cb = id) => doAsyncIO(d, '鸡蛋块', cb)
// 番茄与鸡蛋块一起炒熟
const e = (v番茄块, v鸡蛋块, cb = id) => doAsyncIO(e, '番茄炒鸡蛋', cb) [a,b,c,d,e].forEach(task =>{
task.state = {done: false}
task.done = value => {
task.state = {value, done: true}
return value
}
}) /* 执行任务 */
a('番茄')
b('鸡蛋', function(v蛋液){
c(v蛋液, function(v半熟的鸡蛋){
d(v半熟的鸡蛋)
})
})
poll = () => {
if (a.state.done && d.state.done);else return setTimeout(poll, 100) e(a.state.value, d.state.value)
}
setTimeout(poll, 0)

异步调用所带来的问题是

  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这货,不说了继续填坑去:)

尊重原创,转载请注明来自:http://www.cnblogs.com/fsjohnhuang/p/6109701.html _肥仔John

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

  1. 深究JS异步编程模型

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

  2. JS魔法堂:ES6新特性——GeneratorFunction介绍

    一.前言       第一次看koajs的示例时,发现该语句 function *(next){...............} ,这是啥啊?于是搜索一下,原来这是就是ES6的新特性Generator ...

  3. JS魔法堂:IMG元素加载行为详解

    一.前言 在<JS魔法堂:jsDeferred源码剖析>中我们了解到img元素加载失败可以作为函数异步执行的优化方案,本文打算对img元素的加载行为进行更深入的探讨. 二.资源加载的相关属 ...

  4. JS魔法堂:jsDeferred源码剖析

    一.前言 最近在研究Promises/A+规范及实现,而Promise/A+规范的制定则很大程度地参考了由日本geek cho45发起的jsDeferred项目(<JavaScript框架设计& ...

  5. JS魔法堂:浏览器模式和文档模式怎么玩?

    一.前言 从IE8开始引入了文档兼容模式的概念,作为开发人员的我们可以在开发人员工具中通过“浏览器模式”和“文档模式”(IE11开始改为“浏览器模式”改成更贴切的“用户代理字符串”)品味一番,它的出现 ...

  6. JS魔法堂:不完全国际化&本地化手册 之 理論篇

    前言  最近加入到新项目组负责前端技术预研和选型,其中涉及到一个熟悉又陌生的需求--国际化&本地化.熟悉的是之前的项目也玩过,陌生的是之前的实现仅仅停留在"有"的阶段而已. ...

  7. JS魔法堂:不完全国际化&本地化手册 之 实战篇

    前言  最近加入到新项目组负责前端技术预研和选型,其中涉及到一个熟悉又陌生的需求--国际化&本地化.熟悉的是之前的项目也玩过,陌生的是之前的实现仅仅停留在"有"的阶段而已. ...

  8. JS魔法堂:剖析源码理解Promises/A规范

    一.前言 Promises/A是由CommonJS组织制定的异步模式编程规范,有不少库已根据该规范及后来经改进的Promises/A+规范提供了实现 如Q, Bluebird, when, rsvp. ...

  9. JS魔法堂:判断节点位置关系

    一.前言 在polyfill querySelectorAll 和写弹出窗时都需要判断两个节点间的位置关系,通过jQuery我们可以轻松搞定,但原生JS呢?下面我将整理各种判断方法,以供日后查阅. 二 ...

随机推荐

  1. EventBus实现activity跟fragment交互数据

    最近老是听到技术群里面有人提出需求,activity跟fragment交互数据,或者从一个activity跳转到另外一个activity的fragment,所以我给大家介绍一个开源项目,EventBu ...

  2. 【用户交互】APP没有退出前台但改变系统属性如何实时更新UI?监听系统广播,让用户交互更舒心~

    前日,一小伙伴问我一个问题,说它解决了半天都没解决这个问题,截图如下: 大概楼主理解如下: 如果在应用中有一个判断wifi的开关和一个当前音量大小的seekbar以及一个获取当前电量多少的按钮,想知道 ...

  3. 看图理解JWT如何用于单点登录

    单点登录是我比较喜欢的一个技术解决方案,一方面他能够提高产品使用的便利性,另一方面他分离了各个应用都需要的登录服务,对性能以及工作量都有好处.自从上次研究过JWT如何应用于会话管理,加之以前的项目中也 ...

  4. SuperMap-iServer-单点登录功能验证(CAS)

    SuperMap-iServer-单点登录功能验证(CAS) 1.测试目的: 验证SuperMap-iServer使用CAS单点登录的功能是否正常. 2.测试环境: SuperMap-iServer8 ...

  5. SVN的使用

  6. 使用Hudson搭建自动构建服务器

    环境: ubuntu1404_x64 说明: 使用hudson和git搭建自动构建服务器的简单示例 安装hudson及相关插件 安装hudson 安装命令如下: sudo sh -c "ec ...

  7. Linux下高cpu解决方案

    昨天搞定了一个十万火急的issue,客户抱怨产品升级后系统会变慢和CPU使用率相当高,客户脾气很大,声称不尽快解决这个问题就退货,弄得我们 R&D压力很大,解决这个issue的任务分给了我,客 ...

  8. MMORPG大型游戏设计与开发(攻击区域 扇形)

    距离上次发布已经有了很长一段时间,期间由于各种原因没有更新这方面的技术分享,在这里深表遗憾.在MMO或其他的游戏中,会有针对各种形状的计算,通常在攻击区域里不会很复杂,常见的为矩形.圆形.扇形.今天分 ...

  9. 最小生成树(Kruskal算法-边集数组)

    以此图为例: package com.datastruct; import java.util.Scanner; public class TestKruskal { private static c ...

  10. EntityFramework与TransactionScope事务和并发控制

    最近在园子里看到一篇关于TransactionScope的文章,发现事务和并发控制是新接触Entity Framework和Transaction Scope的园友们不易理解的问题,遂组织此文跟大家共 ...