node.js里由于回调函数层层嵌套,使用变量似乎是一件要很谨慎的事? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
robhsiao
V2EX    Node.js

node.js里由于回调函数层层嵌套,使用变量似乎是一件要很谨慎的事?

  •  
  •   robhsiao 2012-12-04 11:52:03 +08:00 13862 次点击
    这是一个创建于 4734 天前的主题,其中的信息可能已经有所发展或是发生改变。
    初次用expressjs写了点小东西,窃以为变量作用域名是node.js(或者应该是Javascript) ugly的一个地方。

    基本每一个模块需要占用一个全局变量,而且由于回调函数里可能需要使用父级函数的局部变量,所以回调函数嵌套几层之后,感觉使用每一个变量要非常谨慎,每一个变量都是一个炸弹。


    请问大家是如何规避这个问题?依靠命名规范?或者是我用法是不正确?
    26 条回复    1970-01-01 08:00:00 +08:00
    luin
        1
    luin  
       2012-12-04 12:43:08 +08:00
    回调函数执行完会自动释放外层局部变量。
    hidden
        2
    hidden  
       2012-12-04 12:46:05 +08:00
    如果没有作用域照样存在这个这个问题,如果限制了作用域,访问callback外面的变量麻烦。用熟了表示没什么压力。
    robhsiao
        3
    robhsiao  
    OP
       2012-12-04 13:00:22 +08:00
    @luin 倒不是担心会内存的泄露,主要是可能会引用错误的变量,或者无意中篡改到外部变量。
    robhsiao
        4
    robhsiao  
    OP
       2012-12-04 13:06:35 +08:00
    @hidden
    明白。可能是表达上有点错误,并不是说变量作用域造成的问题,而是想了解一下大家有没有一些 best practices之类的来解决这样的问题.. ^^
    hyq
        5
    hyq  
       
    @robhsiao 用var关键字声明变量,要养成这个好习惯
    var x = 1;
    function(){
    var x = 2;
    console.log("inner x = " + x);
    }()
    console.log("outer x = " + x);
    robhsiao
        6
    robhsiao  
    OP
       2012-12-04 13:20:19 +08:00
    @hyq
    这个也有了解,但是您知道,node.js 里边处处是异步,所有的都要通过callback,父级函数里边定义的变量,子函数内不能把它盖掉,因此孙函数可能还需要用它。

    这样就造成每个变量命名都要全局是唯一。
    hyq
        7
    hyq  
       2012-12-04 13:31:43 +08:00
    那么你可以用一个变量来保存作用域,如
    function fun(){
    var _scopeFun = this;
    var a = 1;
    function(){
    var a = 2;
    console.log("inner a = ", a);
    console.log("outer a = ", _scopeFun.a);
    }()
    }
    hyq
        8
    hyq  
       2012-12-04 13:34:17 +08:00
    @robhsiao 我觉得这应该是函数式编程都有的问题,如果用面向过程或者面向对象的思维去写Javascript,都是不太合适的.
    BOYPT
        9
    BOYPT  
       2012-12-04 14:01:29 +08:00
    所以coffeescript就是了解js的些尬而定好的一系列而出的。
    robhsiao
        10
    robhsiao  
    OP
       2012-12-04 14:15:50 +08:00
    @BOYPT 原来如此,我还以为是我打开的方式不对..

    当然,也有可能是如 @hyq 所说 思维定式造成..

    看来得试试coffeescript去
    hidden
        11
    hidden  
       2012-12-04 16:45:15 +08:00
    个人使用倒是不麻烦,覆盖了全局变量,并且后面还要用到这个变量的概率是很小的。 即使用到了,后面来修改也不麻烦。
    robhsiao
        12
    robhsiao  
    OP
       2012-12-04 17:09:02 +08:00
    @hidden

    最常见的就是... request、response这样的变量

    最外层的http server callback会有一组这样的变量,里边的callback可能在连memcache、或者使用http.request请求其它API服务时都可能有一个request一个response,而这些callback最终要生成响应吐给客户端,所以最外层的response不能覆盖。

    ---
    也不知道大家是不是清楚我在说什么

    hidden
        13
    hidden  
       2012-12-04 18:20:18 +08:00
    遇到过啊。 这个名字可以变来变去的用。 Request, request, req, res... 全局一个request,里面实例一个req。 套一个callback你可以取出你下面要用的具体数据例: var ip = req.socket.remoteAddress; 在里面就放心覆盖吧。 也就最多套两个callback。 再套的话你就得考虑拧一个函数出来了。 反正蛮灵活的。
    jackyz
        14
    jackyz  
       2012-12-04 19:53:28 +08:00   3
    >> 初次用 expressjs 写了点小东西,窃以为变量作用域名是node.js(或者应该是Javascript) ugly 的一个地方。

    用了很长时间的 node.js 了,真没觉得。

    >> 基本每一个模块需要占用一个全局变量,

    类似

    var m = require('./my_module');

    你可以在 node 命令行里 require 你自己的代码,然后输入 m. 按 tab 试试看。全局的可以直接按 tab 看到,或者 global. tab 查看得到。这会让你对 exports 有个感性认识。你需要了解的是 node.js 的 module 机制。参考下面的代码, module 的概念与之类似:

    (funciton(){ var a = 1; .... return {x:a}; })();

    这个叫啥来着,立即执行的匿名函数?闭包?反正就是这个东西了。

    你的 module 对外可见的,只是你 exports.fun = xxx 的部分,其余的一律不可见(我认为,这种机制比起 public private 什么的,至少是一样的强大)。全局变量的问题,基本不用担心。

    >> 而且由于回调函数里可能需要使用父级函数的局部变量,所以回调函数嵌套几层之后,感觉使用每一个变量要非常谨慎,每一个变量都是一个炸弹。

    ...
    function some_fun(req,res,next){
    redis.incr('next_id', function(e,r){
    if(e) return next(e);
    redis.set('key', object, function(e,r){
    if(e) return next(e);
    res.send([1,'ok']);
    }
    }
    }
    ...

    类似这样的回调层次和作用域(不同层次都有 e 和 r 存在,这里,因为有 var shadow 机制,你可以很放心地取相同的名字),没什么可担心的。

    回调使用外层的变量,这是 Javascript 提供的语法机制,但如果回调里很大量地在使用外层变量,那就有可能是 bad smell 了,这很微妙,但不复杂,这种情况一般都可以很简单地重构为传参的形式。

    >> 请问大家是如何规避这个问题?依靠命名规范?或者是我用法是不正确?

    我认为你可能是还没有“习惯”。

    btw. 个人建议慎用 coffee script 既然 plain Javascript 能解决所有的问题,那就没有必要引入实质上是同一种语言(coffe script IS Javascript)的另外一套语法。有个老外写过一篇,处处不记得了,转载在这里 http://cliclip.com/#clip/7/521
    linlinqi
        15
    linlinqi  
       2012-12-04 20:13:56 +08:00
    解决连环嵌套比较好的方法就是使用promise模式编程了,见这里的解释:

    http://www.infoq.com/cn/news/2011/09/js-promise

    node.js能用上的promise库有哪些呢?

    http://stackoverflow.com/questions/7588581/what-nodejs-library-is-most-like-jquerys-deferreds
    BOYPT
        16
    BOYPT  
       2012-12-05 11:05:44 +08:00
    js不是完美的语言。

    作为一个语言,容易让人混淆困惑的地方都应该认为是语言的缺陷;当然这些可以通过提倡一种“xxxx规范”,在某情况下本来可以有多种approach,但根据规范仅限定仅使用一种,等等。 看C++就是了,什么规范的书比基本语法书要厚几倍。

    Coffeescript从另外一个角度来解决这个问题。有个说法是,计算机科学里面的任意问题都可以通过一层封装解决,Coffeescript就是这么一层封装。那堆麻烦的plain Javascript caveat,看看知道就好了,实在不想时刻惦记着。
    jackyz
        17
    jackyz  
       2012-12-05 12:14:38 +08:00
    @linlinqi 借机与楼上探讨。

    n 层嵌套确实很难看,但我很怀疑 promise 或者 async 这类的解决方案是否真的有效。我也曾经尝试过一些 module 感觉对代码表达的限制很大。这类东西基本都是语法糖,而且是 for contrl flow 的语法糖,但 control flow 其实是一段程序的精髓,是很灵活的东西,对于这个东西的抽象常常让人产生“还不如退回去的感想”。

    举例说明

    function some_fun(req,res,next){
    --redis.incr('next_id', function(e,r){
    ----if(e) return next(e);
    ----var object = {id:r, now:Date.now()};
    ----redis.set('key', object, function(e,r){
    ------if(e) return next(e);
    ------res.send([1,'ok']);
    ----}
    --}
    }

    贴代码没有缩进,就用-符号代替 space 了。

    通常来说,似乎可以用 serial 来理解这里的两层嵌套,但,如果考虑错误处理,问题就很复杂了。比如,if(e)return next(e); 这一句 return 则可以避免内层的 redis 语句执行,是有性能意义的代码。这里的处理是极度简化之后的情况,实际情况比这可能要复杂很多。

    我要表达的意思是,如果以通用的 promise 库来做 serial 之类的流程抽象,似乎无法准确的表达这里的精微之处。

    @linlinqi 对此如何取舍?
    linlinqi
        18
    linlinqi  
       2012-12-05 13:29:19 +08:00
    @jackyz 我试着用promise模式写一下你这个例子

    https://gist.github.com/4212565

    Deferreds是可以嵌套的,这里我就没细写了。用done和fail可以表现出这些流程,习惯之后会好用很多。
    linlinqi
        19
    linlinqi  
       2012-12-05 13:30:24 +08:00
    测试直接插入gist

    <script src="https://gist.github.com/4212565.js"> </script>
    zhangxiao
        20
    zhangxiao  
       2012-12-05 14:53:04 +08:00
    @linlinqi 不要用https,用http就可以嵌入gist了
    jackyz
        21
    jackyz  
       2012-12-05 15:38:07 +08:00
    @linlinqi

    谢谢重构,两个感觉不太适应的地方:

    1. 10 行变成 23 行 ,没感觉在哪里变得更清晰了呢?
    2. 从 node 的 callback(e,r) 风格转变为 resolve -> done reject -> fail 风格。

    另外,如果再加一层回调呢,会变成什么样子?在实际应用中有个 5,6 层的回调不稀奇呀。

    那个层次还需要再包装 resolve 和 reject 还是可以“重用” promise 又或者怎样?各个层次的异常如果要有不同的处理代码,要怎么表达呢?是:
    fun1().done().fail().fun2().done().fail()
    还是:
    fun().done( fun2().done().fail() ).fail()
    又或者还是怎样?

    之前的尝试,进行到这里就退回去了(感觉没啥区别呀,而且还要分别包装 e 和 r ),没准我是那个关节没相通?
    luin
        22
    luin  
       2012-12-05 15:59:35 +08:00
    @jackyz 你觉得使用async如何呢?
    http://gist.github.com/4213629
    linlinqi
        23
    linlinqi  
       2012-12-05 17:33:24 +08:00
    @jackyz 假设redis这个对象已经是基于Deferred模式封装良好的话,那么写的好看一些会是这样

    http://gist.github.com/4212565.js?file=ahhhh.js

    这样的行数是不是满足了?
    jackyz
        24
    jackyz  
       2012-12-05 19:20:05 +08:00
    嵌套层次太多确实难看。要追求漂亮,但不能以失去灵活性为代价。

    @luin 论坛氛围不错,我改写了例子,希望能把问题引向更深入。
    主要的改写是引入了需要传递更多因素的情况,来表达这种灵活性在使用 control flow module 之后的丧失。

    http://gist.github.com/4214679.js

    引入之后,感觉有两个问题。

    传参自由度的问题:

    第 6 行分别引用了两个外层里定义的 id 和 val 。对应在 async 写法的第 24 行,是不 work 的。要解决这个问题,需要显式地向外用 callback 传递或者引入一个 params object 来解决问题。需要传递的参数更多的话,可能更麻烦。

    async 的思路是通过 callback 的参数传递给下一个 function 。各个 function 的变量作用域是并列的,也就是说,嵌套层次的扁平是以引用外层变量的能力作为代价的。

    错误处理自由度的问题:

    上述每一个错误都给了一个不同的错误处理,之前的例子是全都用 next(e) 来处理,所以,体现不出这种约束来。在 async 的版本里,所有的错误都汇集到 27 行来处理,要如何区分这许多种错误呢?

    我也有用 async ,主要是用它抽象数据结构的相关方法,比如 map 之类,但 control flow 的因为上述的问题,就暂时还没有用。

    @linlinqi 这是否意味着需要在 callback(e,r) 的 node.js 标准风格和 deferred 风格之间做适配呢?这个适配,在 node 没有推出标准的 deferred 风格之前,似乎是没有动力去完成的。

    还是那句话,要追求漂亮,但不能以失去灵活性为代价。我知道这可能有些不切实际,必然要 trade off 什么的。不过,思考一下倒也有益。
    jackyz
        25
    jackyz  
       2012-12-07 17:10:26 +08:00
    这个问题踢到铁板讨论不下去了吗?
    robhsiao
        26
    robhsiao  
    OP
       2012-12-07 17:49:11 +08:00
    sorry, 楼主读书不多,只能围观 :(
    各位请继续...
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3970 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 28ms UTC 05:17 PVG 13:17 LAX 21:17 JFK 00:17
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86