【第1304期】聊一聊Redux的前身Flux

前端早讀課2018-06-16 18:29:31

前言

抽獎這東西,歡喜就好,祝各位端午節安康。今日早讀文章由網易考拉海購@Gloria投稿分享。

@Gloria,前端工程師,就職於網易考拉海購

正文從這開始~

現在諸多狀態管理方案湧現,每種方案的背後都有支撐其實現的思想,而這些思想並不是“空穴來風”,都是為了解決開發中出現的各種問題而誕生。

接下來的會深入探討時下比較流行的兩種狀態管理方案,Redux,Mobx。

為了深入瞭解Redux,不可避免地就要談到它的前身Flux。

概念

在正文開始之前,我們需要理解在平時使用諸如react.jsvue.js這類MV*框架時接觸到Model和View的概念。

MVVM兩個概念示意圖.PNG-3.2kB

一個完整的交互流程就如上圖所示。

View

View,意為“視圖”,即最終在瀏覽器上看到的頁面元素。

Model

Model,翻譯過來就是“模型”,那…什麼是“模型”呢?且看下面這些代碼。

<div>
   
<p>{{a.b}}</p>
   
<p>{{a.c}}</p>
   
<p>{{a.d}}</p>
</div>

上面這一段代碼,其中a.ba.ca.d,每當這些屬性值發生改變之後,框架會幫助我們生成View。

如果我們再稍微宏觀地看待這一問題,其實可以a這個對象看作是data(數據),而上面的html代碼就是template(模板),於是就有了這種理解:框架通過將data應用到template上,最後生成View,即b過程。

在這裏data+template就是Model,即所謂的“模型”,而通常意義上template是固定不變的,不會動態發生變化(這種動態變化已經被涵蓋在模板本身的語法中了),所以大多數時候我們實現的各種交互就是改變data上屬性的過程,示意圖中的a過程。

目前開發中存在的問題

ok,介紹完Model和View這兩個概念後,在這兩個抽象層面上談一談平時開發過程中遇到的問題。

碎片化修改

我們實現交互基礎就是操作Model,就拿上面那個代碼片段來説,操作Model就是修改a.ba.ca.d,於是操作這個Model就會像下圖所展示的情況一樣,修改操作會“碎片化”地存在於整個組件文件的各個角落。

碎片化.PNG-24.6kB

對於沒有嚴格開發模式限制的工程,一旦頁面複雜度上去了,如果多人維護這樣的代碼,添加feature的時候可以説會比較刺激了。

大多數情況的表情應該是這樣的

黑人問號.jpg-5.4kB
數據流捉摸不定

1. 複雜的數據流

先來談一談vue.js之類基於檢測數據變動實現局部更新的MVVM框架,這些框架提供了多種多樣影響Model的方式。

看一看這張圖

複雜數據流.PNG-13.7kB

最明顯的,跟上面那張圖相比,增加了從View到Model這一個方向,這種改變自然是框架“雙向數據綁定”所帶來的。毫無疑問,這種feature給我們帶來了一定的便利,但與此同時,它會使得最終生成View的邏輯更加撲朔迷離,為什麼這麼説呢?

從另外一個角度看待這個問題,最終到View的不同路徑數越多,就代表生成View的方式越多,生成View的方式越多,代碼的可預測性就越弱。

很顯然,在這張圖當中,以View做為終點的路徑還是不少的,以碎片化修改為起點的路徑有2條,以View作為起點的路徑有3條。

從路徑數量這個角度,很直觀地就可以得出這類框架設計對於代碼可維護性是不友好的。

2. 簡單的數據流

但是,也有一些框架數據流是比較簡單的(比如React),改變Model的方式僅限於手動調用setState,或者View觸發setState,在代碼的predicatable(可預測性)方面有比較大的優勢。

React數據流.PNG-12.3kB

OK,以上這些與這次的主題有什麼關係呢?

Flux

上面已經談到了現在MV*框架中存在的問題,比如vue,react等都僅僅是視圖層框架,也就是説,它們只負責渲染View,而對於Model的變化沒有統一的管理方案。

Flux的出現其實就是為了管理Model的變化,使得應用的可伸縮性,和代碼的可預測性更強。

單向數據流基礎

Flux其實就是在React單向數據流的基礎上做了一層對Model的管理,那就看一看它是如何借鑑的。

單向數據流基礎.PNG-17.9kB

相比其它框架設計,最大的不同之處就是:React沒有View-->Model這個方向。就拿上面複雜數據流方案來説,以View為起點的數據流路徑就可以減少兩條,保證了最終生成View的邏輯是相對清晰的。

如何看待Flux架構

Flux其實提供了一整套Model修改模式。這種模式的初衷,在我看來,就是為了提高代碼的可預測性,再通俗一點就是,當你看到了一段代碼時,讓你更清晰地知道它會做什麼。

為什麼這麼説呢?我們在維護工程時無外乎就是扮演兩個角色:使用者和定義者。而往往我們在代碼中確很少體現這兩種角色抽象,最多也只是在文檔和代碼規範層面,任你玩出花來,也很難做到比較高的通用性。

再具體一點,Flux將使用者和定義者的抽象引入了Model的修改過程。類似Clent-Service架構,如果使用者(客户端)想要修改數據庫,必須通過調用定義者(服務端)提供的接口實現。

1. 請求

在Flux中,request(請求)等價於action,觸發一個action相當調用一次接口,action的type字段相當於接口地址,其它字段相當於payLoad(請求參數)。

action應該是一個對象:

{
   type
: 'delete-todo',   //接口地址
   todoID
: '1234'      //payLoad
};

既然將action當作了request,那麼我們應該如何實現server(服務器)呢?

2. 路由

就像Clent-Service中一樣,server接收請求並將不同的請求映射為相應的數據庫修改操作。將server中接收請求的部分稱為router(路由)。

一個router應該長這樣:

let router = (function router(){
   let dataBase
= {todos: []}; //模擬數據庫的對象
   
return function(request){
       
switch(request.type){
           
case 'ADD_TODO': deleteToDo(request, dataBase); break;
           
...
       
}
   
};
})();

發送一個請求:

router({type: 'delete-todo', todoID: '1234'});

deleteToDo()其實就是相應修改數據庫的操作,裏面的具體邏輯需要我們自己寫,顯然,刪除一個”待辦事項”,deleteToDo()應該長下面這樣:

function deleteToDo(request, dataBase){
   let todos
= dataBase.todos;
   
for(let i = 0; i < todos; i++){
       
if(todos[i].id === request.todoID){
           todos
.splice(i, 1);
           
return;
       
}
   
}
}

ok,到目前為止,整個流程已經跑通了。定義一個request,使用router發送這個request,router根據request地址分配相應的數據庫處理邏輯,於是就得到了下面這種抽象:

單dataBase架構.PNG-7kB

用上面這種架構已經可以勉強駕馭一些比較簡單的應用場景,而面對稍微複雜一點的應用場景就捉襟見肘了,為什麼這麼説呢?

這種架構最基本的應用單元就是組件,每個組件的Model其實就是對應的dataBase,如果我們想在某個組件內修改其它組件的dataBase,就需要拿到這個組件的router,而”拿router”這件事可並沒有那麼簡單。。大體上根據組件之間的關係,分為3種情況:父子關係、爺孫關係和兄弟關係,於是就會出現下面這種情況。

多dataBase架構.PNG-23.1kB

為了解決這一問題,Flux的另一個概念就來了,dispatcher。

3. 請求分發器

Flux的dispatcher(請求分發器),其實解決了上述問題。

dispatcher相對各個組件而言是全局性的,它可以將請求發送到所有的router,用户無需知道他需要請求的router,讓每個router自行處理進來的request,這種抽象其實是將request視為全局性請求,一個request可以同時操作多個dataBase。

引入dispatcher.PNG-16.7kB

當然,dispatcher不會自己尋找它需要分發到的router,我們需要調用register()方法手動註冊router

dispatcher.register(router);

在註冊好router後,直接調用dispatcher的dispatch()方法即可,可以像下面這樣發送一個request:

dispatcher.dispatch({type: 'delete-todo', todoID: '1234'});

默認情況下,Flux會按照註冊順序依次將request放進router。如果我們希望自定義發送request後,部分router的執行順序怎麼辦?Flux提供了waitFor()方法。

舉個例子:routerA接收到請求之後,希望依次經過routerB,和routerC,可以像下面偽代碼這樣實現:

let tokenB = dispatcher.register(routerB);
let tokenC
= dispatcher.register(routerC);
let routerA
= function(request){
   
switch(request.type){
       
case 'ADD_TODO': dispatcher.waitFor(tokenB, tokenC); break;
       
...
   
}
};

OK,你必須提前拿到routerB和routerC的token,然後按照順序傳入waitFor()方法(個人認為這種”拿token”,無異於上面提到的”拿router”,是一個設計缺陷)。

4. 數據庫

dataBase(數據庫)其實就代表了組件的state(狀態)。

而Flux將router和dataBase視為一體,將請求的解析和數據庫的修改統一交給store來處理。

store.reduce()相當於router,而store._state則相當於dataBase,於是就有了下面這種架構

store架構.PNG-19kB

最後,Flux採用了向外拋事件的方式,將_state映射到Model的工作交給用户去解決。

你可以調用store.addListener()方法,傳入回調函數即可監聽到_state的變化。

store.addListener(() => {
   let state
= store.getState();
   
...映射到Model的操作...
});

結語

Flux的一整套抽象(action,dispatcher,store),在單向數據流的基礎上可以提高應用的可維護性和代碼的可預測性。然而,全局action+多store的架構面對複雜的應用依然不能很好地解決複雜數據流的問題,waitFor()雖然可以滿足自定義多store接收action的順序,但是它會讓數據流變得複雜,難以維護。

Redux作為Flux的繼承者,單store的架構其實就很好地避免了上述問題,之後的文章會深入分析Redux是如何在Flux的基礎上改進自身架構的。

參考:

Flux官方介紹:In Depth Overview

Flux官方倉庫:github.com/facebook/flux

關於本文

作者:@Gloria

原文:

https://zhuanlan.zhihu.com/p/38050036

最後,為你推薦


【第1265期】那些前端MVVM框架是如何誕生的

閲讀原文

TAGS:狀態管理方案碎片化修改框架可以