利用Dectorator分模組儲存Vuex狀態(下)

coffee-ai發表於2019-03-24

1、引言

在上篇中我們介紹了利用Decorator分模組來儲存資料,在Weex頁面例項建立時讀取資料到Vuex,但是存在無效資料儲存冗餘的問題,並且不支援registerModule來動態存取資料,接下來主要介紹下利用Decorator來實現資料儲存的黑白名單和模組動態註冊時資料存取的實現方式。

2、 設定state黑白名單

在設定state的黑白名單時,我們需要對各個模組單獨設定,即允許在不同module中設定黑或白名單,同時需要在unregisterModule移除模組時同時移除黑白名單中的引用,在這裡我們使用WeakMapWeakSet來儲存相關資訊,避免module登出時無法釋放相應的prop

2.1、getter的依賴收集

WeakMap是ES6中新增的資料型別,其鍵屬於弱引用,可以很好的避免在沒有其他引用時,因Map存在引用無法釋放而導致的記憶體洩漏。首先,需要建立黑白名單的修飾器:

const WHITE_TAG = 1; // 白名單tag
const BLACK_TAG = 2; // 黑名單tag
// 儲存state的資訊
const filterMap = new WeakMap();
// 儲存state[prop]的getter,用於在setState時過濾無效的prop
const descriptorSet = new WeakSet();
// 接受黑白名單的tag,返回對應的修飾器
const createDescriptorDecorator = tag => (target, prop) => {
    if (!filterMap.has(tag)) {
        // 設定當前state的黑白名單資訊
        filterMap.set(target, tag);
    } else if (filterMap.get(target) ^ tag) {
        // 在同一state中,不能同時存在黑名單和白名單
        throw new Error('...');
    }
    let value = target[prop];
    return {
        enumerable: true,
        configurable: true,
        get: function() {
            const {get: getter} = Object.getOwnPropertyDescriptor(target, prop) || {};
            if (getter && !descriptorSet.has(getter)) {
                // 收集依賴到WeakSet
                descriptorSet.add(getter);
            }
            return value;
        },
        set: function(newVal) {
            value = newVal;
        }
    };
};

const shouldWrite = createDescriptorDecorator(WHITE_TAG);
const forbidWrite = createDescriptorDecorator(BLACK_TAG);
複製程式碼

這樣就完成了修飾器的編寫,使用時對需要的stateproperty修飾即可:

const module = {
    state: () => ({
        @shouldWrite
        property1: {},
        property2: [],
    })
};
複製程式碼

此時當讀取state.property1時就會將property1getter存入到descriptorSet中,供過濾時使用。

2.2、解析modulestate

getter觸發依賴收集後,我們將stateproperty存入了descriptorSet,現在我們需要根據modulestate,利用descriptorSet將無效的資料過濾掉,返回pureState:

/**
 * [根據黑白名單,過濾state中無效的資料,返回新state]
 * @param  {[type]} module [state對應的module]
 * @param  {[type]} state  [待過濾的state物件]
 * @return {[type]}        [新的state]
 */
const parseModuleState = (module, state) => {
    const {_children, state: moduleState} = module;
    const childrenKeys = Object.keys(_children);
    // 獲取state中各個property的getter
    const descriptors = Object.getOwnPropertyDescriptors(moduleState);
    // 判斷當前state啟用黑名單還是白名單,預設黑名單
    const tag = filterMap.get(moduleState) || BLACK_TAG;
    const isWhiteTag = tag & WHITE_TAG;
    // Object.fromEntries可以用object.fromentries來polyfill
    const pureState = Object.fromEntries(Object.entries(state).filter(([stateKey]) => {
        const {get: getter} = descriptors[stateKey] || {};
        // 過濾子模組的資料,只保留白名單或不在黑名單裡的資料
        return !childrenKeys.some(childKey => childKey === stateKey)
                && !((isWhiteTag ^ descriptorSet.has(getter)));
    }));
    return pureState;
}
複製程式碼

這樣剔除了冗餘的資料,只保留有效資料存入storage

3、registerModule

Vuex支援registerModuleunregisterModule來動態的註冊模組和登出模組,並且可以通過設定{preserveState: true}來儲存SSR渲染的資料,在Weex中同樣需要在registerModule時初始化已有的資料到state,這裡我們選擇在外掛初始化時擴充套件下:

// 根據傳入的newState,初始化module及其子module的state
const setModuleState = async (store, path, newState) => {
    const namespace = path.length ? normalizeNamespace(path) : '';
    const module = store._modulesNamespaceMap[namespace];
    const setChildModuleState = function setChildModuleState(_module, _state) {
        const {_children, state} = _module;
        const childrenKeys = Object.keys(_children);
        Object.entries(_state).map(([key, value]) => {
            if (childrenKeys.every(a => a !== key)) {
                state[key] = value;
            } else if (_children[key]) {
                // 子模組遞迴設定
                setChildModuleState(_children[key], _state[key]);
            }
        });
    };
    setChildModuleState(module, newState);
};
export const createStatePlugin = (option = {}) => {
    const {supportRegister = false} = option;
    return function(store) {
        if (supportRegister) {
            // 擴充套件registerModule方法,先保留對原方法的引用
            const registerModule = store.registerModule;
            // 涉及到對storage的操作,將其擴充套件為async方法
            store.registerModule = async function(path, rawModule, options) {
                // 呼叫Vuex的registerModule
                registerModule.call(store, path, rawModule, options);
                // 在註冊module完成後,初始化資料
                const {rawState} = options || {};
                const newState = typeof rawState === 'function' ? rawState() : rawState;
                if (newState) {
                    // 設定state
                    setModuleState(store, path, newState);
                    ...
                }
            }
        }
    }
}
複製程式碼

以上就完成了在registerModule時,對state資料初始化操作,還可以呼叫parseModuleState後模組所需資料存入storageunregisterModule的擴充套件更簡單,只需在登出的同時刪除storage中對應模組的資料即可,再次不再贅述。

4、小結

在Weex開發時,因各個頁面例項擁有獨立的JS執行環境,無法共享Vuex資料,所以在專案中會使用storageBroadcastChannel等來實現頁面通訊和資料共享。本文利用Decorator來實現Vuex資料的分模組儲存,同時提供stateproperty黑白名單過濾及registerModule時資料動態初始化功能。以上是本人專案中的一點心得,如若有誤,還請斧正。

相關文章