[翻译] 比较不同Observer模式的实现

原文链接:Comparison between different Observer Pattern implementations

下面的比较只是关于订阅、发布事件以及删除事件监听器的一些基本特性。主要是基于各种基本概念实现上的不同和它们使用上的优缺点,而不是可用的特性。有些被标记为缺点的部分可以通过“好的实现”或则“hack”来避免,但是通常情况下,这些缺点都是存在的。

所有的实现都基于一种设计模式(Observer)完成了相同的任务,他们虽然有很多共同点,但运行方式上却有些许不同。本文主要是为了帮助你选择哪种实现最适合你的工作流以及你要解决的问题种类。

Event Emitter/Target/Dispatcher

  • 所有派发自定义事件的对象都需要继承自EventEmitter/EventTarget/EventDspatcher或者实现特定的接口。
  • 使用字符串定义事件类型。
  • DOM2/DOM3 Events就是基于这样的模式。

代码示例

1
2
3
  myObject.addEventListener('myCustomEventTypeString', handler);
  myObject.dispatchEvent(new Event('myCustomEventTypeString'));
  myObject.removeEventListener('myCustomEventTypeString', handler);

优点

  • target object具有完全的控制,确保只监听特定target派发的事件。
  • 能够派发任意的事件类型而不用修改target object。
  • 每一种target/object/event都使用相同的方法。
  • 代码容易理解。
  • 事件的target通常都是object本身,这使得事件冒泡更有逻辑性
  • 流行。

缺点

  • 倾向于使用继承而不是组合。
  • 使用字符串定义事件类型,容易产生拼写错误并且IDE自动完成不能很好的工作。
  • Event handler通常只接受一个参数(Event Object)。
    • 如果想传递额外的数据,必须创建一个实现了特定接口的自定义Event对象或者扩展一个基本的Event对象,这个过程通常是繁琐不便的。

Publish / Subscribe (pub/sub)

  • 使用同一个对象向多个订阅者**广播消息。
    • 并不是必要条件,但大多数实现都使用一个静态的集中的对象作为广播者。
  • 使用字符串定义事件类型。
  • 消息和事件的目标之间并没有关系。

代码示例

1
2
3
  globalBroadcaster.subscribe('myCustomEventTypeString', handler);
  globalBroadcaster.publish('myCustomEventTypeString', paramsArray);
  globalBroadcaster.unsubscribe('myCustomEventTypeString', handler);

优点

  • 任意对象都能订阅/发布任何事件类型。
  • 轻量级。
  • 容易使用/实现。

缺点

  • 任意对象都能订阅/发布任何事件类型(是的,这既是优点也是缺点)。
  • 使用字符串定义事件类型。
    • 容易出错。
    • 没有自动完成(除非将value保存为变量/常量)。
    • 通过命名规范来避免消息被错误的订阅者拦截。

Signals

  • 每种事件类型都有自己的控制器
  • 事件类型不依赖于字符串。

代码示例

1
2
3
  myObject.myCustomEventType.add(handler);
  myObject.myCustomEventType.dispatch(param1, param2, ...);
  myObject.myCustomEventType.remove(handler);

优点

  • 不依赖于字符串。
    • 自动完成能正常工作。
    • 派发或监听一个不存在的事件类型会抛出错误(能够更早的发现错误)。
    • 不需要创建常量来存储字符串值。
  • 细粒度的控制每个监听器和事件类型。
    • 每个signal都是一个特定的目标/容器。
  • 容易定义对象派发的signal
  • 倾向于使用组合而不是继承。
    • 别和原型链混淆了。

缺点

  • 不能派发任意类型的事件。(这在大多数情况下也是个优点)
  • 每个事件类型都是个对象成员。(这在大多数情况下也是个优点)
    • 如果有很多事件类型的话,易导致命名空间混乱
  • 不会将事件类型和目标对象传递给callback使得很难使用通用的handler(工作于多个事件类型和目标)。
  • 与大多数人使用的不同。

结论

就像生活中的大多数东西一样,每种解决方案都有它的优点和缺点。决定哪种方式最适合取决于你。我希望本文能在你做决定时帮助到你。再一次,没有解决方案是万能的。

IE下点击scrollbar会导致焦点移动到body

现象

IE这货果然与众不同,当光标焦点在input时,点击同页面内其他区域的scrollbar,会导致焦点移动到body,从而触发绑定在input上的blur事件,如果input中的值与之前不同,甚至还会触发change事件... Chrome曾经也有类似的问题,但在最新版中已经修正了,而Firefox则完全没有这样的问题。

影响

这个问题看起来微不足道,实际上影响还是非常大的,主要表现在下面2个方面

  • 多数的suggest控件会出错
    suggest往往是通过input(输入部分)和div(下拉框部分)组成。有时,下拉框内容过多,用户需要移动滚动条才能看全选项,但因为点击滚动条会让input失去焦点,导致控件误认为用户结束输入,从而关闭suggest的下拉部分,导致用户实际上无法正确的进行滚动条操作。

  • form
    这个更容易理解了,一般来说form的验证都是绑定在blur或者change事件上,如果form太长,需要移动滚动条才能看全的情况下,一旦鼠标点击滚动条就会错误的触发form验证操作,将无用的错误信息显示给用户。

解决方案

我们来看看jQueryUI的Autocomplete是怎么解决这个问题的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// input's blur event
blur: function( event ) {
    if ( this.cancelBlur ) {
        delete this.cancelBlur;
        return;
    }

    clearTimeout( this.searching );
    this.close( event );
    this._change( event );
}

// dropdown's mousedown event
mousedown: function( event ) {
    // prevent moving focus out of the text field
    event.preventDefault();

    // IE doesn't prevent moving focus even with event.preventDefault()
    // so we set a flag to know when we should ignore the blur event
    this.cancelBlur = true;
    this._delay(function() {
        delete this.cancelBlur;
    });

    // clicking on the scrollbar causes focus to shift to the body
    // but we can't detect a mouseup or a click immediately afterward
    // so we have to track the next mousedown and close the menu if
    // the user clicks somewhere outside of the autocomplete
    var menuElement = this.menu.element[ 0 ];
    if ( !$( event.target ).closest( ".ui-menu-item" ).length ) {
        this._delay(function() {
            var that = this;
            this.document.one( "mousedown", function( event ) {
                if ( event.target !== that.element[ 0 ] &&
                        event.target !== menuElement &&
                        !$.contains( menuElement, event.target ) ) {
                    that.close();
                }
            });
        });
    }
}

这下就很清楚了,要处理这个问题,要点有两个:

  • 通过自定义的flag判断是否需要跳过(直接return)input的blur事件
  • 全局(document)监视下一次mousedown事件,如果不是特定区域才执行blur相关操作

[理解Underscore和Lo-Dash] Collections _.each

_.each

遍历集合,对集合中的每个元素执行回调。

API

Lo-Dash

_.forEach(collection [, callback=identity, thisArg])

Aliases

each

Arguments

  1. collection (Array|Object|String): 要遍历的集合
  2. [callback=identity] (Function): 每次迭代中调用的函数
  3. [thisArg] (任意): 绑定到callbackthis
  4. callback接受三个参数: (value, index|key, collection)

Returns

(Array, Object, String): 返回collection.

Underscore

_.each(list, iterator, [context])

Aliases

forEach

Arguments

  1. list (Array|Object|String): 要遍历的集合
  2. iterator (Function): 每次迭代中调用的函数
  3. [context] (任意): 绑定到callbackthis
  4. iterator接受三个参数: (element|value, index|key, list)

Returns

(Array, Object, String): 返回undefined.

Note

  • Lo-Dash可以省略回调函数,而Underscore则必须传入
  • Lo-Dash可以通过在回调中返回false提前结束迭代
  • Lo-Dash会返回Collection从而允许链式操作,Underscore的返回值则是undefined

Example

Lo-Dash

1
2
3
4
5
6
7
8
_.forEach([1,2,3])
// => 返回[1,2,3]

_([1, 2, 3]).forEach(alert).join(',');
// => alert每个数字并返回'1,2,3'

_.forEach({ 'one': 1, 'two': 2, 'three': 3 }, alert);
// => alert每个数字value(不保证按照定义的顺序执行)

Underscore

1
2
3
4
_.each([1, 2, 3], alert);
// => alert每个数字
_.each({one : 1, two : 2, three : 3}, alert);
// => alert每个数字value(不保证按照定义的顺序执行)

Source

Lo-Dash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function forEach(collection, callback, thisArg) {
    var index = -1,
    length = collection ? collection.length : 0;

    callback = callback && typeof thisArg == 'undefined' ? callback : lodash.createCallback(callback, thisArg);
    if (typeof length == 'number') {
        while (++index < length) {
            if (callback(collection[index], index, collection) === false) {
                break;
            }
        }
    } else {
        forOwn(collection, callback);
    }
    return collection;
}

Underscore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var each = _.each = _.forEach = function(obj, iterator, context) {
    if (obj == null) return;
    if (nativeForEach && obj.forEach === nativeForEach) {
      obj.forEach(iterator, context);
    } else if (obj.length === +obj.length) {
      for (var i = 0, l = obj.length; i < l; i++) {
        if (iterator.call(context, obj[i], i, obj) === breaker) return;
      }
    } else {
      for (var key in obj) {
        if (_.has(obj, key)) {
          if (iterator.call(context, obj[key], key, obj) === breaker) return;
        }
      }
    }
  };

Additional

  • obj.length === +obj.length

+obj: 将obj转换成10进制数,否则返回NaN。因此,上面的判断等价于obj.length && typeof obj.length == 'number'

  • if (iterator.call(context, obj[i], i, obj) === breaker) return;

breaker是预先定义的空对象({}),Underscore内部用于提前结束循环的标志,并没有对外公开。另外,因为对象的===比较的是对象地址,所以就算用户在自己的iterator中返回{},上述if仍然不成立

  • for in循环不会遍历non-enumerable属性,因此像ObjecttoString等就不会被迭代

JavaScript中位操作符的特殊作用

Javascript主要有以下几种位操作符:

  • AND ( & )
  • OR ( | )
  • XOR ( ^ )
  • NOT ( ~ )
  • LEFT SHIFT ( << )
  • RIGHT SHIFT ( >> )
  • ZERO-FILL RIGHT SHIFT ( >>> )

一般来说,我们在Javascript中很少能用到这些位操作符,但在某些特殊情况下,这些简单的操作符却能抵得上好几行代码(如果不在乎可读性的话)。

-(n+1)

对一个数进行~运算,等同于-(n+1)

1
~1 === -2 // => true

Note: 这只能应用于整数部分,~1.1 === -2

这在实际使用中常常配合indexOf一起使用,if (~array.indexOf(string))等同于if (array中没有string)

取整(忽略小数部分)

1
2
~~1.1 === 1 // => true
1.1 ^ 0 === 1 // => true

这两个在某些JS库或游戏编程中经常使用。

总结

总的来说,位操作符毕竟可读性不太好,列出来只是为了以后遇到这样的代码能看的懂,实际项目中还是不要玩这些花的东西比较好。

如何将一个String和多个String值进行比较

开发中我们经常需要将一个String和多个String值进行比较。直觉反应是使用||符号连接多个===完成,比如:

1
2
3
if (string === 'banana' || string === 'pineapple') {
   fruitColor = 'yellow';
}

这样能够很好的完成需求,但总觉得有点笨,并且对扩展不友好,当我们的水果种类变多时:

1
2
3
if (string === 'banana' || string === 'pineapple' || string === 'mongo' || string === 'lemon') {
   fruitColor = 'yellow';
}

上面的代码看起来就不那么好看了,让我们看看有什么其他方式能够处理这种需求。

Switch

1
2
3
4
5
6
7
switch(string) {
    case 'banana':
    case 'pineapple':
    case 'mongo':
    case 'lemon':
      fruitColor = 'yellow';
}

这看起来不错,但是总是要多打些字,对于不喜欢多打字的人来说不是个好方法。

Array

1
2
3
if (['banana', 'pineapple', 'mongo', 'lemon'].indexOf(string) >= 0) {
    fruitColor = 'yellow';
}

这下好多了,但还有个问题,IE9以下的IE浏览器并不支持indexOf方法,如果你要在IE<=8的环境中使用Array方式比较多个string值,要么自己写一个indexOf方法,要么就得引入一些库来做浏览器兼容。

jQuery

jQuery提供了一个inArray方法

1
2
3
if ($.inArray(['banana', 'pineapple', 'mongo', 'lemon'], string) >= 0) {
    fruitColor = 'yellow';
}

Underscore

Underscore提供了一个contains方法

1
2
3
if (_.contains(['banana', 'pineapple', 'mongo', 'lemon'], string)) {
    fruitColor = 'yellow';
}

正则表达式

当然,我们还有终极武器——正则表达式

1
2
3
if (/^(banana|pineapple|mongo|lemon)$/.test(string)) {
    fruitColor = 'yellow';
}

JavaScript Throttle & Debounce

Throttle

无视一定时间内所有的调用,适合在发生频度比较高的,处理比较重的时候使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var throttle = function (func, threshold, alt) {
    var last = Date.now();
    threshold = threshold || 100;

    return function () {
        var now = Date.now();

        if (now - last < threshold) {
            if (alt) {
                alt.apply(this, arguments);
            }
            return;
        }

        last = now;
        func.apply(this, arguments);
    };
};

Debounce

一定间隔内没有调用时,才开始执行被调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var debounce = function (func, threshold, execASAP) {
    var timeout = null;
    threshold = threshold || 100;

    return function () {
        var self = this;
        var args = arguments;
        var delayed = function () {
            if (!execASAP) {
                func.apply(self, args);
            }
            timeout = null;
        };

        if (timeout) {
            clearTimeout(timeout);
        } else if (execASAP) {
            func.apply(self, args);
        }

        timeout = setTimeout(delayed, threshold);
    };
};

Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var test = function (wrapper, threshold) {
    var log = function () {
        console.log(Date.now() - start);
    };
    var wrapperedFunc = wrapper(log, threshold);
    var start = Date.now();
    var arr = [];

    for (var i = 0; i < 10; i++) {
        arr.push(wrapperedFunc);
    }

    while(i > 0) {
        var random = Math.random() * 1000;
        console.log('index: ' + i);
        console.log('random: ' + random);
        setTimeout(arr[--i], random);
    }
};

test(debounce, 1000);
test(throttle, 1000);

innerText和textContent

今天在使用innerText时遇到一个兼容性问题,FireFox不支持innerText方法,查了MDN,发现FireFox下有个类似的方法,叫textContent,它和IE的innerText类似, 都是用来获取(设置)元素中text的方法。

语法

  • 设置

    1
    
    element.textContent = “text”;
    
  • 获取

    1
    
    var text = element.textContent;
    

Note: textContent和innerText类似,也会同时获取子元素的text content,比如

1
2
<div>this is <span>a</span> text!</div>
// div.textContent == "this is a text!"

innerText的区别

  • textContent会获取所有元素的content,包括<script><style>元素
  • innerText不会获取hidden元素的content,而textContent
  • innerText会触发reflow,而textContent不会
  • innerText返回值会被格式化,而textContent不会

主流浏览器支持情况

  • IE 9+
  • Chrome 1+
  • FireFox(Gecko)

Windows环境下Rails安装Bootstrap失败解决方法

Windows环境下,Rails安装Bootstrap总会失败,提示therubyracer无法安装。

这是因为Bootstrap使用的less文件依赖therubyracer实时执行js将less转换成css,而therubyracer这个gem并没有对应的Windows版本。

Hiran Peiris在github上提供了提供了解决方案,他编译了所有的dll和gem。这下,我们终于可以在Windows下用Bootstrap啦。

RequireJS学习

RequireJS是一个JavaScript文件和模块加载器。除了可以在浏览器中使用外,还可以用Node或Rhino等Server端环境。

最新版可以在这里下载。

基本用法

假设你的工程目录结构如下:

  • project
    • index.html
    • js
      • lib
        • jquery.js
      • app
        • sub_app.js
        • app.js

首先,将requirejs.js放入js/lib目录。

  • project
    • index.html
    • js
      • lib
        • jquery.js
        • require.js
      • app
        • sub_app.js
        • app.js

然后,在index.html中引入<script>用来加载require.js。

1
<script data-main="js/app" src="js/lib/require.js"></script>

在app.js中,使用require方法加载其他脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
requirejs.config({
    // 默认从js/lib目录加载
    baseUrl: 'js/lib',
    // 如果模块ID以app开头,则从js/app目录加载
    // paths相对于baseUrl设定
    // 不要指定".js"后缀,因为paths可以是一个目录
    paths: {
        app: '../app',
        jquery: 'jquery.min',
    }
});
 
// app入口
require(['app/sub_app'], function (sub) {
    sub.hello();
});

在sub_app.js中定义一个module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// define相对于baseUrl设定
define(['jquery'], function ($) {
    return {
        log: function (msg) {
            if (window.console && console.log) {
                console.log(msg);
            } else {
                alert(msg);
            }
        },
        hello: function () {
            this.log("Hello, I'm powered by jQuery " + $().jquery + "!");
        }
    };
});

现在,打开浏览器的控制台,应该能看到我们自定义的module成功使用jQuery输出了下面这句话:

1
Hello, I'm powered by jQuery 1.8.3! 

Ruby的private和protected

今天,下面这段程序让我纠结了很久,Ruby中private的概念真的很奇怪。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Test private
  def test_print
    puts 'test'
  end
end

class Test2 < Test
  def test_print2
    # self.test_print #=> 这里加上self就不能调用,private method `test_print' called for # (NoMethodError)
    test_print #=> 不加self就能调用
  end
end

Test2.new.test_print2 

为什么不加self的话,private也可以调用父类的方法呢?

原来在Ruby中,private和Java或者其他语言不一样,子类也可以调用,只是不能指定调用者。

翻了下《The Ruby Way》,书上说:

private:类和子类都能调用,但是private方法不能指定调用者,默认为self。

protected:类和子类都能调用,可以指定调用者。

这就解释了为什么上面的代码中,用self调用会出错,而不加self就能正确执行。