问题描述
在开发过程中,遇到一个需求:在系统初始化时通过http获取一个第三方服务器端的列表,第三方服务器提供了一个接口,可通过分页形式获取列表。
这里有两个问题:
- 未知的列表数量。就算已知总数量,如果数据量巨大,也不应该一次获取全部信息。
- 在node.js中,http是异步的。
假设该接口为:
GET http://www.example.com/api/list
QUERY page=PAGE_INDEX&size=PAGE_SIZE
RETURN {data: [DATA,...], page: {page: PAGE_INDEX, size: PAGE_SIZE}}
期望结果:
所有的数据
[DATA, ...]
目的
借以该问题,了解关于递归的更多知识。
解决方案
-
callback + 辅助变量
let request = require('superagent') let api = 'http://www.example.com/api/list' function getList (page, size, data = [], cb) { request(api).query({page, size}) .then(({body}) => { if (body.data && body.data.length) { // getList(page++, size, data.concat(body.data), cb) getList(++page, size, data.concat(body.data), cb) } else { cb(data) } }) } getList(1, 15, [], (data) => { console.log(data) })
还是能够理解,不断请求,并把获得的数据传到下一个请求;直到返回数据为空时终止,并返回累加的数据。就算不用promise而是普通回答(如使用end方法)也是可以的,可以不依赖函数返回值。但是需要一个没有意义的变量。
-
promise
let request = require('superagent') let api = 'http://www.example.com/api/list' function getList (page, size) { return request(api).query({page, size}) .then(({body}) => { if (body.data && body.data.length) { return getList(++page, size) .then((data) => { return body.data.concat(data) }) } else { return [] } }) } getList(1, 15).then((data) => { console.log(data) })
相比较上面的方法,不在需要辅助变量和回调函数,看起来更像普通的递归。但是由于promise中不断return,使得代码难以理解,且容易忽略掉数据为空时else判断。
-
generater
let request = require('superagent') let co = require('co') let api = 'http://www.example.com/api/list' function* getList (page, size) { let {body} = yield request(api).query({page, size}) if (body.data && body.data.length) { return body.data.concat(yield getList(++page, size)) } else { return [] } } co(function *() { let data = yield getList(1, 100) console.log(data.length); })
同步方式的代码,更加方便阅读和理解。但是复杂的generater函数需要使用相应的环境才能运行,如co,最不喜欢的就是在数组的一些遍历方法中不能使用。
async + await 和generater差不多,也需要相应的环境,但是可以在数组的遍历方法中使用。
思考
对于一下这句话,我总是抱着怀疑的,因为很多时候我会很快想到使用递归可以解决,而不会想到使用循环来解决。在JavaScript中,那些异步的递归函数怎么才能转换为循环呢?
递归和循环两者完全可以互换
在一次面试中,面试官给我描述了这么一个需求:
一个函数期望通过输入两个数,返回一个数组,且不能使用循环。第一个参数x表示结果数组中元素,第二个参数n代表数组长度。
我第一反应就是使用递归,或者使用数组的遍历方法。说真的,我真的没有那么一丢丢的想到循环,然而循环貌似最应该容易想到...。
// 循环
function repeat (x, n) {
let arr = []
while (arr.length < n) {
arr.push(x)
}
return arr
}
// 递归
function repeat (x, n) {
return n > 0 ? [x].concat(repeat(x, --n)) : []
}
// 数组
new Array(x).fill(n)
那么像上面那个请求的异步递归,如果没有ECMScript 2015(使用回调),该怎么写成循环呢?
经典广告词:想不出来
最后复制一段别人总结
如果你看到这里了,文中如有错误,还请指出。关于最后的问题,我也希望得到你的帮助。
留着有时间再来弄懂,首先得知道有什么,然后才是为什么?什么都不知道哪来那么多为什么呢。上次面试一堆相关问题,一脸懵逼,最后挂了,这是在不断提醒应该去弄清楚他啊。
递归算法:
优点:代码简洁、清晰,并且容易验证正确性。(如果你真的理解了算法的话,否则你更晕)
缺点:它的运行需要较多次数的函数调用,如果调用层数比较深,需要增加额外的堆栈处理(还有可能出现堆栈溢出的情况),比如参数传递需要压栈等操作,会对执行效率有一定影响。但是,对于某些问题,如果不使用递归,那将是极端难看的代码。
循环算法:
优点:速度快,结构简单。
缺点:并不能解决所有的问题。有的问题适合使用递归而不是循环。如果使用循环并不困难的话,最好使用循环。
递归算法和循环算法总结:
- 一般递归调用可以处理的算法,也通过循环去解决常需要额外的低效处理。
- 现在的编译器在优化后,对于多次调用的函数处理会有非常好的效率优化,效率未必低于循环。
- 递归和循环两者完全可以互换。如果用到递归的地方可以很方便使用循环替换,而不影响程序的阅读,那么替换成递归往往是好的。(例如:求阶乘的递归实现与循环实现。)