深度好文,本文轉載至:https://yq.aliyun.com/articles/61068
Reflux是Flux模式的一種具體實現。本文從一開始就分別介紹了Flux模式和Reflux的設計原理。之后,又對源碼進行深入剖析,將Reflux拆分成發布者和訂閱者的公共方法、Action和Store的實現、發布者隊列和View的設計等四個方面,并逐一解讀。
Flux模式介紹 Flux是Facebook提出的一種構建web應用的模式,用以幫助開發者快速整合React中的視圖組件。在整個流程中,數據從應用上層到底層,從父組件到子組件,單向流動 (unidirectional data flow)。它由Dispacther、Store、View三個主要部分構成??聪旅孢@張圖
╔═════════╗ ╔════════════╗ ╔═══════╗ ╔══════╗ ║ Action ║──────>║ Dispatcher ║──────>║ Store ║──────>║ View ║ ╚═════════╝ ╚════════════╝ ╚═══════╝ ╚══════╝ ^ ╔════════╗ │ └────────── ║ Action ║ ──────────┘ ╚════════╝ 通過這張圖,我們可以大概的了解什么是Flux模式。
Action收集了視圖變更的行為,比如用戶點擊了按鈕、需要定時發送的請求,然后通知Dispatcher
Dispatcher是一個單例,是一個根據不同Action,觸發對應的回調,維護Store
Store是一個數據中心,只有Store的變化才能直接引發View的變化
Action一直處于就緒狀態,以上三步周而復始
這種設計雖然提高了Store管理的復雜度,但能夠使得數據狀態變得穩定、可預測。由于Flux不是本文的重點,此處有簡化,需要了解更多的話,請訪問官網的Flux介紹。
Reflux是Flux模式的一種實現。不過略有區別。
╔═════════╗ ╔════════╗ ╔═══════╗ ║ Action ║──────>║ Store ║──────>║ View ║ ╚═════════╝ ╚════════╝ ╚═══════╝ ^ │ └─────────────────────────────────┘
Reflux實現了單向數據流,也實現了Flux中提及的Action和Store。它將Action和Dispatcher合并到了一起。Dispatcher不再是一個全局的單例,大大的降低了編碼復雜度和維護的難度和復雜度。一個Action就是一個Dipatcher,可以直接引發Store的變化。Store可以監聽Action的變化。此外,如果有Store互相依賴的情況,那么Store可以直接監聽Store。
說到這里,聰明的你看到我說到“監聽”兩個字,肯定就大概猜到Reflux的代碼大概是怎么寫的。沒錯,Reflux這種設計,就是典型的訂閱發布模式。
在Reflux中,每一個Action都是一個發布者Publisher,View是一個訂閱者Listener。而Store比較特殊,它監聽Action的變化,并引發View的改變,所以它既是一個發布者,又是一個訂閱者。
Reflux的核心代碼都在reflux-core這個庫文件里面,我們可以通過npm install reflux-core下載到本地。入口文件index.js和其他模塊,都在lib文件夾里面。index.js引入了lib下面的大部分文件,并將文件對應的方法掛載在Reflux這個變量下面。大概分成下面幾類:
Reflux的版本信息和公共方法 發布者和訂閱者的公共方法 創建Action和Store Reflux的發布者隊列 后面三塊是Reflux的實現核心,我們后面依次會講到。
在這些模塊中,并沒有涉及到View,說明Relfux是一種純粹的Flux思想的實現方式,可以脫離React與其他的框架一起使用。View的設計,都在refluxjs這個庫里。我們可以通過npm install refluxjs下載代碼到本地。
發布者和訂閱者的公共方法
Reflux中的Action、Store、View其實只有兩種角色,一個是發布者Publisher,一個是訂閱者Listener。于是,Reflux將這兩種角色的公共方法抽象成了兩個模塊PublisherMethods.js和ListenerMethods.js。我們分別來看:
這個文件保存了發布者的公共方法,也就是Action和Store作為發布者都有的方法。文件的返回值是一個如下的對象:
module.exports = { // 觸發之前的回調, 在shouldEmit之前執行 PReEmit: function(){...}, // 是否能夠觸發,返回boolean值 shouldEmit: function(){...}, // 設置監聽事件,觸發后執行 listen: function(){...}, // 當shouldEmit的執行結果為true時,立即執行 trigger: function(){...}, // 當shouldEmit的執行結果為true, 盡快執行 triggerAsync: function(){...}, // 為trigger包裹一層函數defer函數 deferWith: function(){...}}在trigger執行之前,首先會先執行preEmit和shouldEmit回調。preEmit用于修改發布者傳過來的參數,并將返回值會傳給shouldEmit。由shouldEmit的返回值true或者false判斷是否觸發。
listen和trigger listen方法和trigger方法是配套的。先看listen,里面有兩行比較關鍵:
this.emitter.addListener(this.eventLabel, eventHandler);... me.emitter.removeListener(me.eventLabel, eventHandler);...我們在trigger這個方法中,看到代碼
...this.emitter.emit(this.eventLabel, args);...而this.emitter,在后面我們會看到,他就是EventEmitter的一個實例。EventEmitter這個庫,是用作對象注冊和觸發相關事件的。所以listen和trigger兩個方法的意思已經很清楚了。就是listen方法的作用就是注冊監聽,返回一個可以解除注冊事件的函數。而trigger則是觸發事件的方法。
這兩個方法比較有意思,一個是立即執行,一個是盡快執行。什么意思呢。我們看util.js中的對應代碼:
_.nextTick(function () { me.trigger.apply(me, args);});而這個所謂的_.nextTick實際上是這個:
setTimeout(callback, 0);那么實際上就是:
triggerAsync: function(){ let me = this; let args = arugments; setTimeout(function(){ me.trigger.apply(me, args) }, 0);}triggerAsync的設計,主要是為了解決一些異步操作導致的問題。這里我用Uxcore舉個例子。在Uxcore的Form有個重置所有的FormField的方法叫resetValue。它的實現原理是這樣的:Form本身保存了一份原始值,調用resetValues的時候,會把這份原始值異步賦給各個FormField。所以,如果在下面這個場景中,繼續調用trigger,就不會獲得預期效果。要改用triggerAsync。
// User.Search用來搜索符合條件的員工let User = Reflux.createActions({ Search: { children: ['reset', 'do', ...] }});// 調用resetValues,清空搜索表單的值User.Search.reset();// 用初始值搜索一次// 下面這個不會取得預期效果// 這個與User.Search.do()效果相同User.Search.do.trigger();// 要用這個// User.Search.do.triggerAsync();deferWith deferWith重寫了trigger方法。把之前的trigger保存到變量oldTrigger中,并將其作為第一個參數傳遞給deferWith的第一個參數callback,剩下的參數依次傳遞。舉個例子,如果我們執行的是
deferWith(fn, a, b, c)那么,trigger方法就會變成
function(){ fn.apply(this, [oldTrigger, a, b, c]); }這個文件保存了訂閱者的公共方法,也就是Store和View作為訂閱者都有的方法。文件的返回值是一個如下的對象:
module.exports = { // 這個是給validateListening使用的工具方法 hasListener: function(){...}, // 多次調用listenTo, 一次性設置多個監聽 listenToMany: function(){...},, // 這個是給listenTo使用的工具方法 // 校驗監聽函數是否是合法, 比如 // 是否監聽自己,是否通過函數監聽,是否循環監聽 validateListening: function(){...}, // 設置監聽函數 listenTo: function(){...}, // 停止監聽 stopListeningTo: function(){...}, // 停止所有監聽 stopListeningToAll: function(){...}, // 這個是給listenTo使用的工具方法 // 執行發布者的getInitialState方法 // 并以其返回值為參數,執行一個默認的回調defaultCallback fetchInitialState: function(){...}, //下面這四個方法,就是Reflux中發布者隊列了,我們后面來說 joinTrailing: maker("last"), joinLeading: maker("first"), joinConcat: maker("all"), joinStrict: maker("strict")}這個文件有一個核心方法,就是listenTo。它連接了發布者和訂閱者。我們看源代碼:
listenTo: function(listenable, callback, defaultCallback){ ... //訂閱者的數組,保存了所有的訂閱者信息 subs = this.subscriptions = this.subscriptions || []; ... subscriptionobj = { // unsubscriber是一個取消監聽的函數, // 也是stopListeningTo能夠取消監聽的原因 stop: unsubscriber, // listenable指的是發布者,就是誰被監聽 listenable: listenable }; // 把subscriptionobj對象push進訂閱者數組里 subs.push(subscriptionobj); return subscriptionobj;}Action相關的方法被放在ActionMethods.js和createAction.js兩個文件中。另外,index.js文件也定義了同時創建多個Action的createActions方法。
ActionMethods這個模塊代碼只有最簡單的一行
module.exports = {};但是作用可不簡單,它給所有的Action設置了公共的方法,可以在你需要的時候隨時調用。ActionMethods在index.js中被直接掛在了Reflux下面。所以你可以直接使用。
比如說我們定義一個
Reflux.ActionMethods.alert = function (i) { alert(i);};var showMsg = Reflux.createAction();那么你可以這么使用:
showMsg.alert('Hello Reflux!');這樣就會直接彈出一個alert框。非常粗暴,也非常實用。
我們知道createAction用法有這幾個
// 空參數創建var TodoAction1 = Reflux.createAction();// 立即執行還是盡快執行var TodoAction2 = Reflux.createAction({ sync: true});// 是否是異步的Actionvar TodoAction3 = Reflux.createAction({ asyncResult: true});// 設置子方法var TodoAction4 = Reflux.createAction({ children: ['success', 'warning']});// TodoAction5是一個有多個Action的數組var TodoAction5 = Reflux.createAction(['create', 'retrieve', {update: {sync: true}}]);...我們再跟一下源碼,看是怎么做的。createAction方法一開始就有兩個for循環,用以檢驗要Action的名稱合法性,不能與Reflux.ActionMethods中的方法重名,也不能與已定義過的Action重名,我們假設叫做TodoAction。
源碼如下:
var createAction = function createAction(definition) { ... // 省略校驗的代碼 ... // 定義子Action definition.children = definition.children || []; // 如果是一個異步的操作,那么就額外給其加上兩個子Action,completed和failed if (definition.asyncResult) { definition.children = definition.children.concat(["completed", "failed"]); } // 這里是是個遞歸,生成所有的子Action // 將所有的children遍歷一遍,為每一個都執行createAction方法 var i = 0, childActions = {}; for (; i < definition.children.length; i++) { var name = definition.children[i]; childActions[name] = createAction(name); } // 將發布者的公共方法,Action公共的方法和當前要創建的TodoAction的配置merge到一起 var context = _.extend({ eventLabel: "action", emitter: new _.EventEmitter(), _isAction: true }, PublisherMethods, ActionMethods, definition); // 設置如果把當前要創建的Action TodoAction當做函數直接執行的策略 // 如果sync為true,那么執行TodoAction()就相當于執行TodoAction.trigger() // 反之,就相當于執行TodoAction.triggerAsync() var functor = function functor() { var triggerType = functor.sync ? "trigger" : "triggerAsync"; return functor[triggerType].apply(functor, arguments); }; //繼續合并 _.extend(functor, childActions, context); //將生成的Action,保存進Keep.createdActions數組里面 Keep.createdActions.push(functor); return functor;}module.exports = createAction;創建多個Action,我們一般有兩種用法:
// 參數是數組var TextActions1 = Reflux.createActions(['create', 'retrieve', 'update', 'delete']);// 參數是對象var TextActions2 = Reflux.createActions({ 'init': { sync: true }, 'destroy': { asyncResult: true }});所以,index.js中的createActions,其實就是判斷參數是否是一個數組,如果是,就對每一個數組項都調用一次createAction方法。反之,就當成一個key-value型的對象處理。所有的key都作為Action的名稱,所有的value都作為對應Action的配置。
Store相關的方法被放在StoreMethods.js和createStore.js兩個文件中。
StoreMethods這個模塊與ActionMethods類似,代碼只有最簡單的一行
module.exports = {};但是作用可不簡單,它給所有的Store設置了公共的方法。
createStore與createAction也很類似。createStore方法一開始也有兩個for循環,用以檢驗要Store的名稱合法性,不能與Reflux.StoreMethods中的方法重名,也不能與已定義過的Store重名。我們來看具體的代碼:
module.exports = function (definition) { var StoreMethods = require("./StoreMethods"), PublisherMethods = require("./PublisherMethods"), ListenerMethods = require("./ListenerMethods"); // 這里與createAction一樣,是校驗Store名稱的合法性 ... // 這里是Store的核心方法 function Store() { var i = 0, arr; // 同樣的 訂閱者數組 this.subscriptions = []; // 這就是我們之前在PublisherMethods中講過的emitter this.emitter = new _.EventEmitter(); ... // 如果有init方法,則執行 // 如果沒有用listenToMany設置監聽方法,那么就需要在init中設置listenTo了 if (this.init && _.isFunction(this.init)) { this.init(); } // 如果有訂閱的回調,則執行ListenMethods中的方法監聽 if (this.listenables) { arr = [].concat(this.listenables); for (; i < arr.length; i++) { this.listenToMany(arr[i]); } } } // 這里是核心的一步,給Store的原型上merge進訂閱者、發布者、Store的公共方法和當前創建的Store的配置 _.extend(Store.prototype, ListenerMethods, PublisherMethods, StoreMethods, definition); // 實例化Store var store = new Store(); // 把sotre放入一個公共的數據,方便統一管理 Keep.createdStores.push(store); return store;};剛才在ListenMethods中,訂閱者可以訂閱多個發布者的消息,這些發布者形成了一個隊列。如果發布者隊列遇到插隊的問題怎么辦呢?舉個例子,S順序訂閱了A和B。如果執行完A(‘a’),B(‘b’)即將執行的時候,用戶插入了A(‘A’),。那么S怎樣處理A(‘a’)、A(‘A’)和B(‘b’)的執行結果呢?
Reflux提出了joinTrailing、joinLeading、joinConcat、joinStrict四種處理策略,分別對應了last、first、all、strict四種邏輯, 亦即,執行A(‘A’)->B(‘b’)、A(‘a’)->B(‘b’)、A(‘a’)->A(‘A’)->B(‘b’)、A(‘a’)執行后報錯。上一個的執行結果,會傳給下一個。
因為這個相對較少使用,我在這里以Action為發布者,Store為監聽者為例寫一段代碼,用以幫助理解。
var A = Reflux.createAction();var B = Reflux.createAction();var Store = Reflux.createStore({ init: function() { let me = this; // 這里要根據需要設置成不同的策略 me.joinStrict(A, B, me.trigger); }});Store.listen(function() { console.log('result:', JSON.stringify(arguments));});// 測試片段1//A('a');//A('A');//B('b');//B('B');// 測試片段2A('a');B('b');A('A');B('B');在這段代碼中,把A和B形成了一個隊列。執行順序為A->B。對不同策略分別執行測試片段1和測試片段2。
測試片段1 Uncaught Error: Strict join failed because listener triggered twice. result: {“0”:[“a”],”1”:[“b”]} 測試片段2 result:{“0”:[“a”],”1”:[“b”]} result:{“0”:[“A”],”1”:[“B”]} 結論 A->B之間,如果插入了A,就會執行第一個A,同時拋出一個錯誤,停止執行。
測試片段1 result: {“0”:[“a”],”1”:[“b”]} 測試片段2 result: {“0”:[“a”],”1”:[“b”]} result: {“0”:[“A”],”1”:[“B”]}
結論 A->B之間,如果插入了A,就執行第一個A,跳過后面的。 第一個A的執行結果,作為參數傳遞給B。B依照這個邏輯,繼續執行。
測試片段1 result: {“0”:[“A”],”1”:[“b”]} 測試片段2 result: {“0”:[“a”],”1”:[“b”]} result: {“0”:[“A”],”1”:[“B”]}
結論 A->B之間,如果插入了A,就執行后一個A,跳過前面的。 后一個A的執行結果,作為參數傳遞給B。B依照這個邏輯,繼續執行。
測試片段1 result: {“0”:[[“a”],[“A”]],”1”:[[“b”]]}
測試片段2 result: {“0”:[[“a”]],”1”:[[“b”]]} result: {“0”:[[“A”]],”1”:[[“B”]]}
結論 A->B之間,如果插入了A,就再執行一次A。 兩個A的執行結果,放到一個數組里面,作為參數都傳遞給B。B依照這個邏輯,繼續執行。
這里我們簡單做一個總結。
策略 邏輯 遇到插隊時 是否繼續執行 joinStrict strict 拋出錯誤 否 joinLeading first 執行第一個 是 joinTrailing last 執行后一個 是 joinConcat all 都會執行 是 這四種策略,都定義在joins.js文件里面。我們看一段核心代碼:
// 返回一個函數// 該函數根據不同的策略,確定不同的后面監聽函數的參數function newListener(i, join) { return function () { var callargs = slice.call(arguments); // 對應的監聽若果尚未被觸發,就根據相應的策略來確定該監聽的參數 if (join.listenablesEmitted[i]) { switch (join.strategy) { // 如果是strict的,則只能執行一次,拋出錯誤 case "strict": throw new Error("Strict join failed because listener triggered twice."); // 如果是last的,則監聽函數的參數就為該函數的參數 case "last": join.args[i] = callargs;break; // 如果是all的,則監聽函數的參數是之前執行過的所有監聽的返回值構成的數組 case "all": join.args[i].push(callargs); } } else { // 設置監聽已觸發 join.listenablesEmitted[i] = true; join.args[i] = join.strategy === "all" ? [callargs] : callargs; } // 所有的監聽都觸發后執行join.callback,并重置隊列 // 這里打個斷點,可以幫助我們更好的理解上面的示例代碼 emitIfAllListenablesEmitted(join); };}...發布者隊列類似于Flux模式中的waitFor設計,具有非常廣泛的使用場景:
請求完一個接口后,繼續請求一個接口 新手引導 先出現loading提示,再請求接口,最后取消loading或者顯示loaded 一個的處理結果,需要等待另一個的處理結果 …
我們前文分析過,View是一個訂閱者。那么View就要有ListenerMethods的所有方法。因為我們的View層是基于React框架的,那么訂閱和發布d的消息,應該在對應的生命周期里發生。源碼中也確實是這么實現的。
在實際使用中,我們一般通過mixins,將Reflux和React聯系在一起。這樣,Reflux就可以在React對應的生命周期執行對應的操作。下面依舊從refluxjs的入口文件src/index.js分析。index.js中,也給Reflux變量掛載了幾個方法。這幾個方法在設計上是比較雷同的,一般是分兩步。第一步,是在componentDidMount的時候,注冊監聽;第二步,則是在componentWillUnmount的時候,移除所有的監聽。我們分開來看。
ListenerMixin是View其他方法所共用的,類似ListenerMethods。
...module.exports = _.extend({ componentWillUnmount: ListenerMethods.stopListeningToAll}, ListenerMethods);它返回一個merge了ListenerMethods的對象。這個對象明確要求,組件要卸載(移除)的時候取消所有注冊的監聽。
listenTo方法將某個Store與組件的某個方法關聯起來。當Store變化時,就調用設置的回調callback。
...// 這里的三個參數實際上就是// 要監聽的 store// store 變化后要執行的回調 callback// initial 是計算完初始值后執行的回調(一般不需要)// 這個就是剛才fetchInitialState中說到的回調defaultCallbackmodule.exports = function(listenable,callback,initial){ return { ... componentDidMount: function() { ... // 通過 listenTo 注冊監聽 this.listenTo(listenable,callback,initial); }, ... // 通過 stopListeningToAll 取消所有監聽 componentWillUnmount: ListenerMethods.stopListeningToAll };};listenTo方法的實現方式很簡單了,在組件加載完成的時候,注冊監聽,在組件要卸載的時候,取消監聽。
listenToMany與listenTo基本一樣。區別就是listenToMany調用了ListenerMethods的listenToMany,可以同時注冊多個監聽。
module.exports = function(listenables){ return { componentDidMount: function() { ... // 通過 listenToMany 注冊監聽 this.listenToMany(listenables); }, ... // 通過 stopListeningToAll 取消所有監聽 componentWillUnmount: ListenerMethods.stopListeningToAll };};connect方法可以將組件的某一部分state,與指定的Store上。當Store變化的時候,組件的state也同步更新。
// listenable 指的就是要監聽的store// key 則為與store綁定后,需要變化的state[key]的key// 也就是說,store變化后,state[key]也同步變化module.exports = function(listenable, key) { // 如果事件沒有key,則直接報錯 _.throwIf(typeof(key) === 'undefined', 'Reflux.connect() requires a key.'); return { // 獲取state初始值 // 因為是mixin到React中的,所以比React中的getInitialState要先執行 getInitialState: function() { ... }, componentDidMount: function() { var me = this; // 依然是給React 混入ListenerMethods的方法 _.extend(me, ListenerMethods); // 設置監聽 this.listenTo(listenable, function(v) { me.setState(_.object([key],[v])); }); }, // 這里其實就是取消所有的監聽 componentWillUnmount: ListenerMixin.componentWillUnmount };};connectFilter與connect設計思路基本類似,只不過每次在state的值被被setState前,都會執行一個filterFunc函數來做處理。connectFilter的設計,既能夠幫助開發人員保護state不被污染,又能夠減少不必要的更新。
module.exports = function(listenable, key, filterFunc) { // 省略部分是校驗key值的合法性 … return { // 獲取state初始值 getInitialState: function() { … // 這里是與上一節的connect方法不同的地方 // 在返回state的之前,先執行filterFunc函數 var result = filterFunc.call(this, listenable.getInitialState()); … }, componentDidMount: function() { … this.listenTo(listenable, function(value) { // setState前先處理 var result = filterFunc.call(me, value); me.setState(_.object([key], [result])); }); }, // 取消所有的監聽 componentWillUnmount: ListenerMixin.componentWillUnmount }; };
新聞熱點
疑難解答