稍微學一下 Vuex 原理

vue.jpg

博客原文

介紹

Vuex 是一個專為 Vue.js 應用程序開發(fā)的狀態(tài)管理模式
這種集中管理應用狀態(tài)的模式相比父子組件通信來說,使數(shù)據(jù)的通信更方便,狀態(tài)的更改也更加直觀侮攀。

Bus

肯定有不少同學在寫 Vue 時使用過 new Vue() 創(chuàng)建 bus 進行數(shù)據(jù)通信。

import Vue from 'vue';
const bus = new Vue();
export default {
  install(Vue) {
    Object.defineProperty(Vue.prototype, '$bus', {
      get () { return bus }
    });
  }
};

組件中使用 this.$bus.$on this.$bus.$emit 監(jiān)聽和觸發(fā) bus 事件進行通信厢拭。
bus 的通信是不依賴組件的父子關系的魏身,因此實際上可以理解為最簡單的一種狀態(tài)管理模式。
通過 new Vue() 可以注冊響應式的數(shù)據(jù)蚪腐,
下面基于此對 bus 進行改造,實現(xiàn)一個最基本的狀態(tài)管理:

// /src/vuex/bus.js
let Vue
// 導出一個 Store 類税朴,一個 install 方法
class Store {
  constructor (options) {
    // 將 options.state 注冊為響應式數(shù)據(jù)
    this._bus = new Vue({
      data: {
        state: options.state
      }
    })
  }
  // 定義 state 屬性
  get state() {
    return this._bus._data.state;
  }
}
function install (_Vue) {
  Vue = _Vue
  // 全局混入 beforeCreate 鉤子
  Vue.mixin({
    beforeCreate () {
      // 存在 $options.store 則為根組件
      if (this.$options.store) {
        // $options.store 就是創(chuàng)建根組件時傳入的 store 實例回季,直接掛在 vue 原型對象上
        Vue.prototype.$store = this.$options.store
      }
    }
  })
}
export default {
  Store,
  install
}

創(chuàng)建并導出 store 實例:

// /src/store.js
import Vue from 'vue'
import Vuex from './vuex/bus'
Vue.use(Vuex) // 調用 Vuex.install 方法
export default new Vuex.Store({
  state: {
    count: 0
  }
})

創(chuàng)建根組件并傳入 store 實例:

// /src/main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

組件中使用示例:

<!-- /src/App.vue -->
<template>
  <div id="app">
    {{ count }}
    <button @click="changeCount">+1</button>
  </div>
</template>
<script>
export default {
  name: 'app',
  computed: {
    count() {
      return this.$store.state.count;
    }
  },
  methods: {
    changeCount() {
      this.$store.state.count++
    }
  }
}
</script>

從零實現(xiàn)一個 Vuex

前一節(jié)通過 new Vue() 定義一個響應式屬性并通過 minxin 為所有組件混入 beforeCreate 生命周期鉤子函數(shù)的方法為每個組件內添加 $store 屬性指向根組件的 store 實例的方式,實現(xiàn)了最基本的狀態(tài)管理正林。
繼續(xù)這個思路泡一,下面從零一步步實現(xiàn)一個最基本的 Vuex。

以下代碼的 git 地址:simple-vuex

整體結構

let Vue;
class Store {}
function install() {}
export default {
  Store,
  install
}

install 函數(shù)

// 執(zhí)行 Vue.use(Vuex) 時調用 并傳入 Vue 類
// 作用是為所有 vue 組件內部添加 `$store` 屬性
function install(_Vue) {
  // 避免重復安裝
  if (Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error('[vuex] already installed. Vue.use(Vuex) should be called only once.');
    }
    return
  }
  Vue = _Vue; // 暫存 Vue 用于其他地方有用到 Vue 上的方法
  Vue.mixin({
    // 全局所有組件混入 beforeCreate 鉤子觅廓,給每個組件中添加 $store 屬性指向 store 實例
    beforeCreate: function vuexInit() {
      const options = this.$options;
      if (options.store) {
        // 接收參數(shù)有=中有 store 屬性則為根組件
        this.$store = options.store;
      } else if (options.parent && options.parent.$store) {
        // 非根組件通過 parent 父組件獲取
        this.$store = options.parent.$store;
      }
    }
  })
}

Store 類

// 執(zhí)行 new Vuex.Store({}) 時調用
class Store {
  constructor(options = {}) {
    // 初始化 getters mutations actions
    this.getters = {};
    this._mutations = {};
    this._actions = {};
    // 給每個 module 注冊 _children 屬性指向子 module
    // 用于后面 installModule 中根據(jù) _children 屬性查找子 module 進行遞歸處理
    this._modules = new ModuleCollection(options)
    const { dispatch, commit } = this;
    // 固定 commit dispatch 的 this 指向 Store 實例
    this.commit = (type, payload) => {
      return commit.call(this, type, payload);
    }
    this.dispatch = (type, payload) => {
      return dispatch.call(this, type, payload);
    }
    // 通過 new Vue 定義響應式 state
    const state = options.state;
    this._vm = new Vue({
      data: {
        state: state
      }
    });
    // 注冊 getters  mutations actions
    // 并根據(jù) _children 屬性對子 module 遞歸執(zhí)行 installModule
    installModule(this, state, [], this._modules.root);
  }
  // 定義 state commit dispatch
  get state() {
    return this._vm._data.state;
  }
  set state(v){
    throw new Error('[Vuex] vuex root state is read only.')
  }
  commit(type, payload) {
    return this._mutations[type].forEach(handler => handler(payload));
  }
  dispatch(type, payload) {
    return this._actions[type].forEach(handler => handler(payload));
  }
}

ModuleCollection 類

Store 類的構造函數(shù)中初始化 _modules 時是通過調用 ModuleCollection 這個類鼻忠,內部從根模塊開始遞歸遍歷 modules 屬性,初始化模塊的 _children 屬性指向子模塊杈绸。

class ModuleCollection {
  constructor(rawRootModule) {
    this.register([], rawRootModule)
  }
  // 遞歸注冊帖蔓,path 是記錄 module 的數(shù)組 初始為 []
  register(path, rawModule) {
    const newModule = {
      _children: {},
      _rawModule: rawModule,
      state: rawModule.state
    }
    if (path.length === 0) {
      this.root = newModule;
    } else {
      // 非最外層路由通過 reduce 從 this.root 開始遍歷找到父級路由
      const parent = path.slice(0, -1).reduce((module, key) => {
        return module._children[key];
      }, this.root);
      // 給父級路由添加 _children 屬性指向該路由
      parent._children[path[path.length - 1]] = newModule;
      // 父級路由 state 中也添加該路由的 state
      Vue.set(parent.state, path[path.length - 1], newModule.state);
    }
    // 如果當前 module 還有 module 屬性則遍歷該屬性并拼接 path 進行遞歸
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule);
      })
    }
  }
}

installModule

Store 類的構造函數(shù)中調用 installModule 矮瘟,通過 _modules 的 _children 屬性遍歷到每個模塊并注冊 getters mutations actions

function installModule(store, rootState, path, module) {
  if (path.length > 0) {
    const parentState = rootState;
    const moduleName = path[path.length - 1];
    // 所有子模塊都將 state 添加到根模塊的 state 上
    Vue.set(parentState, moduleName, module.state)
  }
  const context = {
    dispatch: store.dispatch,
    commit: store.commit,
  }
  // 注冊 getters mutations actions
  const local = Object.defineProperties(context, {
    getters: {
      get: () => store.getters
    },
    state: {
      get: () => {
        let state = store.state;
        return path.length ? path.reduce((state, key) => state[key], state) : state
      }
    }
  })
  if (module._rawModule.actions) {
    forEachValue(module._rawModule.actions, (actionFn, actionName) => {
      registerAction(store, actionName, actionFn, local);
    });
  }
  if (module._rawModule.getters) {
    forEachValue(module._rawModule.getters, (getterFn, getterName) => {
      registerGetter(store, getterName, getterFn, local);
    });
  }
  if (module._rawModule.mutations) {
    forEachValue(module._rawModule.mutations, (mutationFn, mutationName) => {
      registerMutation(store, mutationName, mutationFn, local)
    });
  }
  // 根據(jù) _children 拼接 path 并遞歸遍歷
  forEachValue(module._children, (child, key) => {
    installModule(store, rootState, path.concat(key), child)
  })
}

installModule 中用來注冊 getters mutations actions 的函數(shù):

// 給 store 實例的 _mutations 屬性填充
function registerMutation(store, mutationName, mutationFn, local) {
  const entry = store._mutations[mutationName] || (store._mutations[mutationName] = []);
  entry.push((payload) => {
    mutationFn.call(store, local.state, payload);
  });
}

// 給 store 實例的 _actions 屬性填充
function registerAction(store, actionName, actionFn, local) {
  const entry = store._actions[actionName] || (store._actions[actionName] = [])
  entry.push((payload) => {
    return actionFn.call(store, {
      commit: local.commit,
      state: local.state,
    }, payload)
  });
}

// 給 store 實例的 getters 屬性填充
function registerGetter(store, getterName, getterFn, local) {
  Object.defineProperty(store.getters, getterName, {
    get: () => {
      return getterFn(
        local.state,
        local.getters,
        store.state
      )
    }
  })
}

// 將對象中的每一個值放入到傳入的函數(shù)中作為參數(shù)執(zhí)行
function forEachValue(obj, fn) {
  Object.keys(obj).forEach(key => fn(obj[key], key));
}

使用

還有 modules、plugins 等功能還沒有實現(xiàn)塑娇,而且 getters 的并沒有使用 Vue 的 computed 而只是簡單的以函數(shù)的形式實現(xiàn)澈侠,但是已經(jīng)基本完成了 Vuex 的主要功能,下面是一個使用示例:

// /src/store.js
import Vue from 'vue'
import Vuex from './vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    changeCount(state, payload) {
      console.log('changeCount', payload)
      state.count += payload;
    }
  },
  actions: {
    asyncChangeCount(ctx, payload) {
      console.log('asyncChangeCount', payload)
      setTimeout(() => {
        ctx.commit('changeCount', payload);
      }, 500);
    }
  }
})
<!-- /src/App.vue -->
<template>
  <div id="app">
    {{ count }}
    <button @click="changeCount">+1</button>
    <button @click="asyncChangeCount">async +1</button>
  </div>
</template>

<script>
export default {
  name: 'app',
  computed: {
    count() {
      return this.$store.state.count;
    }
  },
  methods: {
    changeCount() {
      this.$store.commit('changeCount', 1);
    },
    asyncChangeCount() {
      this.$store.dispatch('asyncChangeCount', 1);
    }
  },
  mounted() {
    console.log(this.$store)
  }
}
</script>

閱讀源碼的過程中寫了一些方便理解的注釋埋酬,希望給大家閱讀源碼帶來幫助哨啃,github: vuex 源碼

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市写妥,隨后出現(xiàn)的幾起案子拳球,更是在濱河造成了極大的恐慌,老刑警劉巖珍特,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件祝峻,死亡現(xiàn)場離奇詭異,居然都是意外死亡次坡,警方通過查閱死者的電腦和手機呼猪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來砸琅,“玉大人宋距,你說我怎么就攤上這事≈⒅” “怎么了谚赎?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長诱篷。 經(jīng)常有香客問我壶唤,道長,這世上最難降的妖魔是什么棕所? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任闸盔,我火速辦了婚禮,結果婚禮上琳省,老公的妹妹穿的比我還像新娘迎吵。我一直安慰自己,他們只是感情好针贬,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布击费。 她就那樣靜靜地躺著卿拴,像睡著了一般罕伯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上叶组,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音圆仔,去河邊找鬼垃瞧。 笑死,一個胖子當著我的面吹牛荧缘,可吹牛的內容都是我干的皆警。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼截粗,長吁一口氣:“原來是場噩夢啊……” “哼信姓!你這毒婦竟也來了?” 一聲冷哼從身側響起绸罗,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤意推,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后珊蟀,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體菊值,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年育灸,在試婚紗的時候發(fā)現(xiàn)自己被綠了腻窒。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡磅崭,死狀恐怖儿子,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情砸喻,我是刑警寧澤柔逼,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站割岛,受9級特大地震影響愉适,放射性物質發(fā)生泄漏。R本人自食惡果不足惜癣漆,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一维咸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧惠爽,春花似錦癌蓖、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽倒槐。三九已至旬痹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背两残。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工永毅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人人弓。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓沼死,卻偏偏與公主長得像,于是被迫代替她去往敵國和親崔赌。 傳聞我的和親對象是個殘疾皇子意蛀,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內容

  • Vuex 是一個專為 Vue.js 應用程序開發(fā)的狀態(tài)管理模式。它采用集中式存儲管理應用的所有組件的狀態(tài)健芭,并以相應...
    白水螺絲閱讀 4,652評論 7 61
  • Vuex是什么县钥? Vuex 是一個專為 Vue.js應用程序開發(fā)的狀態(tài)管理模式。它采用集中式存儲管理應用的所有組件...
    蕭玄辭閱讀 3,106評論 0 6
  • 組件(Component)是Vue.js最核心的功能慈迈,也是整個架構設計最精彩的地方若贮,當然也是最難掌握的。...
    六個周閱讀 5,582評論 0 32
  • 渲染函數(shù)和jsx 在vue中我們可以不用template來指定組件的模板痒留,而是用render函數(shù)來創(chuàng)建虛擬dom結...
    6e5e50574d74閱讀 711評論 0 0
  • ? 陪伴第428天 第165篇原創(chuàng)文章 爸爸:我今天去超市發(fā)現(xiàn)湯圓好貴伸头,我就沒買了匾效,買了糯米粉回家自己包吧。 媽媽...
    莫莉姑娘閱讀 964評論 4 0