资讯专栏INFORMATION COLUMN

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

idealcn / 3332人阅读

摘要:而同步和异步则是描述另一个方面。异步将数据从内核空间拷贝到用户空间的操作由系统自动处理,然后通知应用程序直接使用数据即可。

前言

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

重申主题

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

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——步骤间存在复杂的状态依赖
异步执行:->番茄切块 |->番茄与鸡蛋块一起炒熟

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

异步调用所带来的问题是

状态依赖关系难以表达,更无法使用if...else,while等流程控制语句。

无法提供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+的实现。
如:

jQuery1.8之前上述代码val2的值与val1一样,jQuery1.8及以后上述代码val2的值就是!val1了。

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()
总结

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

对于状态依赖简单的情况下,callback的方式足矣;

对于状态依赖复杂(譬如做个佛跳墙等大菜时),Promise或Coroutine显然会让代码更简洁直观,更容易测试因此bug更少,更容易维护因此更易被优化。

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

尊重原创,转载请注明来自:http://www.cnblogs.com/fsjohn... ^_^肥仔John

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81073.html

相关文章

  • WebComponent魔法:深究Custom Element 之 面向痛点编程

    摘要:前言最近加入到新项目组负责前端技术预研和选型,一直偏向于以为代表的技术线,于是查阅各类资料想说服老大向这方面靠,最后得到的结果是资料是英语无所谓,最重要是上符合要求,技术的事你说了算。但当我们需要动态实例化元素时,命令式则是最佳的选择。 前言  最近加入到新项目组负责前端技术预研和选型,一直偏向于以Polymer为代表的WebComponent技术线,于是查阅各类资料想说服老大向这方面...

    flyer_dev 评论0 收藏0
  • WebComponent魔法:深究Custom Element 之 标准构建

    摘要:明确各阶段适合的操作用于初始化元素的状态和设置事件监听,或者创建。事件类型转换通过捕获事件,然后通过发起事件来对事件类型进行转换,从而触发更符合元素特征的事件类型。 前言  通过《WebComponent魔法堂:深究Custom Element 之 面向痛点编程》,我们明白到其实Custom Element并不是什么新东西,我们甚至可以在IE5.5上定义自己的alert元素。但这种简单...

    philadelphia 评论0 收藏0
  • CSS魔法:display:none与visibility:hidden的恩怨情仇

    摘要:不耽误表单提交数据虽然我们无法看到的元素,但当表单提交时依然会将隐藏的元素的值提交上去。让元素在见面上不可视,但保留元素原来占有的位置。不过由于各浏览器实现效果均有出入,因此一般不会使用这个值。继承父元素的值。 前言  还记得面试时被问起请说说display:none和visibility:hidden的区别吗?是不是回答完display:none不占用原来的位置,而visibilit...

    selfimpr 评论0 收藏0
  • 前端魔法——异常不仅仅是try/catch

    摘要:我打算分成前端魔法堂异常不仅仅是和前端魔法堂调用栈,异常实例中的宝藏两篇分别叙述内置自定义异常类,捕获运行时异常语法异常网络请求异常事件,什么是调用栈和如何获取调用栈的相关信息。 前言  编程时我们往往拿到的是业务流程正确的业务说明文档或规范,但实际开发中却布满荆棘和例外情况,而这些例外中包含业务用例的例外,也包含技术上的例外。对于业务用例的例外我们别无它法,必须要求实施人员与用户共同...

    bladefury 评论0 收藏0
  • JS魔法之实战:纯前端的图片预览

    摘要:一前言图片上传是一个普通不过的功能,而图片预览就是就是上传功能中必不可少的子功能了。偶然从上找到纯前端图片预览的相关资料,经过整理后记录下来以便日后查阅。类型为,表示在读取文件时发生的错误,只读。 一、前言   图片上传是一个普通不过的功能,而图片预览就是就是上传功能中必不可少的子功能了。在这之前,我曾经通过订阅input[type=file]元素的onchange事件,一旦更改路径...

    岳光 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<