Generator 函数的自动流程管理

前言 💬

Generator 是 ES6 的最新语法,译为生成器 ,是一种异步的解决方案,一种特殊写法的函数,体内有 yield 关键字,译为产出 ,可以把 yield 理解成一个指针,分段执行 Generator 函数内部的状态

js 复制代码
function* gen() {
    yield 'Hello';
    yield 'World';
}

调用生成器 (gen) 会返回一个 迭代器 对象(g),调用 g.next() 方法可以移动 生成器 内部的指针(yield),并返回一个包含 { value: yield后的值, done: false/true } 属性的对象,done属性提示状态(yield)是否都结束了

js 复制代码
const g = gen();
g.next(); // { value: 'Hello', done: false }
g.next(); // { value: 'World', done: false }
g.next(); // { value: undefined, done: true }

当然你也可以手动 return 结束掉

js 复制代码
function* gen() {
    yield 'Hello';
    return 'bye';
    yield 'World';
}
const g = gen();
g.next(); // { value: 'Hello', done: false }
g.next(); // { value: 'bye', done: true }
g.next(); // { value: undefined, done: true }

Genarator 与 Iterator的关系 👬

我们知道所有部署了[Symbol.iterator]属性的数据类型,都可以被for of遍历

js 复制代码
const obj = {}
obj[Symbol.iterator] = function* (){
  yield 1;
  yield 2;
  yield 3;
}

console.log([...obj])
// [ 1, 2, 3 ]

for (let i of obj) {
  console.log(i)
}
// 1 2 3

yield 返回值 🤷‍

yield 表达式本身没有返回值,或者总是返回 undefined

js 复制代码
function* gen() {
  const x = yield 1;
  console.log('x:', x)
}

const g = gen();
console.log('out:', g.next()) // out: { value: 1, done: false }
console.log('out2:', g.next())

// x: undefined
// out2: { value: undefined, done: true }

可以看到,当打印out2:的时候,x:undefined

迭代器 向 ➡️ 生成器 通信

还是上面的代码,只在第二次 g.next(2)中传了一个 2

js 复制代码
function* gen() {
  const x = yield 1;
  console.log('x:', x)
}

const g = gen();
console.log('out:', g.next())
console.log('out2:', g.next(2)) //<---
// out: { value: 1, done: false }
// x: 2
// out2: { value: undefined, done: true }

可以发现 迭代器 通过 .next()方法成功的把值,传递进了 生成器 内部上一阶段(yield)的返回值

回顾异步的历史

回调函数♻️

js 复制代码
step1(function (value1) {
    step2(value1, function(value2) {
        step3(value2, function(value3) {
            step4(value3, function(value4) {
                // Do something with value4
            });
        });
    });
});

这种持续向右➡️ 延伸的恐惧感就是回调地狱

Promise👌

js 复制代码
Promise.resolve(step1)
  .then(step2)
  .then(step3)
  .then(step4)
  .then(function (value4) {
    // Do something with value4
  }, function (error) {
    // Handle any error from step1 through step4
  })
  .done();

Promise链式调用向右的方向 优化成了向下的直线形式 ,但是代码冗余,一眼看上去全是 then

Generator 异步操作同步化表达 🙋

这种干净清晰的书写方式,像极了顺序执行的同步代码

js 复制代码
function* longTask(value1) {
  try {
    var value2 = yield step1(value1);
    var value3 = yield step2(value2);
    var value4 = yield step3(value3);
    var value5 = yield step4(value4);
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
}

借助一个方法让它自动运行

js 复制代码
function scheduler(task) {
  var taskObj = task.next(task.value);
  // 如果Generator函数未结束,就继续调用
  if (!taskObj.done) {
    task.value = taskObj.value
    scheduler(task);
  }
}

scheduler(longTask(initialValue));
  1. longTask() 在传入scheduler的时候已经调用并生成了task迭代器
  2. initialValue 会当作 value1 传递给 step1(value1)
  3. 外部 var taskObj 接收了 step1(value1) 任务的返回值
  4. taskObj.done 属性判断任务还没有结束(继续)
  5. 把第 1 次任务的结果 (task.value),进入调度函数 scheduler, 赋值给 var value2,同时把 var value2 当作第 2 次任务的参数传入 step2(value2)
  6. 循环往复,直到所有任务完结 task.done:true

以上只适合同步操作

这种做法只适合同步操作,即所有的任务步骤都是同步的,没有异步操作。这是因为调度器函数 scheduler 在执行任务步骤时,没有考虑异步操作的完成时间。

在同步操作中,每个步骤的执行是顺序的,每个步骤的结果都会立即返回,然后传递给下一个步骤。调度器函数 scheduler 在每个步骤完成后,通过调用 task.next() 继续执行下一个步骤。

然而,在存在异步操作的情况下,调度器函数 scheduler 的简单递归调用方式就无法正确处理异步操作的完成时间。异步操作的完成时间是不确定的,可能需要一段时间才能得到结果。而简单的递归调用方式会立即执行下一个步骤,而不等待异步操作完成。

Generator 函数的异步应用

Js 是单线程的,大部分任务都是采取异步的方式,如果 Generator 只能玩同步,那... 好像没啥意思

回顾 - 读取文件的异步操作

1. 回调函数

先看下 nodefs 模块异步读取文件的操作

  • 参数1: 你要读取的文件在哪里(文件路径
  • 参数2: 读取的回调结果(是成功还是失败
js 复制代码
fs.readFile('/etc/passwd', function (err, data) {
  if (err) throw err;
  console.log(data);
});

🙅:回调地狱

2. Promise

js 复制代码
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);
});

🙅‍♂️: 代码冗余,一眼看去都是一堆then

Thunk

Thunk 函数 是自动执行 Generator 函数的一种方法,可以理解成一种 高阶函数

Thunk 函数多参数函数 ,替换成一个只接受回调函数作为参数的单参数函数 ( 是不是嗅到了一丝 柯里化 的味道~ )

Thunk 函数 可以用于 Generator 函数的自动流程管理

js 复制代码
function a (b, c) {
    return b + c;
}

function thunk (b) {
    return function(c) {
        return b + c;
    }
}

thunk(b)(c);

逐渐演化到应用场景

js 复制代码
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);

Thunkify 模块 📦

推荐一个包 thunkify ,使用方式如下

js 复制代码
var thunkify = require('thunkify');
var fs = require('fs');

var readFileThunk = thunkify(fs.readFile);
readFileThunk('package.json')(function(err, str){
  // ...
});

Thunk 函数的自动流程管理

Thunk 函数真正的威力是,可以自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 Generator 执行器

js 复制代码
var thunkify = require('thunkify');

var readFileThunk = thunkify(fs.readFile);

function* g() {
  var f1 = yield readFileThunk('fileA');
  var f2 = yield readFileThunk('fileB');
  // ...
  var fn = yield readFileThunk('fileN');
}

function run(fn) {
  var gen = fn();
  
  function next(err, data) {
    var result = gen.next(data);
    if (result.done) return;
    result.value(next);
  }

  next();
}

run(g);

请细细的品这部分代码,result.value(next) 中的 next()本身就是异步操作的 回调函数

加之,thunkify(fs.readFile)fs.readFileThunk化,

所以每次 readFileThunk('fileN') 执行后的返回值,是一个函数,就是 result.value, 准备 接收第2个参数 , 那个回调函数

相关推荐
余生H13 分钟前
前端Python应用指南(二)深入Flask:理解Flask的应用结构与模块化设计
前端·后端·python·flask·全栈
outstanding木槿18 分钟前
JS中for循环里的ajax请求不数据
前端·javascript·react.js·ajax
酥饼~25 分钟前
html固定头和第一列简单例子
前端·javascript·html
一只不会编程的猫28 分钟前
高德地图自定义折线矢量图形
前端·vue.js·vue
所以经济危机就是没有新技术拉动增长了28 分钟前
二、javascript的进阶知识
开发语言·javascript·ecmascript
m0_7482509330 分钟前
html 通用错误页面
前端·html
来吧~39 分钟前
vue3使用video-player实现视频播放(可拖动视频窗口、调整大小)
前端·vue.js·音视频
Bubluu39 分钟前
浏览器点击视频裁剪当前帧,然后粘贴到页面
开发语言·javascript·音视频
鎈卟誃筅甡1 小时前
Vuex 的使用和原理详解
前端·javascript
呆呆小雅1 小时前
二、创建第一个VUE项目
前端·javascript·vue.js