阅读更多

1顶
0踩

编程语言

原创新闻 深入浅出 JS 异步处理技术方案

2017-12-27 10:31 by 副主编 jihong10102006 评论(0) 有18766人浏览
引用
来源:gitbook
作者:Gitbook-icepy

为什么要异步

"当我们在星巴克买咖啡时,假设有100个人在排队,也许咖啡的下单只要10S,但是咖啡的制作到客人领取咖啡要1000S。如果在同步的场景下,第一个客人下单到领取完咖啡要1010S才能轮到下一个客人,这在效率(某些场景)上来说会比较低下。如果我们异步处理这个流程,客人下单10S拿到凭证,客人就可以去做别的事情,并且10S后下一个客人可以继续下单,并不阻碍流程。反而可以通过凭证,让客人拿到自己的咖啡,也许时间上并不是第一个下单的客人先拿到。"

在网页的世界里也是同样的道理,不妨我们看看在执行JS代码的主线程里,如果遇到了AJAX请求,用户事件等,如果不采用异步的方案,你会一直等待,等待第一个耗时的处理完成才能接上下一个JS代码的执行,于是界面就卡住了。

“也许有人会想,既然大家都说现在网页上性能损耗最大的属于DOM节点的操作,把这些搞成异步,行不行?其实这会带来一个不确定性问题:既“成功”的状态到底谁先来的问题。可以想象一下,如果我们在操作DOM,既给节点添加内容,也给节点删除,那么到底以谁为基准呢?考虑到复杂性,也就可见一斑了。”

Event loop

虽然异步与event loop没有太直接的关系,准确的来讲event loop 只是实现异步的一种机制。(了解为主)

“还是以上面咖啡馆为例子,假定场景还是100人,这100人除了下单是与咖啡本身有关联之外,其余的时间,比如看书,玩游戏的等可以视为自己的执行逻辑。如果用event loop来给它做一个简单的画像,那么它就像:在与咖啡店店员沟通下单视为主执行栈,咖啡的制作可以视为一个异步任务,添加到一个任务队列里,一直等带100个人都下单完成,然后开始读取任务队列中的异步任务,事件名就是下单凭证,如果有对应的handler,那么就执行叫对应的客人来领取咖啡。这个过程,是循环不断的。假设没有客人来下单的时候,也就是店员处于空闲时间(可能自己去搞点别的)。”

传统的Callback

假定一个asyncFetchDataSource函数用于获取远程数据源,可能有20S。
function asyncFetchDataSource(cb){
   (… 获取数据, function(response){
      typeof cb === 'function' && cb(response)
   })
}

这种形式的callback可以适用于简单场景,如果这里有一个更复杂的场景,比如获取完数据源之后,依据id,获取到某个数据,在这某个数据中再依据id来更新某个列表,可以遇见的能看到代码变成了:
asyncFetchDataSource('',function(data_a){
  const { id_a } = data_a
   asyncFetchDataSource( id_a,function(data_b){
           const { id_b } = data_b
    asyncFetchDataSource(id, function(data_c){

    })
   })
})

如果有极端情况出现,这里的callback就会变成无极限了。

Thunk函数

这是一种“传名调用”的策略,表现的形式就是将参数放入一个临时函数,然后再将这个临时函数传入函数体内。
function asyncFetchDataSource(url){
   return function(callback){
      fetch(url, callback)
   }
}

const dataSource = asyncFetchDataSource('https://github.com/icepy');
dataSource(function(data){

})

Promise

Promise正是想来处理这样的异步编程,如果我们用Promise该如何处理一段Ajax?
function fetch(){
  return new Promise(function(resolve,reject){
    $.ajax({
      url: 'xxx',
      success:function(data){
        resolve(data)
      },
      error:function(error){
        reject(error)
      }
    })
  })
}
fetch().then(function(data){
}).catch(function(error){})

Promise声明周期:
  • 进行中(pending)
  • 已经完成(fulfilled)
  • 拒绝(rejected)
如同上面Ajax的例子,我们可以很好的包装一个函数,让fetch函数返回一个Promise对象。在Promise构造函数里,可以传入一个callback,并且在这里完成主体逻辑的编写。唯一需要注意的是:Promise对象只能通过resolve和reject函数来返回,在外部使用then或catch来获取。如果你直接抛出一个错误(throw new Error('error')),catch也是可以正确的捕获到的。

Promise其他的方法

Promise.all(当所有在可迭代参数中的 promises 已完成,或者第一个传递的 promise(指 reject)失败时,返回 promise。)
var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, "foo");
}); 
Promise.all([p1, p2, p3]).then(values => { 
  console.log(values); // [3, 1337, "foo"] 
});

Promise.race(返回一个新的 promise,参数iterable中只要有一个promise对象"完成(resolve)"或"失败(reject)",新的promise就会立刻"完成(resolve)"或者"失败(reject)",并获得之前那个promise对象的返回值或者错误原因。)
var p1 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 500, "one"); 
});
var p2 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 100, "two"); 
});
Promise.race([p1, p2]).then(function(value) {
  console.log(value); // "two"
  // 两个都完成,但 p2 更快
});

有趣的是如果你使用ES6的class,你是可以去派生Promise的。
class MePromise extends Promise{
 // 处理 ...
}

Generator

Generator可以辅助我们完成很多复杂的任务,而这些基础知识,又与iterator息息相关,举一个很简单的例子,相信有很多朋友,应该使用过co这个异步编程的库,它就是用Generator来实现,当然它的设计会比例子要复杂的多,我们先来看一个co简单的用法:
import co from 'co'
co(function* () {
  var result = yield Promise.resolve(true);
  return result;
}).then(function (value) {
  console.log(value);
}, function (err) {
  console.error(err.stack);
});

相应的,我们来实现一个简化的版本:
function co(task){
  let _task = task()
  let resl = _task.next();
  while(!resl.done){
    console.log(resl);
    resl = _task.next(resl.value);
  }
}
function sayName(){
  return {
    name: 'icepy'
  }
}
function assign *(f){
  console.log(f)
  let g = yield sayName()
  return Object.assign(g,{age:f});
}
co(function *(){
  let info = yield *assign(18)
  console.log(info)
})

虽然,这个例子中,还不能很好的看出来“异步”的场景,但是它很好的描述了Generator的使用方式。

从最开始的定义中,已经和大家说明了,Generator最终返回的依然是一个迭代器对象,有了这个迭代器对象,当你在处理某些场景时,你可以通过yield来控制,流程的走向。通过co函数,我们可以看出,先来执行next方法,然后通过一个while循环,来判断done是否为true,如果为true则代表整个迭代过程的结束,于是,这里就可以退出循环了。在Generator中的返回值,可以通过给next方法传递参数的方式来实现,也就是遇上第一个yield的返回值。

有逻辑,自然会存在错误,在Generator捕获错误的时机与执行throw方法的顺序有关系,一个小例子:
let hu = function *(){
  let g = yield 1;
  try {
    let j = yield 2;
  } catch(e){
    console.log(e)
  }
  return 34
}
let _it = hu();
console.log(_it.next())
console.log(_it.next())
console.log(_it.throw(new Error('hu error')))

当我能捕获到错误的时机是允许完第二次的yield,这个时候就可以try了。

async await
async function createNewDoc() {
  let response = await db.post({}); // post a new doc
  return await db.get(response.id); // find by id
}

https://tc39.github.io/ecmascript-asyncawait/

根据规范规定一个asnyc函数总是要返回一个Promise,从代码直观上来说,虽然简洁了,但是async await并未万能,它有很大的局限性,比如:
  • 因为是顺序执行,假设有三个请求,那么这里并没有很好的利用到异步带来的止损(再包装一个Promise.all)
  • 如果要捕获异常,需要去包try catch
  • 缺少控制流程,比如progress(进度)pause,resume等周期性的方法
  • 没有打断的功能
主流的异步处理方案

我喜欢用co,而且社区使用也很广泛,https://github.com/tj/co
co(function* () {
  var result = yield Promise.resolve(true);
  return result;
}).then(function (value) {
  console.log(value);
}, function (err) {
  console.error(err.stack);
});

babel polyfill 支持,在浏览器环境中使用异步解决方案

如果你想使用全的polyfiil,直接npm install --save babel-polyfill,然后在webpack里进行配置即可。
module.exports = {
  entry: ["babel-polyfill", "./app/js"]
};

当然由于我目前的开发基于的浏览器都比较高,所以我一般是挑选其中的:

https://github.com/facebook/regenerator/tree/master/packages/regenerator-runtime
https://github.com/facebook/regenerator/tree/master/packages/regenerator-transform

如果你要使用async await 配置上http://babeljs.io/docs/plugins/transform-async-to-generator/即可

Node.js 环境中使用异步解决方案

由于本人的node使用的LTS已经是8.9.3版本了,所以大部分情况下已经不再使用babel去进行转换,而是直接使用co这样的库。当然co也不是万能,一定要根据业务场景,与其他异步处理的方式,配合中使用。

总结

相信未来的JS编程,只会越来越简单,不要拘泥于语法,语言上的特性,不妨多看一看“外面的世界“。
来自: gitbook
1
0
评论 共 0 条 请登录后发表评论

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 《深入浅出Node.js》:Node异步编程解决方案之事件发布-订阅模式.docx

    《深入浅出Node.js》:Node异步编程解决方案之事件发布-订阅模式.docx

  • 深入浅出JavaScript异步编程

    深入浅出JavaScript异步编程1.什么是异步编程2. 为什么浏览器下有异步编程3. 异步回调有什么问题4. 解决异步回调问题的方案5. 从浏览器原理分析Promise原理6. 生成器与协程7. co框架的原理,Promise与生成器的结合8....

  • 学习深入浅出Vue.js(刘博文)

    1.vue.js声明式操作DOM 2.所谓渐进式框架,就是把框架分层。 最核心的部分是视图层渲染,然后往外是组件机制,在这个基础上再加入路由机制,再加入状态管理,最外层是构建工具 3.任何技术的引入都是在解决一些...

  • 【NOTE】【深入浅出nodejs】异步 I/O(一)

    对于《深入浅出Node.js》第3章的个人摘抄

  • 深入javascript计划六:深入浅出异步

    但是回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,你可能就会写出如下代码: ajax(url, () => { // 处理逻辑 ajax(url1, () => { // 处理逻辑 ajax(url2...

  • Javascript异步编程深入浅出

    javascript异步编程的详细剖析

  • 《JavaScript》异步编程(三)深入浅出Promise

    Promise啊Promise,前端的异步领域中一座不得不跨越的大山,虽然在实际的项目开发中对Promise的运用会太深入,但是它却又是一个衡量前端工程师技术水平的门槛,最直接的体现,现在如果出去面试,如果面试中不来点...

  • 《深入浅出 node.js》笔记part1

    阅读《深入浅出node.js》笔记-Part1

  • 深入浅出NodeJS——异步I/O

    底层操作系统,异步通过信号量、消息等方式有着广泛的应用。 PHP语言从头到尾都是以同步堵塞方式执行,利于程序猿顺序编写业务逻辑。 异步I/O、事件驱动、单线程构成Node的基调。 why异步I/O (1)、用户体验 在...

  • 读《深入浅出Node.js》笔记

    本着对js这门语言的喜爱,我阅读了node.js。希望阅读过程中对其有所收获。以此共勉 Node简介 – 第一章 我惊奇于Node.js竟然是一门这么年轻的语言,2009年就诞生了 选择js的原因 高性能web服务器有两个要点:事件...

  • 【深入浅出node.js】二刷划重点

    1.异步IO 前端:Ajax请求为最典型,前端请求发送请求后,执行其他后续代码,后续执行完成后,通过‘Don’t call me,I will call you’的原则,返回前端结果。 后端:以文件读取为例,可以发起读取文件后,执行其他...

  • Node.js 深入浅出

    本文是对于Node.js的简单介绍,读者可以通过本文对于Node.JS有一个全新的认识

  • 深入浅出Node.js - 异步I/O

    异步I/O 异步I/O与非阻塞I/O 从计算机内核I/O而言,同步/异步和阻塞/非阻塞是两个不同的概念。 操作系统内核对于I/O只有两种方式,阻塞和非阻塞。 阻塞I/O的特点是调用之后一定要等到系统内核层面完成所有操作后,...

  • 深入浅出Nodejs读书笔记

    深入浅出Nodejs读书笔记

  • 深入浅出JS—17 Promise产生背景与使用方法

    Promise为处理异步事件提供了很好的解决方案 场景 在开发中,通常异步请求函数和处理数据函数并不在同一个文件中,在request.js中发起异步请求,得到返回结果 // 发起异步请求 function requestData(url) { ...

  • 《深入浅出Node.js》:Node异步编程解决方案 之 事件发布-订阅模式

    灵活掌握函数式编程,异步编程就有了得心应手的基础。之所以说是基础,是还需要对异步的编程思路有清晰的认识。 为照顾开发者的阅读思维习惯,同步I/O曾盛行多年。但随着技术快速发展,性能瓶颈问题已无法回避。虽然...

  • [书籍精读]《深入浅出Node.js》精读笔记分享

    书籍介绍:本书由首章Node介绍为索引,涉及Node的各个方面,主要内容包含模块机制的揭示、异步I/O实现原理的展现、异步编程的探讨、内存控制的介绍、二进制数据Buffer的细节、Node中的网络编程基础、Node中的Web开发...

  • 深入浅出Node.js

    深入浅出Node.js 一直想致力于写一篇关于广义讲解Node.js系统的文章,苦于时间有限,资源有限。这篇文章是在结合自己的学习心得以及与行业大佬共同探讨下争对于熟练掌握JS语言后的广义Node.js.至于为什么叫作广义在...

  • AVR单片机项目-ADC键盘(源码+仿真+效果图).zip

    使用adc功能来判断不同电压,那必定是通过电压的不同来区分的,这就需要按键与电阻进行组合,我设计打算使用正比关系的按键阻值,这样会比较好在程序判断,最后就如仿真图那样设计,按键按下让某部分电路短路,剩下的电路得到不同的电压值,而不同按键按下,对应的电阻值是10k的倍数,很好区分。而基地的电阻设为10k,按键靠近gnd的电压值最小,远离则慢慢增大,可大概计算出来的,分压的电压为5v。按键不按时为0v,有按键按的电压范围为2.5v~0.238v。然后用以前编写好的数码管驱动拿过来用,也就是用动态扫描的方式进行显示的。然后编写adc代码,根据atmega16的数据手册就可以慢慢写出来了,即配置好ADMUX、ADCSRA寄存器,使用单次触发的方式,写好对应的函数,在初始化之后,使用定时器1中断进行adc的读取和数码管的刷新显示。而adc对应按键的判断也使用了for循环对1024分成1~21份,对其附近符合的值即可判断为按键i-1,可直接显示出来,而误差值可以多次测量后进行调整。 使用adc功能来判断不同电压,那必定是通过电压的不同来区分的,这就需要按键与电阻进行组合,我设计打算使用正比关系的按

Global site tag (gtag.js) - Google Analytics