从一个网络请求出发
如下示例,用于在nodejs中发送一个网络请求,通过callback拿到数据:
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)
})
接下来我们加入迭代器的代码,尝试将其转换成‘同步’请求,以下是一个错误的示例:
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函数:
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:
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 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
// 正常版本的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 函数转换器。
// 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)。
var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);
var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);
下面是另一个完整的例子:
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,第二个为实际返回的值:
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的值,防止重复执行
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);
}
}
}
}
手动执行代码,观察一下实际上是一个递归的过程:
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);
});
});
然后理所应当改为递归的形式:
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 对象。基本用法如下:
// 示例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 对象的自动执行器的实现,与上文的实现代码类似。