兩種資料消費方式:pull與push,陰與陽

csRyan發表於2020-06-15

pull和push,是在軟體中消費資料的兩種方式,它們描述了資料生產者(或持有者)資料消費者之間是如何通訊的。過去我們肯定了解過它們,不過可能會在程式設計中會忽略它們之間的區別與聯絡,本篇文章希望幫助大家理解這兩者的區別於聯絡,從而在程式設計中有意識地分辨與選擇它們。

我們可以用一個現實生活中的例子來理解pull與push:
你某天想要閱讀新聞,於是開啟瀏覽器,輸入新聞網站的地址,敲下回車,於是新聞內容展現在你的眼前。這是一個pull模型;
你也可以,下載一個新聞App,設定訊息推送功能,讓它時不時向你推送重要的新聞。這是一個push模型。

pull系統

在pull系統中,資料消費者決定自己何時請求並接收資料;資料持有者只能被動地響應請求。

程式語言的函式機制就是pull系統的例子。函式是資料生產者,呼叫者是資料消費者。呼叫者在自己需要的時候,呼叫函式,從函式中“拉”出一個結果,即let result = func(args);

JavaScript的Generator function則是pull系統的又一個範例,只不過消費者可以多次pull,依次拿出順序有關的多個結果:

function* generator(i) {
  yield i;
  yield i + 10;
}
const gen = generator(10);
const result1 = gen.next().value;
const result2 = gen.next().value;

Async Iterator與普通Iterator類似,是典型的pull模型,只不過pull的結果是非同步返回的:

async function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 1000));
    yield i;
  }
}
let asyncIterator = generateSequence(1, 5);
let result = await asyncIterator.next(); // {value: 1, done: false}
result = await asyncIterator.next(); // {value: 2, done: false}

陣列遍歷for (let x of iterable)for (let x of asyncIterable)也是兩種基於pull理念的語法。

push系統

在push系統中,資料生產者決定何時向消費者推送資料。資料消費者不知道何時會收到資料更新。

Promise是一個push系統,它的資料生產者是Promise中封裝的非同步邏輯,資料消費者則是then中的callback函式。生產者決定何時通知消費者。Promise生產者只能給消費者推送一次資料。

RxJS的Observable也是一個push系統,它的生產者能給消費者推送多次資料。

對比總結

pull與push的特點對比總結如下:

生產者 消費者
pull 被動。收到請求時返回資料 主動。決定何時請求資料
push 主動。決定何時推送資料 被動。響應資料的更新

兩者的典型範例總結如下:

單結果 多結果
pull Function 陣列遍歷、Iterator (Generator)
push Promise Observable

可以看出,陣列等Iterable,與Observable是既對等又相反的關係。

RxJS為push模型提供了統一的抽象組合手段;與之相對應的,IxJS為pull模型提供了統一抽象組合手段。這兩個庫的API甚至能夠一一對應起來。

pull與push,陰與陽

如果從上面的微觀的視角來看Promise,Promise本身確實是一個典型的push模型。但是如果從巨集觀的視角來看以下這段程式碼:

// 在檢視控制器中的某段程式碼:
requestServer().then((data) => {
    view.update(data);
});

// 等價於:
const data = await requestServer();
view.update(data);

又何嘗不是一種pull模型呢?畢竟檢視控制器是資料消費者,它主動從伺服器(資料生產者)請求資料並使用。

再換一個視角,如果從View的角度看,它被檢視控制器通知新資料的到來,View自己只能被動地對資料更新產生反應。這難道不是一種push模型嗎?

再換一個視角,上面的這段檢視控制器程式碼是什麼時候執行的呢?它總不可能在程式啟動的時候執行一次就完成任務了吧?這段程式碼必定也處在某個事件響應函式中(比如某個按鈕的點選事件回撥),或者某個元件生命週期鉤子中(比如onMounted)。那麼它作為一個事件響應函式,是不是必定處於一個push系統中?

再換一個視角,在requestServer中,向伺服器傳送請求的時候,底層需要通過DNS系統來解析域名,這個查詢過程是pull模型,即let ip = await resolveDNS('www.server.com');

再換一個視角,在作業系統底層,當伺服器響應從網路中到來的時候,作業系統喚醒了對應的執行緒,執行後續的程式碼。這個過程又是push模型。

可以看到,push與pull,它們作為編碼模型的兩種選擇,是相互競爭的,但是如果你站在不同的抽象層次上,總是能發現另一方的身影。這種“你中有我,我中有你,既相互競爭,又相互依存”的關係,像極了古代中國的哲學思想:陰陽
陰陽.jpg

參考資料

相關文章