基于源码解析 Flux 架构

Flux

Flux 是 Facebook 在 2014 年提出的 Web 应用架构,其核心思想是将应用划分为 action 层、dispatcher 层、store 层和 view 层。

整个应用的运行方式是,用户页面交互行为触发特定类型的 action,dispatcher 会携带特定的 action type 和数据到 store 层,store 层会处理不同 action 类型下的数据变更逻辑,再把变动更新到 view 层。

flux framework

源码分析

Actions

Actions 就是一个描述操作类型和携带数据的对象,可以设计为一个 Action Creator;

var todoDispatcher = new Dispatcher();

var Actions = {
    addTodo(text) {
        todoDispatcher.dispatch({
            type: 'ADD_TODO',
            text
        });
    }
}

Dispatcher

Dispatcher 在 Flux 架构中扮演的角色是连接 Actions 和 Stores,Dispatcher 提供两个方法,dispatcher.register 用于注册处理 state 变更和 view 更新的逻辑,dispatcher.dispatch 用于触发 dispatcher 注册的各个回调;

Dispatcher 在 Flux 架构中是一个单例,所有 stores 都绑定在一个 dispatcher;

var _prefix = 'ID_';

class Dispatcher {
    constructor() {
        this._isDispatching = false;
        this.isPending = {};
        this._isHandled = {};
        this._pendingPayload = null;
        this._lastID = 1;
        this._callbacks = {};
    }

    register(callback) {
        var id = _prefix + this._lastID++;
        this._callbacks[id] = callback;
        return id;
    }

    // 同一个 payload 将会作为所有 callback 的 payload
    dispatch(payload) {
        this._startDispatching(payload);
        try {
            for (var id in this._callbacks) {
                if (this._isPending[id]) continue;
                this._invokeCallback(id);
            }
        } finally {
            this._stopDispatching();
        }
    }
    _startDispatching(payload) {
        for (var id in this._callbacks) {
            this._isPending[id] = false;
            this._isHandled[id] = false;
        }
        this._pendingPayload = payload;
        this._isDispatching = true;
    }
    _stopDispatching() {
        delete this._pendingPayload;
        this._isDispatching = false;
    }
    _invokeCallack(id) {
        this._isPending[id] = true;
        this._callbacks[id](this._pendingPayload);
    }
}

class TodoStore extends ReduceStore {
    constructor() {
        super(todoDispatcher);
    }

    getInitialState() {
        // todo
        return null;
    }

    reduce(state, action) {
        switch(action.type) {
            case 'ADD_TODO':
                if (!action.text) return state;
                const id = uuid.v4();
                return state.set(id, {
                    id,
                    text: action.text,
                });
            default:
                return state;
        }
    }
}

export default new TodoStore();

Store

Flux 架构在创建业务 Store 的时候,将全局单例 dispatcher 和 store 进行绑定,并在 dispatcher 中注册回调函数;

dispatcher.dispatch 调用时,store 中注册的回调函数会触发,遍历和调用 dispatcher._callbacks 的所有回调;

回调函数会调用 store.reduce 方法,如果产生新的 state,则会发送变更事件;

注意,这里的 reduce 是一个纯函数,当 state 发生变化时,会返回一个新的 Immutable 对象,如果没有发生变化则返回原来的 Immutable 对象,根据 Immutable 对象是否发生变化触发后续渲染更新;

Store 可以根据业务分割不同的特定 Store

// ReduceStore
class FlexReduceStore extends FluxStore {
    constructor(dispatch) {
        super(dispatch);
        this._state = this.getInitialState();
    }
    getState() {
        return this._state;
    }
    /**
   * Checks if two versions of state are the same. You do not need to override
   * this if your state is immutable.
   */
  areEqual(one: TState, two: TState): boolean {
    return one === two;
  }
    // 抽象方法
    getInitialState() {}
    // 抽象方法
    reduce() {}
    __invokeOnDispatch() {
        this.__changed = false;
        const startingState = this._state;
        const endingState = this.reduce(startingState, action);

        if (!this.artEqual(startingState, endingState)) {
            this._state = endingState;
            this.__emitChanged();
        }

        if (this.__changed) {
            this.__emitter.emit(this.__changedEvent);
        }
    }
}

// FluxStore
class FluxStore {
    constructor(dispatcher) {
        this.__changed = false;
        this.__changeEvent = 'change';
        this.__dispatcher = dispatcher;
        this.__emitter = new EventEmitter;
        this._dispatchToken = dispatcher.register((payload) => {
            this.__invokeOnDispatch(payload);
        });
    }
    getDispatchToken() {
        return this._dispatchToken;
    }
    addListener(callback) {
        return this.__emitter.addListener(this.__changedEvent, callback);
    }
    __invokeOnDispatch(payload) {
        this.__changed = false;
        this.__onDispatch(payload);
        if (this.__changed) {
            this.__emitter.emit(this.__changedEvent);
        }
    }
    // 抽象方法
    __onDispatch(action) {

    }
    __emitChange() {
        this.__changed = true;
    }
}

Container

绑定 View 和 Stores 的 controller-view,使得经过 dispatcher.dispatch 后变化的数据能够传递给 view 并进行更新

function getStores() {
    return [
        todoStore,
    ];
}

function getState() {
    return {
        todos: todoStore.getState(),
        onAdd: todoActions.addToDo,
    }
}

Container.createFunctional(AppView, getStores, getState);

// createFunctional
function createFunctional(viewFn, getStores, calculateState) {
    class FunctionalContainer extends React.Component {
        state = null;
        static getStores(props, context) { 
            return getStores(props, context);
        }
        static calculateState(prevState, props, context) {
            return calculateState(prevState, props, context);
        }
        render() {
            return viewFn(this.state)
        }
    }

    return create(FunctionalContainer);
}

// create
function create(Base) {
    const getState = (state, maybeProps, maybeContext) => {
        return Base.calculateState(state, maybeProps, maybeContext);
    }

    const getStores = (maybeProps, maybeContext) => {
        return Base.getStores(maybeProps, maybeContext);
    }

    class ContainerClass extends Base {
        _fluxContainerSubscriptions: null;
        constructor(props, context) {
            super(props, context);
            this._fluxContainerSubscriptions = new FluxContainerSubscriptions();
            this._fluxContainerSubscriptions.setStores(getStores(props, context));
            // *核心*
            this._fluxContainerSubscriptions.addListener(() => {
                this.setState((prevState, currentProps) => getState(
                    prevState,
                    currentProps,
                    context,
                ));
            });
            const calculatedState = getState(undefined, props, context);
            this.state = {
                ...(this.state || {}),
                ...calculatedState,
            }
        }

        componentWillUnmount() {
            if (super.componentWillUnmount) {
                super.componenetWillUnmount();
            }
            this._fluxContainerSubscriptions.reset();
        }
    }

    const container = ContainerClass;
    return container;
}

// FluxContainerSubscriptions
class FluxContainerSubscriptions {
    _callbacks;
    _stores;
    _tokens;
    _storeGroup;

    constructor() {
        this._callbacks = [];
    }
    setStores(stores) {
        this._stores = stores;
        this._resetTokens();
        this._resetStoreGroup();

        let changed = false;
        let changedStores = [];
        const setChanged = () => { changed = true; }
        this._tokens = store.map(store => store.addListener(setChanged));
        const callCallbacks = () => {
            if (changed) {
                this._callbacks.forEach(fn => fn());
                changed = false;
            }
        }
        this._storeGroup = new FluxStoreGroup(stores, callCallbacks);
    }
    addListener(fn) {
        this._callbacks.push(fn);
    }
    reset() {
        this._resetTokens();
        this._resetStoreGroup();
        this._resetCallbacks();
        this._resetStores();
    }
    _resetTokens() {
        if (this._tokens) {
            this._tokens.forEach(token => token.remove());
            this._tokens = null;
        }
    }
    _resetStoreGroup() {
        if (this._storeGroup) {
            this._storeGroup.release();
            this._storeGroup = null;
        }
    }
    _resetCallbacks() {
        this._callbacks = [];
    }
    _resetStores() {
        this._stores = null;
    }
}

// FluxStoreGroup
class FluxStoreGroup {
    constructor(stores, callback) {
        this._dispatcher = _getUniformDispatcher(stores);
        var storeTokens = stores.map(store => store.getDispatchToken());
        this._dispatchToken = this._dispatcher.register(payload => {
            this._dispatcher.waitFor(storeTokens);
            callback();
        });
    }
    release() {
        this._dispatcher.unregister(this._dispatchToken);
    }
}

function _getUniformDispatcher() {
    var dispatcher = stores[0].getDispatcher();
    return dispatcher;
}

Flux 运行机制

Dispatcher

Dispatcher 是全局单例,多个 stores 和单一 dispatcher 绑定,每实例化一个 store,都会向 dispatcher 注册对应的回调函数;

dispatcher.dispatch

  • 遍历 dispatcher._callbacks,会调用所有注册的回调函数

  • 所有注册的回调函数都是一样的,就是 store.__invokeOnDispatch(payload)

  • 如何知道当前 action 应该正确触发那个 store.reduce 生成新的 ,通过对应 store 的 store.reduce 中的 action.type 进行匹配。

new Store()

  • 每实例化一个 Store,会 dispatcher.register 一个回调函数
  • 回调函数会放入 dispatcher._callbacks 对象中
  • 回调函数部署的是 store.__invokeOnDispatch(payload) 方法

store.__invokeOnDispatch(payload)

  • 调用 store.reduce(prevState, action) 方法,计算出更新后的 state
  • 如果数据发生改变,则通过 store.__emiiter.emit() 发送 change 事件

Container

function getStores() {
  return [store]
}

function getState() {
  return {
    todos: store.getState(),
  }
}

  • 通过 Container.createFunctional(AppView, getStores, getState) 将 stores 和 view 绑定在一起
  • 内部通过 ContainerClass 生成 view,ContainerClass 在 constructor 通过 FluxContainerSubscriptions 部署订阅 stores 变更的回调函数

FluxContainerSubscriptions (sub)

  • view 在初始化时,通过 sub.setStores(stores) 部署事件回调函数
  • 通过 this._tokens = stores.map(store => store.addListener(setChanged)),标识 sub 实例状态是否发生变化,只有在 changed = true 的情况下,绑定在 sub 上的更新 view 的更新逻辑才会触发
  • store.addListener(callback) 内部部署 store.__emitter.addListener('change', callback) 方法,当 store.reduce() 方法调用后 state 发生变化,会通知给到 sub,并设置 changed 为 true;
  • view 更新回调函数部署在 callCallback 函数,并托管在 FluxStoreGroup 实例

FluxStoreGroup

  • FluxStoreGroup 会额外将更新视图的逻辑 callCallback 注册到 dispatcher;
  • 并且需要等到其他注册到 dispatcher 的 stores 回调函数调用后,才会进行 callCallback()
  • 只有先等到其他注册到 dispatcher 的 stores 回调函数调用,才能知道 state 是否发生改变,从而决定是否更新视图

总的来看,Flux 架构通过 Container 将 stores 和 view 绑定在一起;每个 stores 在实例化的时候,都会向全局单例 dispatcher 注册自身的 reduce,store.reduce 定义了针对不同 actions ,state 所作出的改变;此外,Container 在初始化时,还会额外将更新视图的逻辑 component.setState 注册到 dispatcher 中,将所有的 stores 注册 change 事件的回调函数,用于后续再 state 发生变化时,设置 changed 这一状态锁,这一状态锁用于控制是否进行视图渲染;当 view 视图发生交互,通过 dispatcher.dispatch 提交特定 action 时,dispatcher 会遍历先前注册好的回调列表,回调列表中的函数会调用 store.reduce 计算出 new state,然后会比较前后 state,如果 state 发生变化,store 则会发送 change 事件;change 事件会使得 changed 设置为 true,因此在遍历到更新视图的回调函数时,会开启视图渲染;

Immutable 对象

Flux 架构中,采用 Immutable 对象进行状态前后的比较。

之所以采用 Immutable 对象,是因为在比较前后状态变化的时候,需要快速识别出状态是否产生变化,最快的比较方式是采用浅比较(引用比较);

Immutable 对象的特点就是 Immutable 对象一旦创建就无法修改,如果数据发生变化会产生一个新的 Immutable 对象,如果没有发生变化则会返回原来的 Immutable 对象的引用,因此非常好的契合上述场景。此外,Immutable 对象修改时所创建的新的对象采用优化算法,开销较小,并且有利于实现变化追踪技术。

Immutable 对象被视为值,所以 Immutable 对象在进行 === 比较时是值比较;

// 如果 Immutable 对象未发生改变,则 Immutable 对象是相等的
const map1 = Map({a: 1, b: 2, c: {d: 1}});
const map2 = Map({a: 1, b: 2, c: {d: 1}});
const map3 = map1.set('a', 1);
map1.equals(map2); // true
map1 === map2; // true
map3 === map1; // true  由于值为发生变化

// 如果值发生变化,则 Immutable 对象返回一个新的 Immutable 对象
const map4 = map1.set('a', 2);
map4 === map1; // false

map1.get('a'); // 1
map1.set('a', 1);
map1.getIn(['a', 'c', 'd']); // 1
map1.setIn(['a', 'c', 'd'], 2); // {a: 1, b: 2, c: {d: 2}}
map1.updateIn(['a', 'c', 'd'], value => value + 1); // {a: 1, b: 2, c: {d: 2}}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269