zoukankan      html  css  js  c++  java
  • Node.js 101(2): Promise and async

    ——原文地址:http://blog.chrisyip.im/nodejs-101-package-promise-and-async

    先回想一下 Sagase 的项目结构:

    lib/
        cli.js
        sagase.js
    Gruntfile.js
    package.json
    

    上一篇讲了 package.json,这一篇讲 lib/sagase.js

    由于代码比較长,就分开一节节地讲,完整的点开 GitHub 看吧。

    'use strict';
    

    通知编译器进入 strict mode,基本的作用是让编译器帮你检查一些坑,有经验的 JavaScript 开发人员能够忽略。

    var fs = require('fs-extra'),
        async = require('async'),
        _ = require('lodash'),
        Promise = require('bluebird'),
        path = require('path')
    

    引用全部依赖项。假设是使用 --harmony。建议使用 const 取代 varkeyword,可避免变量被改动。

    var mar = 'March'
    mar = 'June'
    console.log(mar) // 'June'
    
    const july = 'July'
    july = 'May'
    console.log(july) // 'July'
    

    function readFiles (opts) {} 包括非常多信息,一个一个说。

    return new Promise(function (resolve, reject) {}
    

    返回一个 Promise 对象。

    因为 Node.js 的特点是异步。一般都须要通过异步来处理:

    // get all files in DIR
    fs.readdir(DIR, function (err, files) {
      if (err) {
        return errorHandler(err)
      }
      // loop files
      files.forEach(function (file) {
        // get the stat of each files
        fs.stat(file, function (err, stat) {
          // if it's file
          if (stat.isFile()) {
            // get content of file
            fs.readFile(file, function (err, buff) {
              // do whatever you want
            })
          }
        })
      })
    })
    

    假设须要在一个函数里处理非常多事情,甚至说须要让这个函数的返回结果可在多个文件中使用,仅仅靠回调会非常吃力——不知道哪个文件在什么时候才须要使用它的返回结果。

    假设使用 Promise,就会简单非常多:

    // in a.js
    var Promise = require('bluebird'),
        readFile
    
    module.exports = new Promise(function (resolve, reject) {
      fs.readdir(DIR, function (err, files) {
        err ?

    reject(err) : resolve(files) }) })

    // in b.js
    var readFile = require('./a.js')
    
    readFile
      .then(function (files) {
        // do something with files
        return NEW_RESULT;
      }, function (err) {
        // handle error here
      })
      .then(function (data) {
        // do something with NEW_RESULT
      }, function (err) {
        // handle error here
      })
    
    // in c.js
    var readFile = require('./a.js')
    
    readFile.then(function (files) {
      // the files still exist and accessable
    })
    

    通过 Promise 的封装。就能够让 resolve 的结果跨不同的文件,不须要在一个回调函数里处理全部事情。另外,通过 .then() 的第二个函数处理 reject 的错误结果,避免了多重推断。

    注意的是,在这里引入了一个叫 bluebird 的第三方 Promise 库。假设 Node.js 版本号是 >= 0.11.13 的话,是不须要引入第三方库的。

    async.each(
      files,
      function (file, cb) {
      },
      function (err) {
        err ? reject(err) : resolve(res)
      }
    )
    

    async 是一个改善异步回调流程、把异步处理能力赋予普通数组处理函数的库,比方 async.each 就相当于多线程版的 Array.forEach,只是在实际使用中。不要期待运行顺序是乱序或者正序,关键是第三个參数。

    async.each([1, 2, 3], function (item, next) {
      console.log(item)
      next()
    }, function (err) {
      console.log('all tasks done')
    })
    // output:
    //   1
    //   2
    //   3
    //   "all  tasks done"
    

    一般来说,由于 fs.stat 是异步调用的,所以 Array.forEach 遍历完数组之后,非常难保证里面的任务是否所有已完毕,这时候调用Promise.resolve() 就无法保证数据的正确性。而通过 async.each 的第三个參数,就能够得知任务的状态,并保证 Promise 能够得到正确的数据。

    path.resolve(opts.folder + '/' + file).replace(process.cwd(), '.')
    

    path 是一个用于解决和文件夹有关问题的库。path.resolve 会将 ./dir转变为 /Users/USER/PATH/TO/dir (Mac) 格式的完整文件夹。

    process.cwd() 会返回调用这个脚本的进程所在文件夹;另外,另一个__dirname 是指脚本所在文件夹:

    如果有例如以下文件:

    lib/index.js
    index.js
    
    // in lib/index.js
    module.exports = function () {
      return __dirname
    }
    
    // in index.js
    console.log(process.cwd()) // '/Users/USER/PATH/'
    
    console.log(require('./lib')()) // '/Users/USER/PATH/lib/'
    

    剩下的代码不一一解释,大致做了下面工作:

    1. 先读取指定文件夹的全部文件 (fs.readdir)
    2. 使用 async.each 遍历获取的结果
    3. 推断每一个文件的 stat 是不是文件夹 (fs.stat)
      1. 若是,检查符不符合条件,符合则进入下一轮递归 (readFiles)
      2. 若否,检查符不符合条件,符合则加入到终于结果的数组中 (res.push())
    4. 反复 1-3 直至遍历全然部文件

    需注意的是:

    • cb() 在多处出现,用于通知 async.each 这个任务已运行完成
    • 递归中用 Promise readFiles.then() 的第一个參数处理 res 的返回、用readFiles.catch 处理 error、用 readFiles.finally 处理 cb() (因必须通知 async 任务已完毕,在此处统一处理)
    • Promise.finally 是 bluebird 特有的 API,原生 Promise 需这样实现:readFiles.then().catch().then(),第二个 then 相当于 finally (只是不够直观)
    function formatOptions (opts) {}
    

    用于格式化传入的參数,就不多解释了。

    须要注意的是,在这里用了一个 opts 对象来包括全部參数并传递给readFiles

    因为 JavaScript 的特性,用一个 JSON 对象传递參数会方便非常多,比方说:

    function formatOptions (folder, pattern, ignoreCase, nameOnly, exclude, excludeNameOnly, recusive) {}
    

    因为 Node.js 用的 V8 仍未包括函数參数默认值,所以最方便的做法是用 JSON 对象:

    var options = _.assign({}, {
      key_1: default_value_1,
      key_2: default_value_2
      key_3: default_value_3
    }, opts)
    

    而且 JSON 对象也利于扩展——不管是添加还是删除 key,都不须要更改接口。

    最后,定义了一个 Sagase 类,并在外部调用这个类时,创建一个新的Sagase 对象:

    function Sagase () {
      Object.defineProperties(
        this,
        {
          find: {
            enumerable: true,
            value: function (opts) {
              return readFiles(formatOptions(opts))
            }
          }
        }
      )
    }
    
    module.exports = new Sagase()
    

    Object.defineProperty 和 Object.defineProperties 是 ECMAScript 5 中新增加的特性。通过它们就能够创建非常多好玩的东西,比方传统的jQuery.fn.size()

    jQuery.fn.size = function () {
      return Number.MAX_SAFE_INTEGER
    }
    
    var body = $('body')
    body.length // 1
    body.size() // 9007199254740991
    

    换成 ES 5 的写法:

    Object.defineProperties(
      jQuery.fn,
      {
        size: {
          enumerable: true,
          get: function () {
            return this.length
          }
        }
      }
    )
    
    var body = $('body')
    body.length // 1
    body.size // 1
    
    jQuery.fn.size = function () {
      return Number.MAX_SAFE_INTEGER
    }
    
    body.size // 1
    body.size() // TypeError: number is not a function
    

    合理利用 const 和 Object.defineProperty 能够避开一些非预期的情况,保证程序健壮性。

    从 lib/sagase.js 的代码能够看出。Node.js 的异步特性导致函数是一层套一层 (`fs.readdir -> fs.stat -> isDirectory()) 。写起来事实上不好看。也不利于理解,比方:

    function getJSON () {
      var json
      fs.readFile('demo.json', function (err, buff) {
        json = JSON.parse(buff.toString())
      })
      return json
    }
    
    getJSON() // undefined
    

    当然,要在 Node.js 里使用同步接口也是能够的,如 fs.readdirSync,但:

    • Node.js 同步接口不见得比 Python、Ruby 等语言高效
    • 不是全部接口都有同步版本号,如 child_process.exec

    为了发挥 Node.js 的优势。就须要正确利用 Promise、async 来编敲代码。比方说有这种一个场景。浏览器端须要获取购物车里全部商品、赠品的数据,常见的步骤大概是:找商品数据,通过商品 ID 找促销规则得到赠品,计算总价,返回结果。这些步骤能够通过多次请求数据库最后用后端语言拼接;假设是 RESTful API 模式,也能够发起多次请求,最后在浏览器端拼接。

    假设在浏览器端和server端增加异步处理呢?

    var router = express.Router()
    
    router.get('/cart', function (req, res, next) {
      async.parallel(
        [
          // get all products data
          function (next) {
            request('/api/products', OPTIONS)
              .then(function (data) {
                next(null, data)
              })
          },
          // get products gifts
          function (next) {
            async.map(
              PRODUCT_LIST,
              function (p, cb) {
                request('/api/product/:id/gifts', OPTIONS)
                  .then(function (data) {
                    cb(null, data)
                  })
              },
              function (err, results) {
                next(null, results)
              }
            )
          }
        ],
        function (err, results) {
          RESPONSE_BODY = {
            products: results[0],
            gifts: results[1],
            total: calcTotal(results[0])
          }
          res.send(RESPONSE_BODY)
        }
      )
    })
    
    $.ajax('/cart').then(function () {
      // handle products and gifts here
    })
    

    通过异步的处理,浏览器就能够用一次请求完毕多次请求的效果。而且不会破坏 RESTful API 的结构。这对于资源紧张、网络环境多变的移动端来说,是很有利的;而对于电脑端则通过降低请求时间来提高交互响应速度。提高用户体验。

    这一篇主要内容是如何利用 Promise、async 等库绕开 Node.js 的回调函数坑。回调函数算是 Node.js 最多人黑的地方,假设不能掌控它。写出来的 Node.js 代码将会相当丑陋、不易维护。

    而为了让代码好看一些。ECMAScript 6 里增加了一个新特性——GeneratorKoa 已经開始使用 generator 来构建项目。详细怎么用,下一篇说吧。

    长按图片识别图中二维码(或搜索微信公众号FrontEndStory)关注“前端那些事儿”。带你了解最新的前端技术。



  • 相关阅读:
    GB/T 38635.1-2020 信息安全技术 SM9标识密码算法 第1部分:总则
    信息安全行业国家标准汇总,信息安全行业从业人员必看
    贪吃蛇游戏(printf输出C语言版本)
    C 实战练习题目57
    C 实战练习题目56
    C 实战练习题目55
    C 实战练习题目54
    C 实战练习题目53
    C 实战练习题目52
    C 实战练习题目51
  • 原文地址:https://www.cnblogs.com/jzssuanfa/p/6821481.html
Copyright © 2011-2022 走看看