Nodejs怎样去避免同步写法,写出真正的异步代码?

Nodejs怎样去避免同步写法,写出真正的异步代码?

我有两段代码,用来抓取学校的新闻图片,由于每个页面的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更快得出结果的前提下?

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


17 回复

为了将你的同步代码转换为异步代码,并且避免回调地狱,可以使用Promise或者async/await语法。以下是修改后的代码示例:

使用Promises

const cheerio = require('cheerio');
const request = require('request-promise-native'); // 使用request-promise-native库
const fs = require('fs');
const gm = require('gm');

const address = 'http://ssdut.dlut.edu.cn';
const url = "http://ssdut.dlut.edu.cn/index.php/News/index/p/";
const _dirname = "E:/pictures/";

async function fetchNewsLinks() {
  let news = [];
  for (let i = 1; i <= 82; i++) {
    const response = await request(`${url}${i}`);
    const $ = cheerio.load(response);
    const links = [];
    $('table').eq(2).find('table').eq(1).find('tr').slice(1, 13).each((index, ele) => {
      links.push($(ele).find('a').attr('href'));
    });
    news = news.concat(links);
  }
  return news;
}

async function downloadImages(newsLinks) {
  for (const link of newsLinks) {
    const response = await request(`${address}${link}`);
    const $ = cheerio.load(response);
    const images = [];
    $('img').slice(1).each((index, ele) => {
      images.push($(ele).attr('src'));
    });

    for (const imgSrc of images) {
      await new Promise((resolve, reject) => {
        gm(`${address}${imgSrc}`)
          .write(`${_dirname}${picNum}.jpg`, (err) => {
            if (!err) {
              console.log(`No:${picNum}:done!`);
            }
            picNum++;
            resolve();
          });
      });
    }
  }
}

(async () => {
  try {
    const newsLinks = await fetchNewsLinks();
    await downloadImages(newsLinks);
  } catch (error) {
    console.error(error);
  }
})();

解释

  1. 使用 request-promise-native:这个库允许你使用Promise来处理请求,而不是传统的回调。
  2. 使用 asyncawait:这样可以使代码更易于阅读和维护,同时保持异步特性。
  3. 分离逻辑:将获取新闻链接和下载图片的逻辑分离到不同的函数中,每个函数返回一个Promise。
  4. 异步循环:使用 for...of 循环来处理数组中的每个元素,并等待每个异步操作完成。

通过这种方式,你可以更好地管理异步流程,避免回调地狱,并使代码更清晰和易于维护。


去看看 async 或者 iced coffeescript

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

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

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

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

也可以试下EventProxy~~

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

嗯,我去看看。

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

这样……

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

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

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

异步确实还是Promises 推荐RSVP

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

要将上述同步代码改为异步代码,可以使用Promise、async/await等方法来避免回调地狱,并确保异步操作顺序正确。以下是改进后的代码示例:

使用async/await

const cheerio = require('cheerio');
const request = require('request-promise-native'); // 使用request-promise-native替代原生request
const fs = require('fs');
const path = require('path');

const baseUrl = 'http://ssdut.dlut.edu.cn';
const apiUrl = `${baseUrl}/index.php/News/index/p/`;
const downloadDir = path.join(__dirname, 'pictures');

async function fetchNewsUrls() {
    let newsUrls = [];
    for (let i = 1; i <= 82; i++) {
        const response = await request(apiUrl + i);
        const $ = cheerio.load(response);
        const newsLinks = $('table')
            .eq(2)
            .find('table')
            .eq(1)
            .find('tr')
            .slice(1, 13)
            .map((_, el) => $(el).find('a').attr('href'))
            .get();
        newsUrls = newsUrls.concat(newsLinks);
    }
    return newsUrls;
}

async function downloadImages(newsUrls) {
    for (const newsUrl of newsUrls) {
        try {
            const response = await request(baseUrl + newsUrl);
            const $ = cheerio.load(response);
            const imgSrcs = $('img').slice(1).map((_, el) => $(el).attr('src')).get();
            for (let i = 0; i < imgSrcs.length; i++) {
                const imgSrc = imgSrcs[i];
                const imagePath = path.join(downloadDir, `${i}.jpg`);
                await new Promise((resolve, reject) => {
                    gm(fs.createReadStream(imgSrc))
                        .write(imagePath, (err) => {
                            if (err) return reject(err);
                            resolve();
                        });
                });
                console.log(`Downloaded: ${imagePath}`);
            }
        } catch (error) {
            console.error(`Failed to download images from ${newsUrl}:`, error);
        }
    }
}

(async () => {
    try {
        const newsUrls = await fetchNewsUrls();
        await downloadImages(newsUrls);
    } catch (error) {
        console.error('Error:', error);
    }
})();

解释

  1. 使用request-promise-native:它返回一个Promise对象,这样我们可以更方便地使用async/await语法。
  2. fetchNewsUrls函数:通过循环获取所有新闻链接,并将其存储在newsUrls数组中。
  3. downloadImages函数:对于每个新闻链接,下载所有图片并保存到本地目录。
  4. 异步主函数:首先调用fetchNewsUrls获取新闻链接,然后调用downloadImages下载图片。

这种方式可以确保所有异步操作都按顺序执行,避免了回调地狱,并且代码结构更加清晰。

回到顶部