• Javascript异步编程之二回调函数


    for (var i = 0; i < 5; i++)

    { setTimeout(function() { console.log(i); }, 1000);}

    console.log(i);

    我相信只要是做过前端笔试题的都见过这样的题目,那么输出的结果是什么呢?

    第一种可能的答案:0 1 2 3 4 5 5
    第二种可能的答案:5 5 5 5 5 5 5(后面每个5隔一秒输出)
    显然第二种结果是正确的,接下来我们分析一下这个题目。首先看一下目前大家都在用的一个口令或者说方法:

    同步优先、异步靠边、回调垫底
    用公式表达就是:同步 => 异步 => 回调
    现在根据这个口令我们来分析一下结果为什么是这个:
    1)for循环和循环体外部的console是同步的,所以先执行for循环,再执行外部的console.log。(同步优先)
    2)for循环里面有一个setTimeout回调,他是垫底的存在,只能最后执行。(回调垫底)
    那么,为什么我们最先输出的是5呢?
    这个也是非常好理解,for循环先执行,但是不会给setTimeout传参(回调垫底),等for循环执行完,就会给setTimeout传参,而外部的console打印出5是因为for循环执行完成了。

     
    传统的同步函数需要返回一个结果的话都是通过return语句实现,例如:
     
    复制代码
    function foo() {
         var a = 3,
              b = 2;
         return a+b;
    }
    
    var c = foo();
    console.log(c); //5
    复制代码
     
    就是说后面的代码console.log要得到函数foo的运行结果只要调用该函数就可以得到它所返回的值a+b。
     
    但是如果foo是一个异步函数,可以这样做吗?
     
    异步函数的定义:
     
    首先说一下javascript里面怎样书写异步函数。基本的方法就是,在你的函数定义里面调用别人已经提供的异步api (不管是原生的还是第三方的),你的函数也就是个异步函数了:
     
    复制代码
    function foo(callback) {
         你自己的代码;
         asyncFn(function() {
              var result = 你自己的代码;
              callback(result);
         });
    }
    复制代码
    上面这个例子中,你要定义一个函数foo,里面调用了一个异步函数asyncFn,在asyncFn运行完了之后调用foo的回调函数callback,来对结果result进行处理。
     
    上面是一般异步函数的定义格式。setTimeout是javascript里面经常用的异步api。 做试验的时候经常用它来模拟异步操作,下面的例子模拟一个操作要运行1秒后才返回结果 (当然你可以设0秒,但仍然是异步的):
     
    复制代码
    function foo(callback) {
         你自己的代码;
         setTimeout(function() {
              var result = 你自己的代码;
              callback(result);
         }, 1000);
    }
    复制代码
     
    在node.js里面提供了其他的api来起到类似的作用(把你的同步代码写成异步函数),setImmediate或者process.nextTick,其作用就是异步调用你的代码。这两个基本上用法类似,但特定情况下是不同的,可以上网自行查找setImmediate,setTimeout(fn, 0)和process.nextTick 的区别,一般在不了解的情况下,建议使用setImmediate.
     
    复制代码
    function foo(callback) {
         你自己的代码;
         setImmediate(function() {
              var result = 你自己的代码;
              callback(result);
         });
    }
     
    function foo(callback) {
         你自己的代码;
         process.nextTick(function() {
              var result = 你自己的代码;
              callback(result);
         });
    }
    复制代码

    异步函数的调用:

    以上是异步函数的定义,下面讲一下异步函数是怎样调用的。在调用异步函数的时候,回调函数有两种写法,一是直接写个匿名回调函数,下面例子中function(data) {...} 就是匿名回掉函数:
     
    foo(function(data) {
         你的代码来使用传回来的data;
    });
     
    二是先定义一个函数,然后使用函数引用作为回调:
     
    复制代码
    function bar(data) {
         你的代码来使用传回来的data;
    }
    
    foo(bar);
    复制代码
     
    现在回到开篇的问题,有同学一定看明白了,为什么异步函数不能用return来返回值。 下面把两种写法放一起,方便比较:
     
    正确写法:
    复制代码
    function foo(callback) {
         你自己的代码;
         asyncFn(function() {
              var result = 你自己的代码;
              callback(result);
         });
    }
    复制代码
     
    错误写法:
    复制代码
    function bar() {
         你自己的代码;
         asyncFn(function() {
              var result = 你自己的代码;
         });
         
         return result;
    }
    复制代码
     
    在错误写法中,bar企图使用return来返回result。在上一篇已经讲过,异步api不会等执行完了再往下执行,也就是说asyncFn在调用后马上会往下执行return result这句,这时候asyncFn还在异步执行当中,result根本还没有计算出来,所以不能return期望结果。当然对javascript语法比较熟的同学也清楚,函数外部不能访问函数内部变量,就是说在asyncFn函数的外部是无法访问到result这个变量的,不管那个是主要原因,哪个是次要,异步api是肯定不能用return来返回值。
     
    以上就是基本的异步函数的定义和调用。
     
    异步函数实例:
     
    任何只讲理论不讲应用的教程都是耍流氓。。。好吧,举个实际的例子:
     
    网络请求是node.js异步API中常用的一种,能够进行网络请求的node.js原生和第三方的api非常多,下面以superagent为例来演示一下(运行之前先用npm安装superagent):
     
    复制代码
    var agent = require('superagent');
    
    agent.get('http://www.baidu.com')
         .end(function(err, res) {
              if(err) {
                   console.log(err);
              } else {
                   console.log('http status: ' + res.status);
                   console.log(res.header);
              }
         });
    复制代码
     
    上面代码是可以直接运行的。 这个例子中agent.get(url).end(callback)是一个异步api调用, 在回调函数里面定义自己的代码。 回掉函数传回来两个数据,err和res。 如果不出问题的话res对象是完整的http响应的内容,如果出错的话出错信息会保存在err对象中。
    这里为了演示只是在控制台打印传回来的res的status和header (注:superagent是个很不错的http客户端,可以去github了解其提供的具体功能)。
     
    顺带提一下: 异步函数不能像同步代码一样用try...catch捕获异常,所以这里有一个约定俗成,就是回掉函数一般需要有两个参数,上面superagent的例子中就是err和res,一般第一个参数是error,当异步代码发生异常的时候用这个参数返回异常详细信息, 第二个参数才是返回的有用数据,当没有异常的时候用它来返回,即superagent的例子中的res,返回的是http响应内容。
     
    异步嵌套:
     
    在同步代码中,如果有三个函数顺序执行,前一个的输出作为后一个的输入,只要按顺序执行三个函数即可。但在异步代码中,要做到这个就有点麻烦了。具体的原因就是因为不能像同步函数一样用return来返回值,而必须用回掉函数。所以,第二个函数要在第一个函数的回调中调用,同样第三个函数要在第二个函数的回调中调用,这就是所谓的回调嵌套。下面的例子是要先从a.txt读取其内容,然后从b.txt读取其内容,最后把两个内容合并写入ab.txt,完了后控制台打印:read and write done!
     
    复制代码
    var fs = require('fs');
    
    fs.readFile('./a.txt', function(err1, data1) {
         fs.readFile('./b.txt', function(err2, data2) {
              fs.writeFile('./ab.txt', data1 + data2, function(err) {
                   console.log('read and write done!');
              });
         });
    });
    复制代码
     
    三个异步函数嵌套看起来挺简单的。设想一下,如果有5个,10个甚至更多的异步函数要顺序执行,那要嵌套多少层呢?(大家都不喜欢身材横着长吧哈哈)说实话相当恐怖,代码会变得异常难读,难调试,难维护。这就是所谓的回调地狱或者callback hell。正是为了解决这个问题,才有了后面两节要讲的内容,用promise或generator进行异步流程管理。异步流程管理说白了就是为了解决回调地狱的问题。所以说任何事情都有两面性,异步编程有它独特的优势,却也同时遇到了同步编程根本不会有的代码组织难题。
     
    思考题:
     
    自己写异步函数还有一种经常遇到的情况,就是在循环中,比如for循环中不断调用异步函数asyncFn,因为每次循环调用asyncFn你都不知道它会运行到什么时候,这种情况下你的foo函数什么时候调用回掉函数来返回最终结果? 大家可以用读写文件来试验,使用fs.readFile这个函数来循环读取某个目录下面所有的文件内容,最后用fs.writeFile合并写到一个新的文件里面。答案下回分解 :)
     
    复制代码
    function foo(arr, callback) {
         for(var i=0; i<arr.length; ++i) {
              asyncFn(function() {
                   你的代码;
              })
         }
    }
    复制代码

    转载请标明出处: http://www.cnblogs.com/chrischjh/p/4667713.html

  • 相关阅读:
    基于Lumisoft.NET实现的邮件发送功能
    jqueryautocomplete 使用手册
    asp.net访问网络路径方法模拟用户登录
    JavaScript判断浏览器类型及版本
    解决jquery.autocomplete在IE6下被下拉框遮住的问题
    How to resovle aspnet_merge.exe error issue in web deployment project
    敏捷开发之Scrum扫盲篇
    JS 异常: Uncaught RangeError: Maximum call stack size exceeded
    HTTP请求的 转发 重定向 代理
    JS跨域访问 控制
  • 原文地址:https://www.cnblogs.com/asdyzh/p/9802014.html
Copyright © 2020-2023  润新知