跳至主要内容

2 篇文章 含有標籤「javascript」

檢視所有標籤

· 閱讀時間約 9 分鐘
Guy Chien

前言

由於最近想更加深入的了解 Design Pattern ,於是挑選了 Observer Pattern 當作練習的目標,這篇文章後續會提到:

  1. 滿足 Observer Pattern 觀察者模式至少需要滿足的介面
  2. 如何使用 Observer Pattern 實作一個通知中心,能夠滿足 廣播 ( broadcast ) 給每個人。
  3. 新增一些介面來讓現有實作能滿足 推播 ( push ) 給某個人。

那廢話不多說,我們開始吧!


先讓我們來看看 Observer Pattern 的定義:

The observer pattern is a software design pattern in which an object, named the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.

由此段定義可以知道,我們會需要至少兩樣物件,其中一項是其中所敘述到的 subject (在以下案例當中我們稱作 Observable 可被訂閱的 ),而另一項則是被呼叫的 observer。

因此滿足此 pattern 至少要滿足以下介面:
  1. 讓 Observable 能夠被觀察 subscribe
  2. 讓 Observable 能夠被取消觀察 unsubscribe
  3. 讓 Observable 能夠在事件發生時能夠通知所有的觀察者 notify observers

IObservable

被通知後的 Observer 實際上的行為由觀察者自己本身實作 update

interface IObservable {
observers: IObserver[];
subscribe: (observer: IObserver) => void;
unsubscribe: (observer: IObserver) => void;
notify: (payload: any) => void;
}

interface IObserver {
update: (payload: any) => void;
}

而先讓我們實作最一開始的 NotificationCenter ( IObservable ):

class NotificationCenter implements IObservable {
//透過此介面能夠維護目前現有觀察此事件的所有觀察者。
observers: typeNotificationObserver[];

constructor() {
this.observers = [];
}

//透過此介面能夠通知所有觀察者事件發生了。
notify(payload): void {
this.observers.forEach((observer) => {
observer.update({ ...payload });
});
}

//透過此介面能夠讓外界的觀察者留下自身訊息,使後續事件發生時,被觀察者可以通知。
subscribe(observer): void {
this.observers.push(observer);
}

//透過此介面,觀察者可以抹去自己的紀錄,讓往後事件發生,不會再收到通知。
unsubscribe(observer): void {
const target = this.observers.findIndex((obr) => {
return obr === observer;
});

if (target !== -1) {
this.observers.splice(target, 1);
}
}
}

而由於我們是在實作一個通知中心,並且希望這通知中心能夠有個簡單的介面讓我們能達到,紀錄通知的歷史,獲得所有通知歷史,及推播、廣播功能。因此定義了 INotificationCenter,並且讓上方的 NotificationCenter 多重實作介面 IObservable、INotificationCenter,且我們還會需要一個簡單的通知物件介面,讓我們能在這些功能中傳遞。

INotificationCenter

  1. 用來維護所有通知的歷史紀錄 notifications
  2. 廣播給所有的觀察者 broadcast
  3. 推播給某一位觀察者 push
  4. 獲得所有的通知歷史紀錄 getAllNotifications
interface INotificationCenter {
notifications: INotification[];
broadcast: (notification: INotification) => void;
push: (notification: INotification, target: IPerson) => void;
getAllNotifications: () => INotification[];
}

INotification

  1. 通知訊息的識別 id
  2. 通知訊息的時間點 date
  3. 通知訊息的內容 message
interface INotification {
id: string;
date: Date;
message: string;
}

而原先的 NotificationCenter 將被擴充並改寫為以下樣子:

class NotificationCenter implements IObservable, INotificationCenter {
//原有介面
observers: typeNotificationObserver[];

//所有通知的歷史紀錄
notifications: INotification[];

constructor() {
this.observers = [];
this.notifications = [];
}

/* 由於要滿足可以推播給某一位觀察者,這裏實作有一些調整,
* 可以理解為當今天目標是 `all` 時將會對所有觀察者進行通知也就是廣播,
* 而當今天目標是某一個觀察者 id 時只針對那位觀察者進行通知。*/
notify(payload): void {
if (payload.target.id === 'all') {
this.observers.forEach((obr) => {
obr.update({
...payload,
});
});
} else {
// 此處聰明如你,會發現 observer 介面有新增,先別急,下面做解釋。
const target = this.observers.find(
(obr) => obr.id === payload.target.id
);
if (target) {
target.update({
...payload,
});
}
}
}

//原有介面
subscribe(observer): void {
this.observers.push(observer);
}

//原有介面
unsubscribe(observer): void {
const target = this.observers.findIndex((obr) => {
return obr === observer;
});

if (target !== -1) {
this.observers.splice(target, 1);
}
}

//分別傳入通知、目標對象,達到推播功能。
push(notification: INotification, target: IPerson): void {
this.notifications.push(notification);
this.notify({
notification,
target,
});
}

//傳入通知,並且目標對象為所有觀察者 all,達到廣播功能。
broadcast(notification: INotification): void {
this.notifications.push(notification);
this.notify({ notification, target: { id: 'all' } });
}

//獲取所有通知歷史紀錄
getAllNotifications(): INotification[] {
return this.notifications;
}
}
小提醒

眼尖的讀者可能會發現,上方 notify 當中所使用的 observer 多了一個 id 介面可以做使用,目的是為了讓 notify 能辨別 observer,只通知目標觀察者。

而為此定義了一項新介面 ITargetObserver,當今天有針對目標的需求時,能夠透過多重實作此介面來達到需求。而介面如下:

ITargetObserver

  1. 用於識別 Observer id
interface ITargetObserver {
id: string;
}

那我們最終需求的觀察者 Observer 的介面就滿足了,讓我們來看看實作,其中的 update 就是當 observer 收到通知後最終的行為:

interface IObserver {
update: (payload: any) => void;
}

interface ITargetObserver {
id: string;
}

type typeNotificationObserver = IObserver & ITargetObserver;

class NotificationObserver implements typeNotificationObserver {
update: (payload: any) => void;
id: string;

constructor(update, id) {
this.update = update;
this.id = id;
}
}

const update = (payload) => {
const { notification: ntfc, target } = payload;
if (target.id !== 'all') {
console.log(
`[push] ${ntfc.id} , ${ntfc.date.toDateString()} , ${
ntfc.message
} send to ${target.name}`
);
} else {
console.log(
`[broadcast] ${ntfc.id} , ${ntfc.date.toDateString()} , ${
ntfc.message
}`
);
}
};

最後我們將建立一個人的物件,去註冊這些事件,一但 push / broadcast 事件發生時,能夠第一時間的去通知這些註冊此事件的人。

interface IPerson {
id: string;
name: string;
}

type typeEvent = { observable: IObservable; observer: IObserver };

class Person implements IPerson {
id: string;
name: string;
constructor(id, name) {
this.id = id;
this.name = name;
}

register(events: typeEvent[]) {
events.forEach((event) => {
event.observable.subscribe(event.observer);
});
}

unRegister(event: typeEvent) {
event.observable.unsubscribe(event.observer);
}
}

const brian = new Person('001', 'brian');
const brainRegisterNotificationEvent = {
observable: center,
observer: new NotificationObserver(update, brian.id),
};
brian.register([brainRegisterNotificationEvent]);

const guy = new Person('002', 'guy');
const guyRegisterNotificationEvent = {
observable: center,
observer: new NotificationObserver(update, guy.id),
};
guy.register([guyRegisterNotificationEvent]);

最後我們將可以透過 push 跟 broadcast 這兩介面,達到通知中心的推播跟廣播功能:

center.broadcast({
id: '001',
date: new Date(),
message: 'this is broadcast',
});

brian.unRegister(brainRegisterNotificationEvent);

center.push(
{
id: '002',
date: new Date(),
message: 'this is the push notification for brian',
},
brian
);

center.push(
{
id: '003',
date: new Date(),
message: 'this is the push notification for guy',
},
guy
);

· 閱讀時間約 6 分鐘
Guy Chien
  • 在 ES6 尚未流行前,大部分 JS 開發者都是使用 var 關鍵字做變數宣告,很多開發者在宣告函式時也比較習慣使用 function declaration 又稱作 function statement 做函式的宣告。
var cat = 'Civet';

// function declaration || function statement

function sayHi() {
var cat = 'Doraemon';
console.log("hello I'm " + cat);
}

sayHi();
  • 而以上程式碼相信大家腦袋中早已有答案,我們在 Devtool 中將會看到字串 hello I'm Doraemon ,但如果今天的情況變成是以下程式碼?那結果會是如何呢?
var cat = 'Civet';

// function declaration || function statement

function sayHi() {
console.log("hello I'm " + cat);
var cat = 'Doraemon';
}

sayHi();
  • 答案將會是 hello I'm undefined ,驚不驚喜,意不意外? cat 的值竟然不是向外找的 Civet 也沒有錯誤訊息說明 ReferenceError: cat is not defined 取而代之的值卻是 undefined ! 別緊張,通常一般教授 JS 的書籍會很貼心的先分解步驟給學者看,當然這裡也不意外。可以先想像上述的程式碼和下述的程式碼是等價的。
var cat = 'Civet';

// function declaration || function statement

function sayHi() {
var cat;
console.log("hello I'm " + cat);
cat = 'Doraemon';
}

sayHi();
  • 這樣看起來是不是相對合理多了呢!? 這也是為何用提升 ( Hoisting ) 這個詞來描述這種現象,從 sayHi 這個函式當中我們看見了 cat 這個變數被 “提” 早於函式一開始前就做宣告,但並沒有初始化,也就是賦予了這個變數記憶體位置卻沒有賦予它值。而 JS 在此種情況會給予該變數 undefined 的值。

  • 但為什麼會發生提升 ( Hoisting ) 這種現象呢? 這就要說到 JS 在運行時,會有兩個階段

  1. 創建階段 ( creation phase )

  2. 執行階段 ( execution phase )

  • 大家皆明白 JS 是由上而下一行一行的讀取程式碼,而在開始讀取第一行程式碼前,JS 會先進入創建階段,建立全域執行環境 (Global Execution Context),為接下來要運行的 JS 程式碼做準備,以上方程式碼為例,在建立階段會做以下的準備。
  • 會為變數 cat 先保留記憶體位置,以及用 function declaration 宣告的函式 sayHi 做記憶體分配,在此要注意的是這裏建立的 cat 並不是 sayHi 函式中的 cat,而是全域變數中的 cat 。 接著環境準備好後,JS 緊接著就會開始進入執行階段 ( execution phase ) ,執行第一行程式碼,也就是:
var cat = 'Civet';

而此時全域變數 cat 就在這行程式碼被賦予了值 Civet 。

接著繼續向下執行…

function sayHi() {
console.log("hello I'm " + cat);
var cat = 'Doraemon';
}

saiHi();

JS 遇見了第二個作用域 ( 別忘了JS 是以函式作為一等公民切分作用域的 ), 因此 JS 再次進入創建階段 開始進入建立局部區域執行環境 ( Local Execution Context ) ,這時並還沒有執行 sayHi 函式裡的任何一行程式碼,而是為了要執行 sayHi 做前置準備,JS 會將 sayHi 作用域裡的程式碼掃過一遍,看看有沒有使用 var 關鍵字做宣告的變數,並先分配給此變數記憶體,所以此時 cat 就佔了一席之地了,但尚未被初始化。

執行完創建階段後,就開始進入執行階段,先行執行了 console.log("hello I'm" + cat); 而此時局部變數 cat 仍然尚未賦值, 所以想當然就得到了 hello I'm undefined 的值啦!QQ

  • 而提升此種現象並非只出現在 var 關鍵字上,還會出現在上述的 function declaration 所宣告的函式上,所以如果遇見以下狀況別懷疑,是提升搞的鬼。明明就在還沒宣告前就 call sayGoodBye 為什麼還呼叫的到?原因就是 sayGoodBye 函式早已在建立全域執行環境 (Global Execution Context) 時,函式的內容已經做好了準備。
sayGoodBye();

function sayGoodBye() {
console.log('Bye Bye');
}
// output: Bye Bye

以上是我個人對於提升的見解,如果有誤,歡迎告訴我,我會盡快的更改有誤的內容,並且謝謝你讓我有機會更加的進步。

參考資料: JavaScript Visualizer

大家可以到上面這個網站模擬看看,很好玩的,不玩會後悔。

我知道你懂 hoisting,可是你了解到多深?
Javascript Execution Context 簡介