首页 > 网站 > WEB开发 > 正文

Reflux原理与源码详解

2024-04-27 15:10:47
字体:
来源:转载
供稿:网友

深度好文,本文转载至: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原理分析

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的核心代码都在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。我们分别来看:

PublisherMethods

这个文件保存了发布者的公共方法,也就是Action和Store作为发布者都有的方法。文件的返回值是一个如下的对象:

module.exports = { // 触发之前的回调, 在shouldEmit之前执行 PReEmit: function(){...}, // 是否能够触发,返回boolean值 shouldEmit: function(){...}, // 设置监听事件,触发后执行 listen: function(){...}, // 当shouldEmit的执行结果为true时,立即执行 trigger: function(){...}, // 当shouldEmit的执行结果为true, 尽快执行 triggerAsync: function(){...}, // 为trigger包裹一层函数defer函数 deferWith: function(){...}}

preEmit和shouldEmit

在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则是触发事件的方法。

trigger和triggerAsync

这两个方法比较有意思,一个是立即执行,一个是尽快执行。什么意思呢。我们看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]); }

ListenMethods

这个文件保存了订阅者的公共方法,也就是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和Store

创建Action的模块

Action相关的方法被放在ActionMethods.js和createAction.js两个文件中。另外,index.js文件也定义了同时创建多个Action的createActions方法。

ActionMethods

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

我们知道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;

createActions

创建多个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的模块

Store相关的方法被放在StoreMethods.js和createStore.js两个文件中。

StoreMethods

StoreMethods这个模块与ActionMethods类似,代码只有最简单的一行

module.exports = {};

但是作用可不简单,它给所有的Store设置了公共的方法。

createStore

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;};

Reflux的发布者队列

刚才在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。

joinStrict

测试片段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,同时抛出一个错误,停止执行。

joinLeading

测试片段1 result: {“0”:[“a”],”1”:[“b”]} 测试片段2 result: {“0”:[“a”],”1”:[“b”]} result: {“0”:[“A”],”1”:[“B”]}

结论 A->B之间,如果插入了A,就执行第一个A,跳过后面的。 第一个A的执行结果,作为参数传递给B。B依照这个逻辑,继续执行。

joinTrailing

测试片段1 result: {“0”:[“A”],”1”:[“b”]} 测试片段2 result: {“0”:[“a”],”1”:[“b”]} result: {“0”:[“A”],”1”:[“B”]}

结论 A->B之间,如果插入了A,就执行后一个A,跳过前面的。 后一个A的执行结果,作为参数传递给B。B依照这个逻辑,继续执行。

joinConcat

测试片段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是一个订阅者。那么View就要有ListenerMethods的所有方法。因为我们的View层是基于React框架的,那么订阅和发布d的消息,应该在对应的生命周期里发生。源码中也确实是这么实现的。

在实际使用中,我们一般通过mixins,将Reflux和React联系在一起。这样,Reflux就可以在React对应的生命周期执行对应的操作。下面依旧从refluxjs的入口文件src/index.js分析。index.js中,也给Reflux变量挂载了几个方法。这几个方法在设计上是比较雷同的,一般是分两步。第一步,是在componentDidMount的时候,注册监听;第二步,则是在componentWillUnmount的时候,移除所有的监听。我们分开来看。

ListenerMixin

ListenerMixin是View其他方法所共用的,类似ListenerMethods。

...module.exports = _.extend({ componentWillUnmount: ListenerMethods.stopListeningToAll}, ListenerMethods);

它返回一个merge了ListenerMethods的对象。这个对象明确要求,组件要卸载(移除)的时候取消所有注册的监听。

listenTo

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

listenToMany与listenTo基本一样。区别就是listenToMany调用了ListenerMethods的listenToMany,可以同时注册多个监听。

module.exports = function(listenables){ return { componentDidMount: function() { ... // 通过 listenToMany 注册监听 this.listenToMany(listenables); }, ... // 通过 stopListeningToAll 取消所有监听 componentWillUnmount: ListenerMethods.stopListeningToAll };};

connect

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

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 }; };


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表