ES6 javascript的异步操作实例详解


Posted in Javascript onOctober 30, 2017

本文实例讲述了ES6 javascript的异步操作。分享给大家供大家参考,具体如下:

异步编程对 JavaScript 语言太重要。 Javascript 语言的执行环境是“ 单线程” 的, 如果没有异步编程, 根本没法用, 非卡死不可。

ES6 诞生以前, 异步编程的方法, 大概有下面四种。

① 回调函数
② 事件监听
③ 发布 / 订阅
④ Promise 对象

ES6 将 JavaScript 异步编程带入了一个全新的阶段, ES7 的Async函数更是提出了异步编程的终极解决方案。

一、基本概念

1. 异步

所谓 " 异步 ",简单说就是一个任务分成两段, 先执行第一段, 然后转而执行其他任务, 等做好了准备, 再回过头执行第二段。

比如, 有一个任务是读取文件进行处理, 任务的第一段是向操作系统发出请求, 要求读取文件。 然后, 程序执行其他任务, 等到操作系统返回文件,再接着执行任务的第二段( 处理文件)。 这种不连续的执行, 就叫做异步。

相应地, 连续的执行就叫做同步。 由于是连续执行, 不能插入其他任务, 所以操作系统从硬盘读取文件的这段时间, 程序只能干等着。

2. 回调函数

JavaScript 语言对异步编程的实现, 就是回调函数。 所谓回调函数, 就是把任务的第二段单独写在一个函数里面, 等到重新执行这个任务的时候, 就直接调用这个函数。 它的英语名字 callback, 直译过来就是 " 重新调用 "。

读取文件进行处理, 是这样写的。

fs.readFile('/etc/passwd', function(err, data) {
  if(err) throw err;
  console.log(data);
});

上面代码中, readFile 函数的第二个参数, 就是回调函数, 也就是任务的第二段。 等到操作系统返回了 / etc / passwd这个文件以后, 回调函数才会执行。

一个有趣的问题是, 为什么 Node.js 约定, 回调函数的第一个参数, 必须是错误对象 err( 如果没有错误, 该参数就是 null)? 原因是执行分成两段, 在这两段之间抛出的错误, 程序无法捕捉, 只能当作参数, 传入第二段。

3. Promise

回调函数本身并没有问题, 它的问题出现在多个回调函数嵌套。 假定读取 A 文件之后, 再读取 B 文件, 代码如下。

fs.readFile(fileA, function(err, data) {
  fs.readFile(fileB, function(err, data) {
    // ...
  });
});

不难想象, 如果依次读取多个文件, 就会出现多重嵌套。 代码不是纵向发展, 而是横向发展, 很快就会乱成一团, 无法管理。 这种情况就称为 " 回调函数噩梦 " ( callback hell )。

Promise 就是为了解决这个问题而提出的。 它不是新的语法功能, 而是一种新的写法, 允许将回调函数的嵌套, 改成链式调用。 采用 Promise, 连续读取多个文件, 写法如下。

var readFile = require('fs-readfile-promise');
readFile(fileA)
  .then(function(data) {
    console.log(data.toString());
  })
  .then(function() {
    return readFile(fileB);
  })
  .then(function(data) {
    console.log(data.toString());
  })
  .catch(function(err) {
    console.log(err);
  });

上面代码中, 我使用了 fs - readfile - promise 模块, 它的作用就是返回一个 Promise 版本的 readFile 函数。 Promise 提供 then 方法加载回调函数,catch 方法捕捉执行过程中抛出的错误。
可以看到, Promise 的写法只是回调函数的改进, 使用 then 方法以后, 异步任务的两段执行看得更清楚了, 除此以外, 并无新意。
Promise 的最大问题是代码冗余, 原来的任务被 Promise 包装了一下, 不管什么操作, 一眼看去都是一堆 then, 原来的语义变得很不清楚。

那么, 有没有更好的写法呢?

二、Generator 函数

1. 协程

传统的编程语言, 早有异步编程的解决方案( 其实是多任务的解决方案)。 其中有一种叫做 " 协程 "(coroutine), 意思是多个线程互相协作, 完成异步任务。

协程有点像函数, 又有点像线程。 它的运行流程大致如下。

第一步, 协程 A 开始执行。
第二步, 协程 A 执行到一半, 进入暂停, 执行权转移到协程 B。
第三步,( 一段时间后) 协程 B 交还执行权。
第四步, 协程 A 恢复执行。

上面流程的协程 A, 就是异步任务, 因为它分成两段( 或多段) 执行。

举例来说, 读取文件的协程写法如下。

function* asyncJob() {
  // ... 其他代码
  var f = yield readFile(fileA);
  // ... 其他代码
}

上面代码的函数asyncJob是一个协程, 它的奥妙就在其中的yield命令。 它表示执行到此处, 执行权将交给其他协程。 也就是说, yield命令是异步两个阶段的分界线。

协程遇到yield命令就暂停, 等到执行权返回, 再从暂停的地方继续往后执行。 它的最大优点, 就是代码的写法非常像同步操作, 如果去除 yield 命令,简直一模一样。

2. Generator 函数的概念

enerator 函数是协程在 ES6 的实现, 最大特点就是可以交出函数的执行权( 即暂停执行)。

整个 Generator 函数就是一个封装的异步任务, 或者说是异步任务的容器。 异步操作需要暂停的地方, 都用yield语句注明。 Generator 函数的执行方法

如下。

function* gen(x) {
  var y = yield x + 2;
  return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }

上面代码中, 调用 Generator 函数, 会返回一个内部指针( 即遍历器) g。 这是 Generator 函数不同于普通函数的另一个地方, 即执行它不会返回结果, 返回的是指针对象。 调用指针 g 的 next 方法, 会移动内部指针( 即执行异步任务的第一段), 指向第一个遇到的 yield 语句, 上例是执行到x + 2 为止。

换言之, next 方法的作用是分阶段执行 Generator 函数。 每次调用 next 方法, 会返回一个对象, 表示当前阶段的信息( value 属性和 done 属性)。 value属性是 yield 语句后面表达式的值, 表示当前阶段的值; done 属性是一个布尔值, 表示 Generator 函数是否执行完毕, 即是否还有下一个阶段。

3. Generator 函数的数据交换和错误处理

Generator 函数可以暂停执行和恢复执行, 这是它能封装异步任务的根本原因。 除此之外, 它还有两个特性, 使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。

next 方法返回值的 value 属性, 是 Generator 函数向外输出数据; next 方法还可以接受参数, 这是向 Generator 函数体内输入数据。

function* gen(x) {
  var y = yield x + 2;
  return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }

上面代码中, 第一个 next 方法的 value 属性, 返回表达式x + 2 的值( 3)。 第二个 next 方法带有参数 2, 这个参数可以传入 Generator 函数, 作为上个阶段异步任务的返回结果, 被函数体内的变量 y 接收。 因此, 这一步的 value 属性, 返回的就是 2( 变量 y 的值)。

Generator 函数内部还可以部署错误处理代码, 捕获函数体外抛出的错误。

function* gen(x) {
  try {
    var y = yield x + 2;
  } catch(e) {
    console.log(e);
  }
  return y;
}
var g = gen(1);
g.next();
g.throw(' 出错了 ');

上面代码的最后一行, Generator 函数体外, 使用指针对象的throw 方法抛出的错误, 可以被函数体内的try...catch 代码块捕获。 这意味着, 出错的代码与处理错误的代码, 实现了时间和空间上的分离, 这对于异步编程无疑是很重要的。

4. 异步任务的封装

下面看看如何使用 Generator 函数, 执行一个真实的异步任务。

var fetch = require('node-fetch');
function* gen() {
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

上面代码中, Generator 函数封装了一个异步操作, 该操作先读取一个远程接口, 然后从 JSON 格式的数据解析信息。 就像前面说过的, 这段代码非常像同步操作, 除了加上了 yield 命令。

执行这段代码的方法如下。

var g = gen();
var result = g.next();
result.value.then(function(data) {
  return data.json();
}).then(function(data) {
  g.next(data);
});

上面代码中, 首先执行 Generator 函数, 获取遍历器对象, 然后使用 next 方法( 第二行), 执行异步任务的第一阶段。 由于 Fetch 模块返回的是一个Promise 对象, 因此要用 then 方法调用下一个 next 方法。

可以看到, 虽然 Generator 函数将异步操作表示得很简洁, 但是流程管理却不方便( 即何时执行第一阶段、 何时执行第二阶段)。

更多相关内容可查看本站专题:《ECMAScript6(ES6)入门教程》、《JavaScript数组操作技巧总结》、《JavaScript字符与字符串操作技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript错误与调试技巧总结》及《javascript面向对象入门教程》

希望本文所述对大家基于ECMAScript的程序设计有所帮助。

Javascript 相关文章推荐
JS对img进行操作(换图片/切图/轮换/停止)
Apr 17 Javascript
js单向链表的具体实现实例
Jun 21 Javascript
js继承call()和apply()方法总结
Dec 08 Javascript
javascript元素动态创建实现方法
May 13 Javascript
微信js-sdk预览图片接口及从拍照或手机相册中选图接口用法示例
Oct 13 Javascript
require.js+vue开发微信上传图片组件
Oct 27 Javascript
微信小程序-详解数据缓存
Nov 24 Javascript
实例浅析js的this
Dec 11 Javascript
脚本div实现拖放功能(两种)
Feb 13 Javascript
JavaScript实现经纬度转换成地址功能
Mar 28 Javascript
JavaScript闭包的简单应用
Sep 01 Javascript
antd-DatePicker组件获取时间值,及相关设置方式
Oct 27 Javascript
React Native 搭建开发环境的方法步骤
Oct 30 #Javascript
Bootstrap框架建立树形菜单(Tree)的实例代码
Oct 30 #Javascript
react实现一个优雅的图片占位模块组件详解
Oct 30 #Javascript
ES6 javascript中class类的get与set用法实例分析
Oct 30 #Javascript
原生JS与jQuery编写简单选项卡
Oct 30 #jQuery
简单实现jQuery弹窗效果
Oct 30 #jQuery
Bootstrap栅格系统的使用详解
Oct 30 #Javascript
You might like
php学习笔记之面向对象编程
2012/12/29 PHP
PHP反射使用实例和PHP反射API的中文说明
2014/07/02 PHP
php中的字符编码转换函数用法示例
2014/10/20 PHP
Laravel 5框架学习之表单验证
2015/04/08 PHP
Avengerls vs KG BO3 第三场2.18
2021/03/10 DOTA
JS location几个方法小姐
2008/07/09 Javascript
input、button的不同type值在ajax提交表单时导致的陷阱
2009/02/24 Javascript
firefox火狐浏览器与与ie兼容的2个问题总结
2010/07/20 Javascript
使用jquery插件实现图片延迟加载技术详细说明
2011/03/12 Javascript
jQuery LigerUI 使用教程表格篇(1)
2012/01/18 Javascript
node.js中的querystring.unescape方法使用说明
2014/12/10 Javascript
js实现鼠标感应向下滑动隐藏菜单的方法
2015/02/20 Javascript
浅析JavaScript动画
2015/06/10 Javascript
Bootstrap模态对话框的简单使用
2016/04/29 Javascript
关于JavaScript 原型链的一点个人理解
2016/07/31 Javascript
JavaScript中removeChild 方法开发示例代码
2016/08/15 Javascript
JS限制条件补全问题实例分析
2016/12/16 Javascript
JavaScript中localStorage对象存储方式实例分析
2017/01/12 Javascript
jQuery Ajax自定义分页组件(jquery.loehpagerv1.0)实例详解
2017/05/01 jQuery
js判断输入框不能为空格或null值的实现方法
2018/03/02 Javascript
快速解决Vue项目在IE浏览器中显示空白的问题
2018/09/04 Javascript
vue服务端渲染添加缓存的方法
2018/09/18 Javascript
移动端JS实现拖拽两种方法解析
2020/10/12 Javascript
[05:02][DOTA2]DOTA进化论 第一期
2013/09/27 DOTA
python基于xmlrpc实现二进制文件传输的方法
2015/06/02 Python
Python代码实现http/https代理服务器的脚本
2019/08/12 Python
Tensorflow 定义变量,函数,数值计算等名字的更新方式
2020/02/10 Python
H&M美国官网:欧洲最大的服饰零售商
2016/09/07 全球购物
英国领先的狗和宠物美容专家:Christies Direct
2017/04/03 全球购物
C#里面可以避免一个类被其他类继承么?如何?
2013/09/26 面试题
实习护理工作自我评价
2013/09/25 职场文书
优秀团干部个人事迹
2014/05/29 职场文书
授权委托书(完整版)
2014/09/10 职场文书
信访稳定工作汇报
2014/10/27 职场文书
学校食堂管理制度
2015/08/04 职场文书
Mysql数据库命令大全
2021/05/26 MySQL