Skip to content

0x020-es6中迭代器的使用3

从一个网络请求出发

如下示例,用于在nodejs中发送一个网络请求,通过callback拿到数据:

js
const https = require('https');  
  
function get(urlString, resultCallback) {  
    const url = new URL(urlString)  
    https.get(url, function (res) {  
        let body = '';  
        res.on('data', function (chunk) {  
            body += chunk;  
        });  
        res.on('end', function () {  
            resultCallback(body)  
        })  
    });  
}  
  
get('https://66ceeb4a901aab24842024e1.mockapi.io/2', res => {  
    console.log(res)  
})
const https = require('https');  
  
function get(urlString, resultCallback) {  
    const url = new URL(urlString)  
    https.get(url, function (res) {  
        let body = '';  
        res.on('data', function (chunk) {  
            body += chunk;  
        });  
        res.on('end', function () {  
            resultCallback(body)  
        })  
    });  
}  
  
get('https://66ceeb4a901aab24842024e1.mockapi.io/2', res => {  
    console.log(res)  
})

接下来我们加入迭代器的代码,尝试将其转换成‘同步’请求,以下是一个错误的示例

js
const urlString = 'https://66ceeb4a901aab24842024e1.mockapi.io/2'
function* loadGenerator(url) {
    let result = null
    yield get(url, res => {
        result = res // 这里未执行 
    })
    while (result === null) {
        yield
    }
    return result // 这里未执行
}

const iter = loadGenerator(urlString)
for (const result of iter) {
    console.log(result);
}
const urlString = 'https://66ceeb4a901aab24842024e1.mockapi.io/2'
function* loadGenerator(url) {
    let result = null
    yield get(url, res => {
        result = res // 这里未执行 
    })
    while (result === null) {
        yield
    }
    return result // 这里未执行
}

const iter = loadGenerator(urlString)
for (const result of iter) {
    console.log(result);
}

查阅了下文档,yield不支持嵌套函数:

javascript - JS: how to use generator and yield in a callback - Stack Overflow

yield - JavaScript | MDN
yield can only be used directly within the generator function that contains it. It cannot be used within nested functions.

thunk

为了处理解决这个问题,可以引入一点函数式编程的思想,并将执行权交给Generator函数:

js
const https = require('https');
function get(urlString) {
    return function (callback) {
        const url = new URL(urlString)
        https.get(url, callback);
    }
}
const urlString = 'https://66ceeb4a901aab24842024e1.mockapi.io/2'
function* loadGenerator(url) {
    // Thunk 函数,因为它可以在回调函数里,将执行权交还给 Generator 函数
    const rs = yield get(url)
    console.log("aaaa:" + rs); // 2.这里会拿到返回结果。实现了异步转同步的效果
}
const iter = loadGenerator(urlString)
const network = iter.next()
// network.value 是一个thunk函数
network.value(function (res) {
    let body = '';
    res.on('data', function (chunk) {
        body += chunk;
    });
    res.on('end', function () {
        console.log(body);
        // 它可以在回调函数里,将执行权交还给 Generator 函数
         // 1.将数据交还给Generator
        iter.next(body);
    })
})
const https = require('https');
function get(urlString) {
    return function (callback) {
        const url = new URL(urlString)
        https.get(url, callback);
    }
}
const urlString = 'https://66ceeb4a901aab24842024e1.mockapi.io/2'
function* loadGenerator(url) {
    // Thunk 函数,因为它可以在回调函数里,将执行权交还给 Generator 函数
    const rs = yield get(url)
    console.log("aaaa:" + rs); // 2.这里会拿到返回结果。实现了异步转同步的效果
}
const iter = loadGenerator(urlString)
const network = iter.next()
// network.value 是一个thunk函数
network.value(function (res) {
    let body = '';
    res.on('data', function (chunk) {
        body += chunk;
    });
    res.on('end', function () {
        console.log(body);
        // 它可以在回调函数里,将执行权交还给 Generator 函数
         // 1.将数据交还给Generator
        iter.next(body);
    })
})

这个实际上也就是thunk函数。

promise

另外一种方式是使用Promise:

js
const https = require('https');

function get(urlString) {
    return function (callback) {
        const url = new URL(urlString)
        https.get(url, callback);
    }
}

const urlString = 'https://66ceeb4a901aab24842024e1.mockapi.io/2'

function* loadGenerator(url) {
    yield new Promise((resolve, reject) => {
        get(url)(res => {
            let body = '';
            res.on('data', function (chunk) {
                body += chunk;
            });
            res.on('end', function () {
                resolve(body)
            })
        })
    })
}

const iter = loadGenerator(urlString)
iter.next().value
    .then(data => {
        console.log(data)
    })
const https = require('https');

function get(urlString) {
    return function (callback) {
        const url = new URL(urlString)
        https.get(url, callback);
    }
}

const urlString = 'https://66ceeb4a901aab24842024e1.mockapi.io/2'

function* loadGenerator(url) {
    yield new Promise((resolve, reject) => {
        get(url)(res => {
            let body = '';
            res.on('data', function (chunk) {
                body += chunk;
            });
            res.on('end', function () {
                resolve(body)
            })
        })
    })
}

const iter = loadGenerator(urlString)
iter.next().value
    .then(data => {
        console.log(data)
    })

JavaScript 语言的 Thunk 函数

JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。

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);
// 正常版本的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);

上面代码中,fs模块的readFile方法是一个多参数函数,两个参数分别为文件名和回调函数。经过转换器处理,它变成了一个单参数函数,只接受回调函数作为参数。这个单参数版本,就叫做 Thunk 函数。

任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。下面是一个简单的 Thunk 函数转换器。

js
// ES5版本
var Thunk = function(fn){
  return function (){
    var args = Array.prototype.slice.call(arguments);
    return function (callback){
      args.push(callback);
      // apply 函数:为函数依次传入this值和参数数组
      return fn.apply(this, args);
    }
  };
};

// ES6版本
const Thunk = function(fn) {
  return function (...args) {
    return function (callback) {
	  // call 函数:为函数依次传入this值和其他参数
      return fn.call(this, ...args, callback);
    }
  };
};
// ES5版本
var Thunk = function(fn){
  return function (){
    var args = Array.prototype.slice.call(arguments);
    return function (callback){
      args.push(callback);
      // apply 函数:为函数依次传入this值和参数数组
      return fn.apply(this, args);
    }
  };
};

// ES6版本
const Thunk = function(fn) {
  return function (...args) {
    return function (callback) {
	  // call 函数:为函数依次传入this值和其他参数
      return fn.call(this, ...args, callback);
    }
  };
};

使用上面的转换器,生成fs.readFile的 Thunk 函数(原函数最后一个参数需要是callback)。

js
var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);
var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);

下面是另一个完整的例子:

js
function f(a, cb) {
  cb(a);
}
const ft = Thunk(f);
ft(1)(console.log) // 1
function f(a, cb) {
  cb(a);
}
const ft = Thunk(f);
ft(1)(console.log) // 1

在生产环境中使用可以参考:thunkify

Thunk 函数的自动流程管理

Generator函数并不适合异步操作,thunk函数恰好可以解决这个问题, 我们约定callback第一个参数为err,第二个为实际返回的值:

js
function getRandomData(x, callback) {  
    setTimeout(() => {  
        callback(null /*模拟error为空*/, Math.random() * x);  
    }, 500)  
}
function getRandomData(x, callback) {  
    setTimeout(() => {  
        callback(null /*模拟error为空*/, Math.random() * x);  
    }, 500)  
}

thunkify源码:自行传入了function的值,防止重复执行

js
function thunkify(fn) {
    return function () {
        var args = new Array(arguments.length);
        var ctx = this;
        for (var i = 0; i < args.length; ++i) {
            args[i] = arguments[i];
        }
        return function (done) {
            var called;
            // 自行传入了function自动执行 
            args.push(function () { 
	            // 防止手动调用多次  
                if (called) return; 
                called = true; 
                // 执行外部传入的function 
                done.apply(null, arguments);
            });
            // args.push(done) 
            try {
                fn.apply(ctx, args);
            } catch (err) {
                done(err);
            }
        }
    }
}
function thunkify(fn) {
    return function () {
        var args = new Array(arguments.length);
        var ctx = this;
        for (var i = 0; i < args.length; ++i) {
            args[i] = arguments[i];
        }
        return function (done) {
            var called;
            // 自行传入了function自动执行 
            args.push(function () { 
	            // 防止手动调用多次  
                if (called) return; 
                called = true; 
                // 执行外部传入的function 
                done.apply(null, arguments);
            });
            // args.push(done) 
            try {
                fn.apply(ctx, args);
            } catch (err) {
                done(err);
            }
        }
    }
}

手动执行代码,观察一下实际上是一个递归的过程:

js
let tk = thunkify(getRandomData)

const gen = function* () {
    const r1 = yield tk(100);
    console.log(r1.toString());
    const r2 = yield tk(200);
    console.log(r2.toString());
    const r3 = yield tk(300);
    console.log(r3.toString());
    const r4 = yield tk(400);
    console.log(r4.toString());
};

// 手动执行实际上是对next递归
var g = gen();
var r1 = g.next();
r1.value(function (err, data) {
    if (err) throw err;
    var r2 = g.next(data);
    r2.value(function (err, data) {
        if (err) throw err;
        g.next(data);
    });
});
let tk = thunkify(getRandomData)

const gen = function* () {
    const r1 = yield tk(100);
    console.log(r1.toString());
    const r2 = yield tk(200);
    console.log(r2.toString());
    const r3 = yield tk(300);
    console.log(r3.toString());
    const r4 = yield tk(400);
    console.log(r4.toString());
};

// 手动执行实际上是对next递归
var g = gen();
var r1 = g.next();
r1.value(function (err, data) {
    if (err) throw err;
    var r2 = g.next(data);
    r2.value(function (err, data) {
        if (err) throw err;
        g.next(data);
    });
});

然后理所应当改为递归的形式:

js
function run(fn) {  
    // Generate函数初始化  
    var gen = fn();  
    // 递归执行  
    function next(err, data) {  
        // 第一次调用的时候data是undefined,参考手动执行的代码,恰好这个我们是不关心的。实现的相当巧妙  
        var result = gen.next(data);          
        if (result.done) return;  
        // 后面调用的时候,传入正常function,且为约定的格式
        result.value(next);  
    }  
    next();  
}  
run(gen);
function run(fn) {  
    // Generate函数初始化  
    var gen = fn();  
    // 递归执行  
    function next(err, data) {  
        // 第一次调用的时候data是undefined,参考手动执行的代码,恰好这个我们是不关心的。实现的相当巧妙  
        var result = gen.next(data);          
        if (result.done) return;  
        // 后面调用的时候,传入正常function,且为约定的格式
        result.value(next);  
    }  
    next();  
}  
run(gen);

co模块

Note

tj/co
Generator based control flow goodness for nodejs and the browser, using promises, letting you write non-blocking code in a nice-ish way.

co模块的可以让你不用自己编写Generator 函数的执行器。使用 co 的前提条件是,Generator 函数的yield命令后面,只能是 Thunk 函数Promise 对象。基本用法如下:

js
// 示例1:将自动执行
co(function* () {
	const r1 = yield tk(100);
    console.log(r1.toString());
    const r2 = yield tk(200);
    console.log(r2.toString());
})

// 示例2:返回promise
co(function* () {
  var result = yield Promise.resolve(true);
  return result;
}).then(function (value) {
  console.log(value);
}, function (err) {
  console.error(err.stack);
});

//示例3:返回 Promise 的常规函数
var fn = co.wrap(function* (val) {
  return yield Promise.resolve(val);
});
fn(true).then(function (val) {
});
// 示例1:将自动执行
co(function* () {
	const r1 = yield tk(100);
    console.log(r1.toString());
    const r2 = yield tk(200);
    console.log(r2.toString());
})

// 示例2:返回promise
co(function* () {
  var result = yield Promise.resolve(true);
  return result;
}).then(function (value) {
  console.log(value);
}, function (err) {
  console.error(err.stack);
});

//示例3:返回 Promise 的常规函数
var fn = co.wrap(function* (val) {
  return yield Promise.resolve(val);
});
fn(true).then(function (val) {
});

co实际上是基于 Promise 对象的自动执行器的实现,与上文的实现代码类似。