怎样去避免同步写法,写出真正的异步代码?
发布于 11 年前 作者 ggaaooppeenngg 6980 次浏览 最后一次编辑是 8 年前

我有两段代码,用来抓取学校的新闻图片,由于每个页面的url都是大概这样的格式*****.com/1 *****.com/2 *****.com/3 所以我就用一个循环把所有的页面穷举一遍,然后对每个循环请求一次url,抓取里面的新闻链接,存在news里面,在访问news里面的url,把照片down下来。代码大概是这样的:

var cheerio = require('cheerio'),
 request = require('request'),
 _ = require('underscore'),
 gm = require('gm'),
 fs = require('fs');

var fileNumber = 0;
var picNum = 0;
var news = [];
const address = 'http://ssdut.dlut.edu.cn';
const url = "http://ssdut.dlut.edu.cn/index.php/News/index/p/";
const _dirname= "E:/pictures/"
function newsdown (){
 for(var i=0;i<82;i++){
  request(url+i,function(err,res,body){

   //上面的i如果换成filenumber,结果永远是访问第一个页面

   fileNumber++;

   //这里如果我fileNumber写成i,结果永远是82
   //我也不知道,为什么,所以用了两个计数器

   console.log(url+fileNumber+'<-page');
   var $ = cheerio.load(body);
   //console.log($('table').eq(2).find('table').eq(1).find('tr').slice(1,13).html())
      $('table').eq(2).find('table').eq(1).find('tr').slice(1,13)
      .each(function(index,ele){
       //console.log(address+this.find('a').attr('href'));
       //把url存在news里面
       news.push(this.find('a').attr('href'));
      });
  });
 };
};
//访问news里的url,把img标签里的图片存在电脑里
function picdown (){
 _.each(news,function(){
       request(address+this.find('a').attr('href'),function(err,res,body){
  var $ = cheerio.load(body);
   $('img').slice(1).each(function(index,ele){
    gm(address+this.attr('src')).write(_dirname+picNum+'.jpg',function(err){
    if(!err) console.log('No:'+picNum+':done!');
    picNum++;
    });
   });
  });
 });
}
newsdown();
picdown();

这段代码的问题在于……我写成了同步的,newsdown这部分还没只得到结果,picdown就运行了,结果news里面全是null。 然后我写成回调的形式在最后调用picdown,但实际上还是上面的代码没把news得出结果,回调函数就执行了。最后没有办法,我直接把picdown嵌套在迭代的内部,变成下面这段:

var cheerio = require('cheerio'),
 request = require('request'),
 _ = require('underscore'),
 gm = require('gm'),
 fs = require('fs');
//23
var fileNumber = 0;
var picNum = 0;
var news = [];
const address = 'http://ssdut.dlut.edu.cn';
const url = "http://ssdut.dlut.edu.cn/index.php/News/index/p/";
const _dirname= "E:/pictures/"
function newsdown (){
 for(var i=0;i<82;i++){
  request(url+i,function(err,res,body){
   fileNumber++;
   console.log(url+fileNumber+'<-page');
   var $ = cheerio.load(body);
   //console.log($('table').eq(2).find('table').eq(1).find('tr').slice(1,13).html())
      $('table').eq(2).find('table').eq(1).find('tr').slice(1,13)
      .each(function(index,ele){
       //console.log(address+this.find('a').attr('href'));
       request(address+this.find('a').attr('href'),function(err,res,body){
        var $ = cheerio.load(body);
     $('img').slice(1).each(function(index,ele){
      gm(address+this.attr('src')).write(_dirname+picNum+'.jpg',function(err){
      if(!err) console.log('No:'+picNum+':done!');
      picNum++;
      });
     });
       });
      });
  });
 };
};
newsdown();

图片是能down下来了,但是运行的测试信息是这样的:

部分截图
http://ssdut.dlut.edu.cn/index.php/News/index/p/77<-page
http://ssdut.dlut.edu.cn/index.php/News/index/p/78<-page
http://ssdut.dlut.edu.cn/index.php/News/index/p/79<-page
http://ssdut.dlut.edu.cn/index.php/News/index/p/80<-page
http://ssdut.dlut.edu.cn/index.php/News/index/p/81<-page
http://ssdut.dlut.edu.cn/index.php/News/index/p/82<-page
No:0:done!
No:1:done!
No:3:done!
No:4:done!
No:7:done!

也就是外层循环全部执行完了,才来执行内层的循环。(还是说外层的循环先返回结果,而I\O量更大的内层才返回?) 还有就是每次执行都会漏掉一些图片,我就跑了很多遍,我又是按迭代的number来命名的,所以有些图片可能占了 4.jpg ,8.jpg,9.jpg几个名字的情况,要么就是把前面down的图片覆盖了,要么就是被别的图片覆盖了。

然后我又写了一个模拟登陆QQ的代码,发送信息,奇怪的地方写在注释里了,代码大概是这样的:

var request = require('request');

var qqinfo = {
    loginType:2,//1:不登录 2:隐身登录 3:在线登录
    qq:123456,//qq号
    pwd:'123456'//qq密码
};

var Vdata ={
 sid:''
};
var userAgend = 'Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13';
var PostHead = {'User-Agent' : userAgend , 'Content-Type' : 'application/x-www-form-urlencoded'};
var address = 'http://pt.3g.qq.com/handleLogin';
var Post = {
   url:address,
   headers:PostHead,
   method:'POST',
   form:qqinfo
   };
var Friends = [];
//模拟webqq post消息,登陆以后用正则表达式抓取sid
request(Post,function(err,res,body){
 if(res.statusCode == 302){
  var reg = new RegExp('sid=(.[^&]+)','ig');
  //console.log(res.headers.location);
  reg.exec(res.headers.location);
  var sid = RegExp.$1
  Vdata.sid = sid;
  //console.log(sid);
  console.log('login!');
  getFriendsPage(1,function(){
   console.log('\nonline friends:'+Friends.length+'\n');
   for(var i = 0 ;i<Friends.length;i++){
    var qqInfo = Friends[i];
    console.log(i+1+':'+qqInfo.qq+' '+'\t['+qqInfo.name+']');
   };
   if(sendmsg(1,'hi')) console.log('ok!');
  });
  return;
 }
 if(res.body.indexOf('验证码')>=0){
  console.log('需要输入验证码');
  return;
 }
});
//用拿到的sid请求好友列表界面
function getFriendsPage(page,cb) {
 Friends =[];
 var chatMain = 'http://q16.3g.qq.com/g/s?sid=$SID&aid=nqqchatMain&p=$Page';
     chatMain = chatMain.replace('$SID',Vdata.sid);
     chatMain = chatMain.replace('$Page',page);
    request({url:chatMain,headers:{'User-Agent':userAgend,'Cache-Control':'max-age=0'},method:'GET'},function(err,res,body){
     var regx = /u=(\d+)[\s\S]+?class="name" >(.*?)<\/span>/ig
     
//如果我在这里写一个console.log(boody)什么的,下面的body却是null。

     while(regx.exec(body)){
      console.log('qq'+RegExp.$1);
      var qqInfo = {qq:RegExp.$1,name:RegExp.$2};
      Friends.push(qqInfo);
     };
     cb();
     return;
    });
};
//模拟信息发送
function sendmsg(index,msg){
 var qqInfo = Friends[index-1];
 var form ={
  'u':qqInfo.qq,
  'msg':msg,
  'aid':'发送'
 }
 if(!qqInfo) return false;
 var pUrl = 'http://q32.3g.qq.com/g/s?sid=$SID';
 pUrl = pUrl.replace('$SID',Vdata.sid);
 request({url:pUrl,form:form,headers:{'User-Agent':userAgend,'Cache-Control':'max-age=0','Content-Type': 'application/x-www-form-urlencoded'},method:'POST'},function(err,res,body){
  if(body.indexOf('重新登录')>=0 && body.indexOf('书签可能有误')>=0){
            console.log('发送失败');
            console.log(form);
            return false;
        }else{
         return true;
        }
 });
}

如果我删掉之前的调试信息,就勉强能够运行,是不是就是说,我的同步代码要建立在之前的I\O要比之后的I\O更快得出结果的前提下?

这让我很困惑,怎样写才能符合异步的风格呢? 仅仅回调好像也不代表异步………… 如果用事件触发又该是怎样的呢……

18 回复

去看看 async 或者 iced coffeescript

在循环中绑定事件处理如果需要用到循环变量,注意要用传值的方式传递,否则事件处理只是保留了对循环变量的引用,最后触发时使用的都是同一个值

还没理解异步的编程思想,再多看看案例

这个就是大家通常吐槽的 }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}

如果觉得自己能力还可以的话可以去看一下 promise 规范…你的问题这个规范里面都有说

也可以试下EventProxy~~

嗯,这个和闭包有关,我大概知道了。

嗯,我去看看。

用js的闭包方法写,就一定是异步的了。。。。。

这样……

async+eventproxy 这两个基本是必装的模块啊!

iced 看上去很美,但生成的出来的代码根本无法看啊。

@hhuai

如果习惯写coffeescript也不错啊,用不着看生成的代码

不过我还是用coffee+async了,思路比iced清晰一些

我大概看了一下eventproxy,它可以让串行的回调并行运行,然后不用深度嵌套写回调函数。 比如我要创建一个正则表达式,然后去exec。 我想知道,比如 regexp.exec(a)但是这个regexp没有回调的方式,I/O还没有结束,下面的代码就已经拿a去跑了,这个时候怎么办?

@youxiachai 求给个promise链接……

@danielking 也是一个事件驱动的模块么?

异步确实还是Promises 推荐RSVP

还是推荐async,和promise比起来,代码更易维护,也方便写测试用例,同时也符合node社区中的异步回调规范

回到顶部