vuex實現及簡略解析

你們都知道vuexvue的一個狀態管理器,它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。先看看vuex下面的工做流程圖html


經過官方文檔提供的流程圖咱們知道,vuex的工做流程,vue

  • 一、數據從state中渲染到頁面;
  • 二、在頁面經過dispatch來觸發action
  • 三、action經過調用commit,來觸發mutation
  • 四、mutation來更改數據,數據變動以後會觸發dep對象的notify,通知全部Watcher對象去修改對應視圖(vue的雙向數據綁定原理)。

使用vuex

理解vuex的工做流程咱們就看看vuexvue中是怎麼使用的。git

首先用vue-cli建立一個項目工程,以下圖,選擇vuex,而後就是一路的回車鍵github

安裝好以後,就有一個帶有vuexvue項目了。vuex

進入目錄而後看到,src/store.js,在裏面加了一個狀態{count: 100},以下vue-cli

import Vue from 'vue'
import Vuex from 'vuex' // 引入vuex

Vue.use(Vuex) // 使用插件

export default new Vuex.Store({
  state: {
    count: 100 // 加一個狀態
  },
  getter: {
  
  },
  mutations: {
  
  },
  actions: {
  
  }
})

最後在App.vue文件裏面使用上這個狀態,以下數組

<template>
  <div id="app">
    這裏是stort------->{{this.$store.state.count}}
  </div>
</template>

<script>
export default {
  name: 'app'
}
</script>

<style>
</style>

項目跑起來就會看到頁面上看到,頁面上會有100了,以下圖app

到這裏咱們使用vuex建立了一個store,而且在咱們的App組件視圖中使用,可是咱們會有一些列的疑問。異步

  • store是如何被使用到各個組件上的??
  • 爲何state的數據是雙向綁定的??
  • 在組件中爲何用this.$store.dispch能夠觸發storeactions??
  • 在組件中爲何用this.$store.commit能夠觸發storemutations??
  • ....等等等等

帶着一堆問題,咱們來本身實現一個vuex,來理解vuex的工做原理。模塊化

安裝並使用store

src下新建一個vuex.js文件,而後代碼以下

'use strict'

let Vue = null

class Store {
  constructor (options) {
    let { state, getters, actions, mutations } = options
  }
}
// Vue.use(Vuex)
const install = _Vue => {
  // 避免vuex重複安裝
  if (Vue === _Vue) return
  Vue = _Vue
  Vue.mixin({
    // 經過mixins讓每一個組件實例化的時候都會執行下面的beforeCreate
    beforeCreate () {
      // 只有跟節點纔有store配置,因此這裏只走一次
      if (this.$options && this.$options.store) {
        this.$store = this.$options.store
      } else if (this.$parent && this.$parent.$store) { // 子組件深度優先 父 --> 子---> 孫子
        this.$store = this.$parent.$store
      }
    }
  })
}

export default { install, Store }

而後修改store.js中的引入vuex模塊改爲本身的vuex.js

import Vuex from './vuex' // 本身建立的vuex文件

在咱們的代碼中export default { install, Store }導出了一個對象,分別是installStore

install的做用是,當Vue.use(Vuex)就會自動調用install方法,在install方法裏面,咱們用mixin混入了一個beforeCreate的生命週期的鉤子函數,使得當每一個組件實例化的時候都會調用這個函數。

beforeCreate中,第一次根組件經過store屬性掛載$store,後面子組件調用beforeCreate掛載的$store都會向上找到父級的$store,這樣子經過層層向上尋找,讓每一個組件都掛上了一個$store屬性,而這個屬性的值就是咱們的new Store({...})的實例。以下圖

經過層層向上尋找,讓每一個組件都掛上了一個 $store屬性

設置state響應數據

經過上面,咱們已經從每一個組件都經過this.$store來訪問到咱們的store的實例,下面咱們就編寫state數據,讓其變成雙向綁定的數據。下面咱們改寫store

class Store {
  constructor (options) {
    let { state, getters, actions, mutations } = options // 拿到傳進來的參數
    this.getters = {}
    this.mutations = {}
    this.actions = {}
    // vuex的核心就是借用vue的實例,由於vuex的數據更改回更新視圖
    this._vm = new Vue({
      data: {
        state
      }
    })
  }
  // 訪問state對象時候,就直接返回響應式的數據
  get state() { // Object.defineProperty get 同理
    return this._vm.state
  }
}

傳進來的state對象,經過new Vue({data: {state}})的方式,讓數據變成響應式的。當訪問state對象時候,就直接返回響應式的數據,這樣子在App.vue中就能夠經過this.$store.state.count拿到state的數據啦,而且是響應式的呢。

編寫mutations、actions、getters

上面咱們已經設置好state爲響應式的數據,這裏咱們在store.js裏面寫上mutations、actions、getters,以下

import Vue from 'vue'
import Vuex from './vuex' // 引入咱們的本身編寫的文件

Vue.use(Vuex) // 安裝store
// 實例化store,參數數對象
export default new Vuex.Store({
  state: {
    count : 1000
  },
  getters : {
    newCount (state) {
      return state.count + 100
    }
  },
  mutations: {
    change (state) {
      console.log(state.count)
      state.count += 10
    }
  },
  actions: {
    change ({commit}) {
      // 模擬異步
      setTimeout(() => {
        commit('change')
      }, 1000)
    }
  }
})

配置選項都寫好以後,就看到getters對象裏面有個newCount函數,mutationsactions對象裏面都有個change函數,配置好store以後咱們在App.vue就能夠寫上,dispatchcommit,分別能夠觸發actionsmutations,代碼以下

<template>
  <div id="app">
    這裏是store的state------->{{this.$store.state.count}} <br/>
    這裏是store的getter------->{{this.$store.getters.newCount}} <br/>
    <button @click="change">點擊觸發dispach--> actions</button>
    <button @click="change1">點擊觸發commit---> mutations</button>
  </div>
</template>

<script>
export default {
  name: 'app',
  methods: {
    change () {
      this.$store.dispatch('change') // 觸發actions對應的change
    },
    change1 () {
      this.$store.commit('change') // 觸發mutations對應的change
    }
  },
  mounted () {
    console.log(this.$store)
  }
}
</script>

數據都配置好以後,咱們開始編寫store類,在此以前咱們先編寫一個循環對象工具函數。

const myforEach = (obj, callback) => Object.keys(obj).forEach(key => callback(key, obj[key]))
// 做用:
// 例如{a: '123'}, 把對象的key和value做爲參數
// 而後就是函數運行callback(a, '123')

工具函數都準備好了,以後,下面直接縣編寫gettersmutationsactions的實現

class Store {
  constructor (options) {
    let { state = {}, getters = {}, actions = {}, mutations = {} } = options
    this.getters = {}
    this.mutations = {}
    this.actions = {}
    // vuex的核心就是借用vue的實例,由於vuex的數據更改回更新視圖
    this._vm = new Vue({
      data: {
        state
      }
    })
    // 循環getters的對象
    myforEach(getters, (getterName, getterFn) => {
      // 對this.getters對象進行包裝,和vue的computed是差很少的
      // 例如 this.getters['newCount'] = fn(state)
      // 執行 this.getters['newCount']()就會返回計算的數據啦
      Object.defineProperty(this.getters, getterName, {
        get: () => getterFn(state)
      })
    })
    // 這裏是mutations各個key和值都寫到,this.mutations對象上面
    // 執行的時候就是例如:this.mutations['change']()
    myforEach(mutations, (mutationName, mutationsFn) => {
      // this.mutations.change = () => { change(state) }
      this.mutations[mutationName] = () => {
        mutationsFn.call(this, state)
      }
    })
    // 原理同上
    myforEach(actions, (actionName, actionFn) => {
      // this.mutations.change = () => { change(state) }
      this.actions[actionName] = () => {
        actionFn.call(this, this)
      }
    })
    const {commit , dispatch} = this // 先存一份,避免this.commit會覆蓋原型上的this.commit
    // 解構 把this綁定好
    // 經過結構的方式也要先調用這類,而後在下面在調用原型的對應函數
    this.commit = type => {
      commit.call(this, type)
    }
    this.dispatch = type => {
      dispatch.call(this, type)
    }
  }
  get state() { // Object.defineProperty 同理
    return this._vm.state
  }
  // commi調用
  commit (type) {
    this.mutations[type]()
  }
  // dispatch調用
  dispatch (type) {
    this.actions[type]()
  }
}

經過上面的,咱們能夠看出,其實mutationsactions都是把傳入的參數,賦值到store實例上的this.mutationsthis.actions對象裏面。

當組件中this.$store.commit('change')的時候 實際上是調用this.mutations.change(state),就達到了改變數據的效果,actions同理。

getters是經過對Object.defineProperty(this.getters, getterName, {})
對this.getters進行包裝當組件中this.$store.getters.newCount實際上是調用getters對象裏面的newCount(state),而後返回計算結果。就能夠顯示到界面上了。

你們看看完成後的效果圖。

到這裏你們應該懂了vuex的內部代碼的工做流程了,vuex的一半核心應該在這裏了。爲何說一半,由於還有一個核心概念module,也就是vuex的數據的模塊化。

vuex數據模塊化

因爲使用單一狀態樹,應用的全部狀態會集中到一個比較大的對象。當應用變得很是複雜時,store 對象就有可能變得至關臃腫。

爲了解決以上問題,Vuex 容許咱們將 store 分割成模塊(module)。每一個模塊擁有本身的 state、mutation、action、getter、甚至是嵌套子模塊——從上至下進行一樣方式的分割

例以下面的store.js

// 實例化store,參數數對象
export default new Vuex.Store({
  modules: {
    // 模塊a
    a: {
      state: {
        count: 4000
      },
      actions: {
        change ({state}) {
          state.count += 21
        }
      },
      modules: {
        // 模塊b
        b: {
          state: {
            count: 5000
          }
        }
      }
    }
  },
  state: {
    count : 1000
  },
  getters : {
    newCount (state) {
      return state.count + 100
    }
  },
  mutations: {
    change (state) {
      console.log(state.count)
      state.count += 10
    }
  },
  actions: {
    change ({commit}) {
      // 模擬異步
      setTimeout(() => {
        commit('change')
      }, 1000)
    }
  }
})

而後就能夠在界面上就能夠寫上this.$store.state.a.count(顯示a模塊count)this.$store.state.a.b.count(顯示a模塊下,b模塊的count),這裏還有一個要注意的,其實在組件中調用this.$store.dispatch('change')會同時觸發,根的actionsa模塊actions裏面的change函數。

下面咱們就直接去實現models的代碼,也就是整個vuex的實現代碼,

'use strict'

let Vue = null
const myforEach = (obj, callback) => Object.keys(obj).forEach(key => callback(key, obj[key]))

class Store {
  constructor (options) {
    let state = options.state
    this.getters = {}
    this.mutations = {}
    this.actions = {}
    // vuex的核心就是借用vue的實例,由於vuex的數據更改回更新視圖
    this._vm = new Vue({
      data: {
        state
      }
    })

    // 把模塊之間的關係進行整理, 本身根據用戶參數維護了一個對象
    // root._children => a._children => b
    this.modules = new ModulesCollections(options)
    // 不管子模塊仍是 孫子模塊 ,全部的mutations 都是根上的
    // 安裝模塊
    installModules(this, state, [], this.modules.root)

    // 解構 把this綁定好
    const {commit , dispatch} = this
    // 經過結構的方式也要先調用這類,而後在下面在調用原型的對應函數
    this.commit = type => {
      commit.call(this, type)
    }
    this.dispatch = type => {
      dispatch.call(this, type)
    }
  }
  get state() { // Object.defineProperty 同理
    return this._vm.state
  }
  commit (type) {
    // 由於是數組,因此要遍歷執行
    this.mutations[type].forEach(fn => fn())
  }
  dispatch (type) {
    // 由於是數組,因此要遍歷執行
    this.actions[type].forEach(fn => fn())
  }
}

class ModulesCollections {
  constructor (options) { // vuex []
    // 註冊模塊
    this.register([], options)
  }
  register (path, rawModule) {
    // path 是空數組, rawModule 就是個對象
    let newModule = {
      _raw: rawModule, // 對象
      _children: {}, // 把子模塊掛載到這裏
      state: rawModule.state
    }
    if (path.length === 0) { // 第一次
      this.root = newModule
    } else {
      // [a, b] ==> [a]
      let parent = path.slice(0, -1).reduce((root, current) => {
        return root._children[current]
      }, this.root)
      parent._children[path[path.length - 1]] = newModule
    }
    if (rawModule.modules) {
      // 遍歷註冊子模塊
      myforEach(rawModule.modules, (childName, module) => {
        this.register(path.concat(childName), module)
      })
    }
  }
}

// rootModule {_raw, _children, state }
function installModules (store, rootState, path, rootModule) {
  // rootState.a = {count:200}
  // rootState.a.b = {count: 3000}
  if (path.length > 0) {
    // 根據path找到對應的父級模塊
    // 例如 [a] --> path.slice(0, -1) --> []  此時a模塊的父級模塊是跟模塊
    // 例如 [a,b] --> path.slice(0, -1) --> [a]  此時b模塊的父級模塊是a模塊
    let parent = path.slice(0, -1).reduce((root, current) => {
      return root[current]
    }, rootState)
    // 經過Vue.set設置數據雙向綁定
    Vue.set(parent, path[path.length - 1], rootModule.state)
  }
  // 設置getter
  if (rootModule._raw.getters) {
    myforEach(rootModule._raw.getters, (getterName, getterFn) => {
      Object.defineProperty(store.getters, getterName, {
        get: () => {
          return getterFn(rootModule.state)
        }
      })
    })
  }
  // 在跟模塊設置actions
  if (rootModule._raw.actions) {
    myforEach(rootModule._raw.actions, (actionName, actionsFn) => {
      // 由於同是在根模塊設置,子模塊也有能相同的key
      // 全部把全部的都放到一個數組裏面
      // 就變成了例如 [change, change] , 第一個是跟模塊的actions的change,第二個是a模塊的actions的change
      let entry = store.actions[actionName] || (store.actions[actionName] = [])
      entry.push(() => {
        const commit = store.commit
        const state = rootModule.state
        actionsFn.call(store, {state, commit})
      })
    })
  }
  // 在跟模塊設置mutations, 同理上actions
  if (rootModule._raw.mutations) {
    myforEach(rootModule._raw.mutations, (mutationName, mutationFn) => {
      let entry = store.mutations[mutationName] || (store.mutations[mutationName] = [])
      entry.push(() => {
        mutationFn.call(store, rootModule.state)
      })
    })
  }
  // 遞歸遍歷子節點的設置
  myforEach(rootModule._children, (childName, module) => {
    installModules(store, rootState, path.concat(childName), module)
  })
}

const install = _Vue => {
  // 避免vuex重複安裝
  if (Vue === _Vue) return
  Vue = _Vue
  Vue.mixin({
    // 經過mixins讓每一個組件實例化的時候都會執行下面的beforeCreate
    beforeCreate () {
      // 只有跟節點纔有store配置
      if (this.$options && this.$options.store) {
        this.$store = this.$options.store
      } else if (this.$parent && this.$parent.$store) { // 子組件深度優先 父 --> 子---> 孫子
        this.$store = this.$parent.$store
      }
    }
  })
}

export default { install, Store }

看到代碼以及註釋,主要流程就是根據遞歸的方式,處理數據,而後根據傳進來的配置,進行操做數據。

至此,咱們把vuex的代碼實現了一遍,在咱們App.vue的代碼裏添加

<template>
  <div id="app">
    這裏是store的state------->{{this.$store.state.count}} <br/>
    這裏是store的getter------->{{this.$store.getters.newCount}} <br/>
    這裏是store的state.a------->{{this.$store.state.a.count}} <br/>
    <button @click="change">點擊觸發dispach--> actions</button>
    <button @click="change1">點擊觸發commit---> mutations</button>
  </div>
</template>

最後查看結果。

完結撒花~~~

博客文章地址:https://blog.naice.me/article...

源碼地址:https://github.com/naihe138/w...

相關文章
相關標籤/搜索