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