• wordpress CMS主题:ssmay主题 wordpress CMS主题:ssmay主题
  • 首页 > 前端开发 > 每天阅读一个 npm 模块(5)- ee-first

    每天阅读一个 npm 模块(5)- ee-first

    作者: 分类:前端开发 点击: 38 次
    wordpress CMS主题:ssmay主题

      扫描下面的二维码,“关注”我的头条号,我会每天更新免费视频课程。

      系列文章:

      1. 每天阅读一个 npm 模块(1)- username
      2. 每天阅读一个 npm 模块(2)- mem
      3. 每天阅读一个 npm 模块(3)- mimic-fn
      4. 每天阅读一个 npm 模块(4)- throttle-debounce

      一句话介绍

      今天阅读的模块是 ee-first,通过它我们可以在监听一系列事件时,得知哪一个事件最先发生并进行相应的操作,当前包版本为 1.1.1,周下载量约为 430 万。

      用法

      首先简单介绍一下 ee-first 中的 ee ,它是 EventEmitter 的缩写,也就是事件发生器的意思,Node.js 中不少对象都继承自它,例如:net.Server | fs.ReadStram | stream 等,可以说许多核心 API 都是通过 EventEmitter 来进行事件驱动的,它的使用十分简单,主要是 emit (发出事件)和 on(监听事件) 两个接口:

      const EventEmitter = require('events');
      const emitter = new EventEmitter();
      
      emitter.on('sayHi', (name) => {
          console.log(`hi, my name is ${name}!`);
      });
      
      emitter.emit('sayHi', 'Elvin');
      // => 'hi, my name is Elvin!'
      

      接下来看看 ee-frist 的用法:

      const EventEmitter = require('events');
      const first = require('ee-first');
      
      // 1. 监听第一个发生的事件
      const ee1 = new EventEmitter();
      const ee2 = new EventEmitter();
      
      first([
        [ee1, 'close', 'end', 'error'],
        [ee2, 'error']
      ], function (err, ee, event, args) {
        console.log(`'${event}' happened!`);
      })
      
      ee1.emit('end');
      // => 'end' happened!
      
      // 2. 取消绑定的监听事件
      const ee3 = new EventEmitter();
      const ee4 = new EventEmitter();
      
      const trunk = first([
        [ee3, 'close', 'end', 'error'],
        [ee4, 'error']
      ], function (err, ee, event, args) {
        console.log(`'${event}' happened!`);
      })
      
      trunk.cancel();
      ee1.emit('end');
      // => 什么都不会输出
      
      

      源码学习

      参数校验

      源码中对参数的校验主要是通过 Array.isArray() 判断参数是否为数组,若不是则通过抛出异常给出提示信息 —— 对于第三方模块而言,需要对调用者保持不信任的态度,所以对参数的校验十分重要。

      在早些年的时候,JavaScript 还不支持 Array.isArray() 方法,当时是通过 Object.prototype.toString.call( someVar ) === '[object Array]' 来判断 someVar 是否为数组。当然现在已经是 2018 年了,已经不需要使用这些技巧。

      // 源码 5-1
      function first (stuff, done) {
        if (!Array.isArray(stuff)) {
          throw new TypeError('arg must be an array of [ee, events...] arrays')
        }
          
        for (var i = 0; i < stuff.length; i++) {
          var arr = stuff[i]
      
          if (!Array.isArray(arr) || arr.length < 2) {
            throw new TypeError('each array member must be [ee, events...]')
          }
          
          // ...
        }
      }
      

      生成响应函数

      ee-first 中,首先会对传入的每一个事件名,都会通过 listener 生成一个事件监听函数:

      // 源码 5-2
      
      /**
       * Create the event listener.
       * 
       * @param  {String}    event, 事件名,例如 'end', 'error' 等
       * @param  {Function}  done, 调用 ee-first 时传入的响应函数
       */
      function listener (event, done) {
        return function onevent (arg1) {
          var args = new Array(arguments.length)
          var ee = this
          var err = event === 'error' ? arg1 : null
      
          // copy args to prevent arguments escaping scope
          for (var i = 0; i < args.length; i++) {
            args[i] = arguments[i]
          }
      
          done(err, ee, event, args)
        }
      }
      

      这里有两个需要注意的地方:

      1. error 事件进行了特殊的处理,因为在 Node.js 中,假如进行某些操作失败了的话,那么会将错误信息作为第一个参数传给回调函数,例如文件的读取操作:fs.readFile(filePath, (err, data) => { ... }。在我看来,这种将错误信息作为第一个参数传给回调函数的做法,能够引起开发者对异常信息的重视,是十分值得推荐的编码规范。
      2. 通过 new Array() 和循环赋值的操作,将 onevent 函数的参数保存在了新数组 args 中,并将其传递给 done 函数。假如不考虑低版本兼容性的话,这里可以使用 ES6 的方法 Array.from() 实现这个功能。不过我暂时没有想出为什么要进行这个复制操作,虽然作者进行了注释,说是为了防止参数作用域异常,但是我没有想到这个场景,希望知道的读者能在评论区指出来~

      绑定响应函数

      接下来则是将生成的事件响应函数绑定到对应的 EventEmitter 上即可,关键就是 var fn = listener(event, callback); ee.on(event, fn) 这两句话:

      // 源码 5-3
      function first (stuff, done) {
        var cleanups = []
      
        for (var i = 0; i < stuff.length; i++) {
          var arr = stuff[i]
          var ee = arr[0]
      
          for (var j = 1; j < arr.length; j++) {
            var event = arr[j]
            var fn = listener(event, callback)
      
            // listen to the event
            ee.on(event, fn)
            // push this listener to the list of cleanups
            cleanups.push({
              ee: ee,
              event: event,
              fn: fn
            })
          }
        }
          
        function callback () {
          cleanup()
          done.apply(null, arguments)
        }
        
        // ...
      }
      

      移除响应函数

      在上一步中,不知道有没有大家注意到两个 cleanup

      1. 在源码 5-3 的开头,声明了 cleanups 这个数组,并在每一次绑定响应函数的时候,都通过 cleanups.push() 的方式,将事件和响应函数一一对应地存储了起来。

      2. 源码 5-3 尾部的 callback 函数中,在执行 done() 这个响应函数之前,会调用 cleanup() 函数,该函数十分简单,就是通过遍历 cleanups 数组,将之前绑定的事件监听函数再逐一移除。之所以需要清除是因为绑定事件监听函数会对内存有不小的消耗(这也是为什么在 Node.js 中,默认情况下每一个 EventEmitter 最多只能绑定 10 个监听函数),其实现如下:

        // 源码 5-4
        function cleanup () {
          var x
          for (var i = 0; i < cleanups.length; i++) {
            x = cleanups[i]
            x.ee.removeListener(x.event, x.fn)
          }
        }
        

      thunk 函数

      最后还剩下一点代码没有说到,这段代码最短,但也是让我收获最大的地方 —— 帮我理解了 thunk 这个常用概念的具体含义。

      // 源码 5-5
      function first (stuff, done) {
        // ...
      
        function thunk (fn) {
          done = fn
        }
      
        thunk.cancel = cleanup
      
        return thunk
      }
      

      thunk.cancel = cleanup 这行很容易理解,就是让 first() 的返回值拥有移除所有响应函数的能力。关键在于这里 thunk 函数的声明我一开始不能理解它的作用:用 const thunk = {calcel: cleanup} 替代不也能实现同样的移除功能嘛?

      后来通过阅读作者所写的测试代码才发了在 README.md 中没有提到的用法:

      // 源码 5-6 测试代码
      const EventEmitter = require('events').EventEmitter
      const assert = require('assert')
      const first = require('ee-first')
      
      it('should return a thunk', function (testDone) {
          const thunk = first([
              [ee1, 'a', 'b', 'c'],
              [ee2, 'a', 'b', 'c'],
              [ee3, 'a', 'b', 'c'],
          ])
          thunk(function (err, ee, event, args) {
              assert.ifError(err)
              assert.equal(ee, ee2)
              assert.equal(event, 'b')
              assert.deepEqual(args, [1, 2, 3])
              testDone()
          })
      
          ee2.emit('b', 1, 2, 3)
      })
      

      上面的代码很好的展示了 thunk 的作用:它将本来需要两个参数的 first(stuff, done) 函数变成了只需要一个回调函数作为参数的 thunk(done) 函数。

      这里引用阮一峰老师在 Thunk 函数的含义和用法 一文中所做的定义,我觉得非常准确,也非常易于理解:

      在 JavaScript 语言中,Thunk 函数将多参数函数替换成单参数的版本,且只接受回调函数作为参数

      当然,更广义地而言,所谓 thunk 就是将一段代码通过函数包裹起来,从而延迟它的执行(A thunk is a function that wraps an expression to delay its evaluation)。

      // 这段代码会立即执行
      // x === 3
      let x = 1 + 2;
      
      // 1 + 2 只有在 foo 函数被调用时才执行
      // 所以 foo 就是一个 thunk
      let foo = () => 1 + 2
      

      这段解释和示例代码来自于 redux-thunk - Whtat's a thunk ?

      写在最后

      ee-first 是我这些天读过的最舒服的代码,既有详尽的注释,也不会像昨天所阅读的 throttle-debounce 模块那样让人觉得注释过于冗余。

      另外当面对一段代码不知有何作用时,可以通过相关的测试代码入手进行探索。

      关于我:毕业于华科,工作在腾讯,elvin 的博客 欢迎来访 ^_^



      欢迎“关注”我的头条号,我会每天更新免费视频课程。

      头条二维码
      关注我的头条号
      头条二维码
      加入我的QQ群

    文章作者:sunny
    本文地址:http://wanlimm.com/77202006218488.html
    版权所有 © 转载时必须以链接形式注明作者和原始出处!

    上一篇:
    下一篇:
    wordpress CMS主题:ssmay主题

    或许你会感兴趣的文章:

    发表评论

    电子邮件地址不会被公开。 必填项已用*标注

    This site uses Akismet to reduce spam. Learn how your comment data is processed.