ES6 系列之 Generator 的自动执行的方法示例


Posted in Javascript onOctober 19, 2018

单个异步任务

var fetch = require('node-fetch');

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

为了获得最终的执行结果,你需要这样做:

var g = gen();
var result = g.next();

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

首先执行 Generator 函数,获取遍历器对象。

然后使用 next 方法,执行异步任务的第一阶段,即 fetch(url)。

注意,由于 fetch(url) 会返回一个 Promise 对象,所以 result 的值为:

{ value: Promise { <pending> }, done: false }

最后我们为这个 Promise 对象添加一个 then 方法,先将其返回的数据格式化(data.json()),再调用 g.next,将获得的数据传进去,由此可以执行异步任务的第二阶段,代码执行完毕。

多个异步任务

上节我们只调用了一个接口,那如果我们调用了多个接口,使用了多个 yield,我们岂不是要在 then 函数中不断的嵌套下去……

所以我们来看看执行多个异步任务的情况:

var fetch = require('node-fetch');

function* gen() {
  var r1 = yield fetch('https://api.github.com/users/github');
  var r2 = yield fetch('https://api.github.com/users/github/followers');
  var r3 = yield fetch('https://api.github.com/users/github/repos');

  console.log([r1.bio, r2[0].login, r3[0].full_name].join('\n'));
}

为了获得最终的执行结果,你可能要写成:

var g = gen();
var result1 = g.next();

result1.value.then(function(data){
  return data.json();
})
.then(function(data){
  return g.next(data).value;
})
.then(function(data){
  return data.json();
})
.then(function(data){
  return g.next(data).value
})
.then(function(data){
  return data.json();
})
.then(function(data){
  g.next(data)
});

但我知道你肯定不想写成这样……

其实,利用递归,我们可以这样写:

function run(gen) {
  var g = gen();

  function next(data) {
    var result = g.next(data);

    if (result.done) return;

    result.value.then(function(data) {
      return data.json();
    }).then(function(data) {
      next(data);
    });

  }

  next();
}

run(gen);

其中的关键就是 yield 的时候返回一个 Promise 对象,给这个 Promise 对象添加 then 方法,当异步操作成功时执行 then 中的 onFullfilled 函数,onFullfilled 函数中又去执行 g.next,从而让 Generator 继续执行,然后再返回一个 Promise,再在成功时执行 g.next,然后再返回……

启动器函数

在 run 这个启动器函数中,我们在 then 函数中将数据格式化 data.json(),但在更广泛的情况下,比如 yield 直接跟一个 Promise,而非一个 fetch 函数返回的 Promise,因为没有 json 方法,代码就会报错。所以为了更具备通用性,连同这个例子和启动器,我们修改为:

var fetch = require('node-fetch');

function* gen() {
  var r1 = yield fetch('https://api.github.com/users/github');
  var json1 = yield r1.json();
  var r2 = yield fetch('https://api.github.com/users/github/followers');
  var json2 = yield r2.json();
  var r3 = yield fetch('https://api.github.com/users/github/repos');
  var json3 = yield r3.json();

  console.log([json1.bio, json2[0].login, json3[0].full_name].join('\n'));
}

function run(gen) {
  var g = gen();

  function next(data) {
    var result = g.next(data);

    if (result.done) return;

    result.value.then(function(data) {
      next(data);
    });

  }

  next();
}

run(gen);

只要 yield 后跟着一个 Promise 对象,我们就可以利用这个 run 函数将 Generator 函数自动执行。

回调函数

yield 后一定要跟着一个 Promise 对象才能保证 Generator 的自动执行吗?如果只是一个回调函数呢?我们来看个例子:

首先我们来模拟一个普通的异步请求:

function fetchData(url, cb) {
  setTimeout(function(){
    cb({status: 200, data: url})
  }, 1000)
}

我们将这种函数改造成:

function fetchData(url) {
  return function(cb){
    setTimeout(function(){
      cb({status: 200, data: url})
    }, 1000)
  }
}

对于这样的 Generator 函数:

function* gen() {
  var r1 = yield fetchData('https://api.github.com/users/github');
  var r2 = yield fetchData('https://api.github.com/users/github/followers');

  console.log([r1.data, r2.data].join('\n'));
}

如果要获得最终的结果:

var g = gen();

var r1 = g.next();

r1.value(function(data) {
  var r2 = g.next(data);
  r2.value(function(data) {
    g.next(data);
  });
});

如果写成这样的话,我们会面临跟第一节同样的问题,那就是当使用多个 yield 时,代码会循环嵌套起来……

同样利用递归,所以我们可以将其改造为:

function run(gen) {
  var g = gen();

  function next(data) {
    var result = g.next(data);

    if (result.done) return;

    result.value(next);
  }

  next();
}

run(gen);

run

由此可以看到 Generator 函数的自动执行需要一种机制,即当异步操作有了结果,能够自动交回执行权。

而两种方法可以做到这一点。

(1)回调函数。将异步操作进行包装,暴露出回调函数,在回调函数里面交回执行权。

(2)Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。

在两种方法中,我们各写了一个 run 启动器函数,那我们能不能将这两种方式结合在一些,写一个通用的 run 函数呢?我们尝试一下:

// 第一版
function run(gen) {
  var gen = gen();

  function next(data) {
    var result = gen.next(data);
    if (result.done) return;

    if (isPromise(result.value)) {
      result.value.then(function(data) {
        next(data);
      });
    } else {
      result.value(next)
    }
  }

  next()
}

function isPromise(obj) {
  return 'function' == typeof obj.then;
}

module.exports = run;

其实实现的很简单,判断 result.value 是否是 Promise,是就添加 then 函数,不是就直接执行。

return Promise

我们已经写了一个不错的启动器函数,支持 yield 后跟回调函数或者 Promise 对象。

现在有一个问题需要思考,就是我们如何获得 Generator 函数的返回值呢?又如果 Generator 函数中出现了错误,就比如 fetch 了一个不存在的接口,这个错误该如何捕获呢?

这很容易让人想到 Promise,如果这个启动器函数返回一个 Promise,我们就可以给这个 Promise 对象添加 then 函数,当所有的异步操作执行成功后,我们执行 onFullfilled 函数,如果有任何失败,就执行 onRejected 函数。

我们写一版:

// 第二版
function run(gen) {
  var gen = gen();

  return new Promise(function(resolve, reject) {

    function next(data) {
      try {
        var result = gen.next(data);
      } catch (e) {
        return reject(e);
      }

      if (result.done) {
        return resolve(result.value)
      };

      var value = toPromise(result.value);

      value.then(function(data) {
        next(data);
      }, function(e) {
        reject(e)
      });
    }

    next()
  })

}

function isPromise(obj) {
  return 'function' == typeof obj.then;
}

function toPromise(obj) {
  if (isPromise(obj)) return obj;
  if ('function' == typeof obj) return thunkToPromise(obj);
  return obj;
}

function thunkToPromise(fn) {
  return new Promise(function(resolve, reject) {
    fn(function(err, res) {
      if (err) return reject(err);
      resolve(res);
    });
  });
}

module.exports = run;

与第一版有很大的不同:

首先,我们返回了一个 Promise,当 result.done 为 true 的时候,我们将该值 resolve(result.value),如果执行的过程中出现错误,被 catch 住,我们会将原因 reject(e)。

其次,我们会使用 thunkToPromise 将回调函数包装成一个 Promise,然后统一的添加 then 函数。在这里值得注意的是,在 thunkToPromise 函数中,我们遵循了 error first 的原则,这意味着当我们处理回调函数的情况时:

// 模拟数据请求
function fetchData(url) {
  return function(cb) {
    setTimeout(function() {
      cb(null, { status: 200, data: url })
    }, 1000)
  }
}

在成功时,第一个参数应该返回 null,表示没有错误原因。

优化

我们在第二版的基础上将代码写的更加简洁优雅一点,最终的代码如下:

// 第三版
function run(gen) {

  return new Promise(function(resolve, reject) {
    if (typeof gen == 'function') gen = gen();

    // 如果 gen 不是一个迭代器
    if (!gen || typeof gen.next !== 'function') return resolve(gen)

    onFulfilled();

    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    function next(ret) {
      if (ret.done) return resolve(ret.value);
      var value = toPromise(ret.value);
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      return onRejected(new TypeError('You may only yield a function, promise ' +
        'but the following object was passed: "' + String(ret.value) + '"'));
    }
  })
}

function isPromise(obj) {
  return 'function' == typeof obj.then;
}

function toPromise(obj) {
  if (isPromise(obj)) return obj;
  if ('function' == typeof obj) return thunkToPromise(obj);
  return obj;
}

function thunkToPromise(fn) {
  return new Promise(function(resolve, reject) {
    fn(function(err, res) {
      if (err) return reject(err);
      resolve(res);
    });
  });
}

module.exports = run;

co

如果我们再将这个启动器函数写的完善一些,我们就相当于写了一个 co,实际上,上面的代码确实是来自于 co……

而 co 是什么? co 是大神 TJ Holowaychuk 于 2013 年 6 月发布的一个小模块,用于 Generator 函数的自动执行。

如果直接使用 co 模块,这两种不同的例子可以简写为:

// yield 后是一个 Promise
var fetch = require('node-fetch');
var co = require('co');

function* gen() {
  var r1 = yield fetch('https://api.github.com/users/github');
  var json1 = yield r1.json();
  var r2 = yield fetch('https://api.github.com/users/github/followers');
  var json2 = yield r2.json();
  var r3 = yield fetch('https://api.github.com/users/github/repos');
  var json3 = yield r3.json();

  console.log([json1.bio, json2[0].login, json3[0].full_name].join('\n'));
}

co(gen);
// yield 后是一个回调函数
var co = require('co');

function fetchData(url) {
  return function(cb) {
    setTimeout(function() {
      cb(null, { status: 200, data: url })
    }, 1000)
  }
}

function* gen() {
  var r1 = yield fetchData('https://api.github.com/users/github');
  var r2 = yield fetchData('https://api.github.com/users/github/followers');

  console.log([r1.data, r2.data].join('\n'));
}

co(gen);

是不是特别的好用?

ES6 系列

ES6 系列目录地址:https://github.com/mqyqingfeng/Blog

ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Jquery选中或取消radio示例
Sep 29 Javascript
jQuery性能优化的38个建议
Mar 04 Javascript
javascript中2个感叹号的用法实例详解
Sep 04 Javascript
JavaScript使用setInterval()函数实现简单轮询操作的方法
Feb 02 Javascript
jQuery+CSS3实现3D立方体旋转效果
Nov 10 Javascript
js无法获取到html标签的属性的解决方法
Jul 26 Javascript
微信小程序 wx.request方法的异步封装实例详解
May 18 Javascript
JS全角与半角转化实例(分享)
Jul 04 Javascript
vue中计算属性(computed)、methods和watched之间的区别
Jul 27 Javascript
JavaScript键盘事件常见用法实例分析
Jan 03 Javascript
跟混乱的页面弹窗说再见
Apr 11 Javascript
重学JS之显示强制类型转换详解
Jun 30 Javascript
js中自定义react数据验证组件实例详解
Oct 19 #Javascript
值得收藏的八个常用的js正则表达式
Oct 19 #Javascript
深入理解移动前端开发之viewport
Oct 19 #Javascript
vue移动端html5页面根据屏幕适配的四种解决方法
Oct 19 #Javascript
Vue用v-for给循环标签自身属性添加属性值的方法
Oct 18 #Javascript
vue中v-for循环给标签属性赋值的方法
Oct 18 #Javascript
webstorm+vue初始化项目的方法
Oct 18 #Javascript
You might like
为什么《星际争霸》是测试人工智能的理想战场
2019/12/03 星际争霸
用windows下编译过的eAccelerator for PHP 5.1.6实现php加速的使用方法
2007/09/30 PHP
PHP遍历目录函数opendir()、readdir()、closedir()、rewinddir()总结
2014/11/18 PHP
Yii2.0 RESTful API 基础配置教程详解
2018/12/26 PHP
jquery中的sortable排序之后的保存状态的解决方法
2010/01/28 Javascript
控制页面按钮在后台执行期间不重复提交的JS方法
2013/06/24 Javascript
javascript ready和load事件的区别示例介绍
2013/08/30 Javascript
jquery获取多个checkbox的值异步提交给php
2015/07/07 Javascript
jQuery Easyui DataGrid点击某个单元格即进入编辑状态焦点移开后保存数据
2016/08/15 Javascript
jQGrid动态填充select下拉框的选项值(动态填充)
2016/11/28 Javascript
详解springmvc 接收json对象的两种方式
2016/12/06 Javascript
Nodejs搭建wss服务器教程
2017/05/24 NodeJs
ExtJs异步无法向外传值和赋值的完美解决办法
2017/06/14 Javascript
nodejs对express中next函数的一些理解
2017/09/08 NodeJs
Vue中使用vee-validate表单验证的方法
2018/05/09 Javascript
详解extract-text-webpack-plugin 的使用及安装
2018/06/12 Javascript
ionic2.0双击返回键退出应用
2019/09/17 Javascript
js模拟F11页面全屏显示
2019/09/17 Javascript
layui table 获取分页 limit的方法
2019/09/20 Javascript
如何在VUE中使用vue-awesome-swiper
2021/01/04 Vue.js
mapboxgl实现带箭头轨迹线的代码
2021/01/04 Javascript
[45:56]Ti4正赛第一天 VG vs NEWBEE 3
2014/07/19 DOTA
Python wxPython库使用wx.ListBox创建列表框示例
2018/09/03 Python
python中时间、日期、时间戳的转换的实现方法
2019/07/06 Python
Django使用 Bootstrap 样式修改书籍列表过程解析
2019/08/09 Python
在django模板中实现超链接配置
2019/08/21 Python
python 写函数在一定条件下需要调用自身时的写法说明
2020/06/01 Python
匡威帆布鞋美国官网:Converse美国
2016/08/22 全球购物
英国第一豪华护肤品牌:Elemis
2017/10/12 全球购物
nohup的用法
2014/08/10 面试题
幼儿园家长会邀请函
2014/01/15 职场文书
大学生党员学习焦裕禄精神思想汇报
2014/09/10 职场文书
群众路线个人剖析材料及整改措施
2014/11/04 职场文书
保险公司反洗钱宣传活动总结
2015/05/08 职场文书
Vue + iView实现Excel上传功能的完整代码
2021/06/22 Vue.js
处理canvas绘制图片模糊问题
2022/05/11 Javascript