前端狀態管理簡易實現(以vuex為例)

Tra發表於2019-01-03

1.狀態管理到底是什麼?有什麼用?

       狀態管理說白了就是一個 全域性變數,var obj = {} 這樣的東西。

       使用全域性變數的缺點:

             無法追蹤資料的更改情況,任意的操作都可導致資料變數,無法所追蹤資料的變化過程,大型應用中不太容易定位bug

       狀態管理:

              可以追蹤到資料變化的全域性變數。

        使用場景:

       在多頁面中,狀態管理應用不大,因為這個變數僅僅存與記憶體中,當跳轉頁面時,資料就會清除。當然,如果對狀態進行儲存(比如sessionStroge),倒也可以實現狀態的複用,但是想想,每次資料都需儲存,相比較sessionStroge api級別的儲存,狀態管理會更復雜一些,帶來應用的複雜性。

        在單頁面中,跳轉系統轉化為路由系統,路由跳轉無需熟悉頁面,儲存到記憶體中的狀態資料生命週期更長,使用也更加廣泛。但依然要注意在web端,重新整理操作的頻率,重新整理頁面會使的資料全部丟失,所以需在程式碼中完成丟失情況的處理,使的程式碼更加健壯性。

2.那如何追蹤一個全域性變數的資料變化?

     如果我們有使用vuex的經驗,我們知道在vuex裡的資料,無法通過store.state.a = 1這樣的賦值操作完成。因為這樣,無法追蹤到資料的變化。

     問題:追蹤一個全域性變數的資料變化?

     方法1:Object.defineProperty();這個應該很熟悉,vue雙向繫結就是通過這裡的setter getter實現的。

    方法2:Proxy ES6新語法,vue3的底層雙向繫結會使用它。(本文我們以Proxy做demo)

學習Proxy,請點選 www.jianshu.com/p/34f0e6abe…

3.在寫一個簡易的狀態管理,需要我們準備什麼?

         前提:本文只討論如何簡易狀態管理,所以效能方面不會考慮,也就是說不會使用Vnode diff演算法等知識。

前端狀態管理簡易實現(以vuex為例)

         檢視上面的圖,有幾個問題我們需要考慮:

          1.只要state發生變化,view層自動更新?

          我們增加個訂閱-釋出器, 每個需要state的view訂閱資料變化,當資料發生變化時,釋出這個變化,state也會跟著變化。

         2.這個指令是什麼?是vuex中的actions mutations嗎?

         我們模擬vuex實現,所以認為指定就是這兩個,actions是非同步操作,mutation是同步操作。

4.程式碼實現:

        本文程式碼大部分來自juejin.im/post/5b7635…

        本文是在上文中總結學習寫出的。

        本文目錄結構如下:

              前端狀態管理簡易實現(以vuex為例)

1.訂閱-釋出模式 js/lib/pubsub.js
export default class PubSub{
    constructor(){
        this.events = {};
    }

    // 訂閱
    subscribe(type,callback){
        if(!this.events.hasOwnProperty(type)){
            this.events[type] = [];
        }
        // 返回該type下訂閱了多少個回掉函式
        // push返回的是當前陣列的length
        return this.events[type].push(callback);
    }

    // 釋出
    publish(type,data={}){
        if(!this.events.hasOwnProperty(type)){
            return [];
        }
        return this.events[type].map((callback)=>{
            callback(data)
        })
    }
}
複製程式碼

2.實現狀態管理的關鍵  js/store/store.js
import pubSub from '../lib/pubsub.js'

export default class Store {
    // 預設引數
    /*
    * 以vuex的寫法舉例
    * new Vuex.store({
    *   state:{
    *
    *   },
    *   mutations:{},
    *   actions:{}
    * })
    * */
    constructor(params) {
        let self = this;
        self.mutations = params.mutations ? params.mutations : {};
        self.actions = params.actions ?  params.actions : {} ;
        this.events = new pubSub();
        self.state = new Proxy((params.state || {}), {
            set(target,key,value){
                target[key] = value;
                console.log(`stateChange: ${key}: ${value}`);
                // 當資料變化時,進行釋出操作
                self.events.publish('stateChange',self.state);
                // 如果資料不是有mutation方式改變,則發出警告
                if(self.status !== 'mutation'){
                    console.warn(`資料應該用mutation方式提交`);
                }
                self.status = 'resting';
                return true;
            }
        });
        self.status = 'resting';
    }

    /*
    * dispatch():提交action,action是一個非同步方法
    * */
    dispatch(actionType,payload){
        let self = this;
        if(typeof self.actions[actionType] !== 'function'){
            console.log(`action ${actionType} 不存在`);
            return false;
        }
        console.groupCollapsed(`ACTION: ${actionType}`);
        self.status = 'action';
        self.actions[actionType](self,payload);
        console.groupEnd();
        return true;
    }

    commit(mutataionType,payload){
        let self = this;
        if(typeof self.mutations[mutataionType] !== 'function'){
            console.log(`Mutation "${mutataionType}" 不存在`);
            return false;
        }
        console.log(`Mutation: ${mutataionType}`)
        self.status = 'mutation';
        let newState = self.mutations[mutataionType](self.state, payload);
        self.state = Object.assign(self.state,newState);
        return true;
    }


}




複製程式碼

例項化Store  js/store/index.js
import Store from './store.js'

export default new Store({
    state:{
        items:[
            1,2
        ]
    },
    mutations:{
        addItem(state,payload){
            state.items.push(payload);
            return state;
        },
        clearItem(state,payload){
            state.items.splice(payload.index,1);
            return state;
        }
    },
    actions:{
        addItem(context,payload){
            context.commit('addItem',payload)
        },
        clearItem(context,payload){
            context.commit('clearItem',payload)
        }
    }
})
複製程式碼

view層:js/lib/component.js
import Store from '../store/store.js'
export default class Component{
    constructor(props={}) {
        let self = this;
        this.render = this.render || function () {
            
        }
        // 關鍵:這個是通用的元件類,可對需要使用state的元件,進行資料訂閱。
        if(props.store instanceof Store){
            props.store.events.subscribe('stateChange',self.render.bind(self))
        }
        if(props.hasOwnProperty('el')){
            self.el = props.el;
        }
    }

}複製程式碼

元件類:List      js/components/List.js
import component from '../lib/component.js'
import store from '../store/index.js'
export default class List extends component{
    constructor() {
        super({
            store,
            el: document.querySelector('.js-items')
        })
    }

        render(){
            let self = this;
            if(store.state.items.length === 0) {
                self.el.innerHTML = `<p class="no-items">You've done nothing yet &#x1f622;
</p>`;
                return;
            }

            self.el.innerHTML = `
                <ul class="app__items">
                ${store.state.items.map(item => {
                        return `
                    <li>${item}<button aria-label="Delete this item">×</button></li>
                    `
                    }).join('')}
                </ul>
            `;

            self.el.querySelectorAll('button').forEach((button, index) => {
                button.addEventListener('click', () => {
                    store.dispatch('clearItem', { index });
                });
            });

        }


}複製程式碼

Count元件   js/components/Count.js
import store from '../store/index.js'
import component from '../lib/component.js'
export default  class Count extends component{
    constructor() {
        super({
            store,
            el:document.querySelector('.js-count')
        })
    }

    render(){
        let suffix = store.state.items.length !== 1 ? 's' : '';
        let emoji = store.state.items.length > 0 ? '&#x1f64c;' : '&#x1f622;';
        this.el.innerHTML = `
            <small>You've done</small>
            ${store.state.items.length}
            <small>thing${suffix} today ${emoji}</small>
        `;
    }

}
複製程式碼

主函式:main.js
import store from './store/index.js';
import Count from './components/Count.js';
import List from './components/List.js';

const formElement = document.querySelector('.js-form');
const inputElement = document.querySelector('#new-item-field');
formElement.addEventListener('submit', evt => {
    evt.preventDefault();

    let value = inputElement.value.trim();

    if(value.length) {
        store.dispatch('addItem', value);
        inputElement.value = '';
        inputElement.focus();
    }
});
const countInstance = new Count();
const listInstance = new List();

countInstance.render();
listInstance.render();複製程式碼

入口html:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="stylesheet" href="css/global.css" />
    <!--<link rel="stylesheet" media="all" href="https://rawgit.com/hankchizljaw/boilerform/master/dist/css/boilerform.min.css?v=1.1.1" />-->
    <title>Vanilla State Management</title>
</head>
<body>
    <main>
        <header class="intro">
            <h1 class="intro__heading">Done list</h1>
            <p class="intro__summary">A list of things that you have achieved today</p>
            <p class="intro__summary"><b>Note:</b> The data isn't stored, so it will disappear if you reload!</p>
        </header>
        <section class="app">
            <section class="app__input">
                <h2 class="app__heading">What you've done</h2>
                <div class="js-items" aria-live="polite" aria-label="A list of items you have done"></div>
                <form class=" new-item boilerform js-form">
                    <div class="boilerform">
                        <!-- Form styles from the https://boilerform.design boilerplate -->
                        <label for="new-item-field" class="new-item__label c-label ">Add a new item</label>
                        <input type="text" class=" new-item__details  c-input-field " id="new-item-field" autocomplete="off" />
                        <button class=" c-button  new-item__button ">Save</button>
                    </div>
                </form>
            </section>
            <aside class="app__status">
                <p role="status" class="visually-hidden">You have done <span class="js-status">1 thing</span> today!</p>
                <div class="app__decor js-count">
                    <small>You've done</small>
                    <span>1</span>
                    <small>things today ?</small>
                </div>
            </aside>
        </section>
    </main>
    <script type="module" src="js/main.js"></script>
</body>
</html>複製程式碼

5.總結

以上,也就實現了一下簡易的狀態管理,其實狀態管理就是將{a:1}的資料修改都通過指定的指令去修改,可以追蹤到資料變化,所以不要把狀態管理想的多麼複雜,多麼高大上。時刻牢記,所謂狀態管理就是一個全域性物件,只是這個物件的屬性變化要經過規定一套流程。

如果你不喜歡狀態管理,而且專案不大的情況下,可以不用。sessionstorage這些也是很好的選擇。


相關文章