從大廠面試題看觀察者模式

winty發表於2019-10-10

題目

請實現下面的自定義事件 Event 物件的介面,功能見註釋(測試1)
該 Event 物件的介面需要能被其他物件擴充複用(測試2)
// 測試1
Event.on('test', function (result) {
    console.log(result);
});
Event.on('test', function () {
    console.log('test');
});
Event.emit('test', 'hello world'); // 輸出 'hello world' 和 'test'
// 測試2
var person1 = {};
var person2 = {};
Object.assign(person1, Event);
Object.assign(person2, Event);
person1.on('call1', function () {
    console.log('person1');
});
person2.on('call2', function () {
    console.log('person2');
});
person1.emit('call1'); // 輸出 'person1'
person1.emit('call2'); // 沒有輸出
person2.emit('call1'); // 沒有輸出
person2.emit('call2'); // 輸出 'person2'
var Event = {
    // 通過on介面監聽事件eventName
    // 如果事件eventName被觸發,則執行callback回撥函式
    on: function (eventName, callback) {
        //你的程式碼
    },
    // 觸發事件 eventName
    emit: function (eventName) {
        //你的程式碼
    }
};
複製程式碼

差點沒把我看暈...

好吧,一步一步來看看怎麼回事。

①瞭解一下觀察者模式

觀察者模式:

這是一種建立鬆散耦合程式碼的技術。它定義物件間 一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都將得到通知。由主體和觀察者組成,主體負責釋出事件,同時觀察者通過訂閱這些事件來觀察該主體。主體並不知道觀察者的任何事情,觀察者知道主體並能註冊事件的回撥函式。

例子:

假如我們正在開發一個商城網站,網站裡有header頭部、nav導航、訊息列表、購物車等模組。這幾個模組的渲染有一個共同的前提條件,就是必須先用ajax非同步請求獲取使用者的登入資訊。這是很正常的,比如使用者的名字和頭像要顯示在header模組裡,而這兩個欄位都來自使用者登入後返回的資訊。這個時候,我們就可以把這幾個模組的渲染事件都放到一個陣列裡面,然後待登入成功之後再遍歷這個陣列並且呼叫每一個方法。 基本模式:

function EventTarget(){     
    this.handlers = {}; 
} 
EventTarget.prototype = {     
    constructor: EventTarget,
    addHandler: function(type, handler){
         if (typeof this.handlers[type] == "undefined"){
              this.handlers[type] = [];
         }
         this.handlers[type].push(handler);
     }, 
    fire: function(event){
         if (!event.target){
             event.target = this;
         }
         if (this.handlers[event.type] instanceof Array){
             var handlers = this.handlers[event.type];
             for (var i=0, len=handlers.length; i < len; i++){
                 handlers[i](event); 
            }
         }
     },
     removeHandler: function(type, handler){ 
        if (this.handlers[type] instanceof Array){ 
            var handlers = this.handlers[type]; 
            for (var i=0, len=handlers.length; i < len; i++){ 
                if (handlers[i] === handler){ 
                    break;
                 }
             }
             handlers.splice(i, 1); 
          }
      }
};
複製程式碼

大概意思就是,建立一個事件管理器。handles是一個儲存事件處理函式的物件。

addHandle:是新增事件的方法,該方法接收兩個引數,一個是要新增的事件的型別,一個是這個事件的回撥函式名。呼叫的時候會首先遍歷handles這個物件,看看這個型別的方法是否已經存在,如果已經存在則新增到該陣列,如果不存在則先建立一個陣列然後新增。

fire:是執行handles這個物件裡面的某個型別的每一個方法。

removeHandle:是相應的刪除函式的方法。

好啦,回到題目,分析一下。

②題目中的測試一:

// 測試1
Event.on('test', function (result) {
    console.log(result);
});
Event.on('test', function () {
    console.log('test');
});
Event.emit('test', 'hello world'); // 輸出 'hello world' 和 'test'
複製程式碼

意思就是,定義一個叫test型別的事件集,並且註冊了兩個test事件。然後呼叫test事件集裡面的全部方法。在這裡on方法等價於addHandle方法,emit方法等價於fire方法。其中第一個引數就是事件型別,第二個引數就是要傳進函式的引數。

是不是這個回事呢?很好,那麼我們要寫的程式碼就是:

var Event = {
    // 通過on介面監聽事件eventName
    // 如果事件eventName被觸發,則執行callback回撥函式
    on: function (eventName, callback) {
        //我的程式碼
        if(!this.handles){
             this.handles={};    
        }      
       if(!this.handles[eventName]){
            this.handles[eventName]=[];
       }
       this.handles[eventName].push(callback);
    },
    // 觸發事件 eventName
    emit: function (eventName) {
        //你的程式碼
       if(this.handles[arguments[0]]){
           for(var i=0;i<this.handles[arguments[0]].length;i++){
               this.handles[arguments[0]][i](arguments[1]);
           }
       }
    }
};
複製程式碼

這樣測試,完美地通過了測試一。

③題目中的測試二:

var person1 = {};
var person2 = {};
Object.assign(person1, Event);
Object.assign(person2, Event);
person1.on('call1', function () {
    console.log('person1');
});
person2.on('call2', function () {
    console.log('person2');
});
person1.emit('call1'); // 輸出 'person1'
person1.emit('call2'); // 沒有輸出
person2.emit('call1'); // 沒有輸出
person2.emit('call2'); // 輸出 'person2'
複製程式碼

大概意思就是為兩個不同person註冊自定義事件,並且兩個person之間是互相獨立的。

直接測試,發現輸出了

GitHub

這個好像是題目要求有點出入呢,或者這才是題目的坑吧!

解釋一下,Object.assign(person1, Event);

這個是ES6的新物件方法,用於物件的合併,將源物件(source)的所有可列舉屬性,複製到目標物件(target)。

意思是將Event裡面的可列舉的物件和方法放到person1裡面。

GitHub

也就是說,如果源物件某個屬性的值是物件,那麼目標物件拷貝得到的是這個物件的引用。由於進行測試一的時候呼叫了on方法,所以event裡面已經有了handles這個可列舉的屬性。然後再分別合併到兩個person裡面的話,兩個person物件裡面的handles都只是一個引用。所以就互相影響了。

如果assign方法要實現深克隆則要這樣:

GitHub

問題是,題目已經固定了方式,我們不能修改這個方法。

所以,我們必須將handles這個屬性定義為不可列舉的,然後在person呼叫on方法的時候再分別產生handles這個物件。

也就是說正確的做法應該是:

var Event = {
    // 通過on介面監聽事件eventName
    // 如果事件eventName被觸發,則執行callback回撥函式
    on: function (eventName, callback) {
        //你的程式碼
        if(!this.handles){
            //this.handles={};
            Object.defineProperty(this, "handles", {
                value: {},
                enumerable: false,
                configurable: true,
                writable: true
            })
        }
       
       if(!this.handles[eventName]){
            this.handles[eventName]=[];
       }
       this.handles[eventName].push(callback);
    },
    // 觸發事件 eventName
    emit: function (eventName) {
        //你的程式碼
       if(this.handles[arguments[0]]){
           for(var i=0;i<this.handles[arguments[0]].length;i++){
               this.handles[arguments[0]][i](arguments[1]);
           }
       }
    }
};
複製程式碼

通過這道題,感覺考得真的很巧妙而且很考基礎。

最後

  • 這是一篇之前寫的部落格,這裡是遷移了過來~~
  • 瞭解更多內容,歡迎關注我的blog, 給我個star~
  • 覺得內容有幫助可以關注下我的公眾號 「前端Q」,一起學習成長~~
    GitHub

相關文章