Reflux原理与源码详解

深度好文,本文转载至: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
});
// 是否是异步的Action
var 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');

// 测试片段2
A('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中说到的回调defaultCallback
module.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
};
};

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值