Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式,它采用集中式存儲管理應用的所有組件的狀態,注意:使用前需要先加載vue文件才可以使用(在node.js下需要使用Vue.use(Vuex)來安裝vuex插件,在瀏覽器環境下直接加載即可,vuex會自行安裝)
vuex的使用方法很簡單,首先調用new Vuex.Store(options)創建一個store實例即可,然后在創建vue實例時把這個store實例作為store屬性傳入即可,調用new Vuex.Store(options)創建一個vuex實例時可以傳入如下參數:
state 存儲的數據
getters 可以認為是store的計算屬性
mutations 這是更改Vuex的store里的數據的唯一方法,只能是同步方法(官網這樣寫的,其實不贊同這個說法,具體請看下面)
actions 可以包含一些異步操作,它提交的是mutation,而不是直接變更狀態。
modules 為了更方便的管理倉庫,我們把一個大的store拆成一些modules(子倉庫),整個modules是一個樹型結構
strict 是否開啟嚴格模式,無論何時發生了狀態變更且不是由mutation函數引起的,將會拋出錯誤,這能保證所有的狀態變更都能被調試工具跟蹤到。 ;默認為false
后面介紹每個api時單獨介紹用法,舉個栗子,如下:
writer by:大沙漠 QQ:22969969
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script> <script src="https://unpkg.com/vuex@3.1.0/dist/vuex.js"></script> </head> <body> <div id="app"> <p>{{message}}</p> <p>{{reverseMessage}}</p> <p>{{no}}</p> <button @click="test1">mutation測試</button> <button @click="test2">action測試</button> </div> <script> const store = new Vuex.Store({ state:{message:'Hello World',no:123}, getters:{ //getters類似於Vue的計算屬性 reverseMessage:state=>{return state.message.split('').reverse().join('')}, increment:state=>{state.no++} }, mutations:{ //mutation包含一些同步操作 increment(state,payload){state.no+=payload.no} }, actions:{ //actions包含一些異步操作 increment({commit},info){ setTimeout(function(){ commit('increment',info) },500) } } }) var app = new Vue({ el:"#app", store, computed:{ no:function(){return this.$store.state.no}, message:function(){return this.$store.state.message}, reverseMessage:function(){return this.$store.getters.reverseMessage} }, methods:{ test1:function(){this.$store.commit('increment',{no:10})}, test2:function(){this.$store.dispatch('increment',{no:10})} } }) </script> </body> </html>
渲染如下:
我們點擊mutation測試這個按鈕123會這個數字會立馬遞增10,而點擊action測試這個按鈕,數字會延遲0.5秒,再遞增10,前者是mutation對應的同步操作,而后者是action對應的異步操作
如果只是這樣顯式數據,感覺vuex沒有什么用處,我們在瀏覽器里輸入store.state.message="Hello Vue"來直接修改state里的數據看看怎么樣,如下:
修改后頁面里的內容立即就變化了,如下:
是不是很神奇,這里只是一個組件引用了vuex,如果很多的組件都引用了同一個vuex實例,那么只要狀態發生變化,對應的組件都會自動更新,這就是vuex的作用。
vuex官網說mutations是更改store里數據的唯一方法,這在邏輯上不嚴謹的,只有設置了strict為true,那么說mutations是更改store里數據的唯一方法還可以接收,比如我們在控制台里直接修改store里的數據了,也沒報錯啥的。
vuex內部的實現原理很簡單,就是定義一個vue實例,把vuex.store里的state作為data屬性(不是根data,而是放到$$state這個屬性里,不過由於值是個對象,因此也是響應式的),getters作為計算屬性來實現的
源碼分析
我們先看看vuex插件導出了哪些符號,打開vuex的源文件,拉到最底部,如下:
var index = { Store: Store, //初始化 install: install, //安裝方法 version: '3.1.0', //版本號 mapState: mapState, //State輔助函數 mapMutations: mapMutations, //Mutations輔助函數 mapGetters: mapGetters, //Getters輔助函數 mapActions: mapActions, //Actions輔助函數 createNamespacedHelpers: createNamespacedHelpers };
可以看到Store就是初始化函數,install是安裝用的,version是版本號,其它幾個都是輔助函數,最后一個是和輔助函數的上下文綁定(也就是命名空間)相關,一般用不到。
我們先看看安裝流程,如下:
function install (_Vue) { //安裝Vuex if (Vue && _Vue === Vue) { //如果Veue存在且等於參數_Vue,表示已經安裝過了,則報錯 { console.error( '[vuex] already installed. Vue.use(Vuex) should be called only once.' ); } return } Vue = _Vue; //將_Vue保存到局部變量Vue里 applyMixin(Vue); //調用applyMixin()進行初始化 }
安裝時最后會執行applyMixin函數,該函數如下:
function applyMixin (Vue) { //將Vuex混入到Vue里面 var version = Number(Vue.version.split('.')[0]); //獲取主版本號 if (version >= 2) { //如果是Vue2.0及以上版 Vue.mixin({ beforeCreate: vuexInit }); //則執行Vue.mixin()方法,植入一個beforeCreate回調函數 } else { // override init and inject vuex init procedure // for 1.x backwards compatibility. var _init = Vue.prototype._init; Vue.prototype._init = function (options) { if ( options === void 0 ) options = {}; options.init = options.init ? [vuexInit].concat(options.init) : vuexInit; _init.call(this, options); }; } /** * Vuex init hook, injected into each instances init hooks list. */ function vuexInit () { //Vuex的安裝方法 var options = this.$options; // store injection if (options.store) { //如果options.store存在,即初始化Vue實例時傳入了store實例 this.$store = typeof options.store === 'function' //則將store保存到大Vue的$store屬性上,如果store是個函數,則執行該函數 ? options.store() : options.store; } else if (options.parent && options.parent.$store) { //如果options.store不存在,但是父實例存在$store(組件的情況下) this.$store = options.parent.$store; //則設置this.$store為父實例的$store } } }
這樣不管是根vue實例,還是組件,都可以通過this.$store來獲取到對應的$store實例了,安裝就是這樣子,下面說一下整體流程
以上面的例子為例,當我們執行new Vuex.Store()創建一個Vuex.Store的實例時會執行到導出符號的Store函數,如下:
var Store = function Store (options) { //構造函數 var this$1 = this; if ( options === void 0 ) options = {}; // Auto install if it is not done yet and `window` has `Vue`. // To allow users to avoid auto-installation in some cases, // this code should be placed here. See #731 if (!Vue && typeof window !== 'undefined' && window.Vue) { //如果局部變量Vue不存在且window.Vue存在,即已經引用了Vue,而且window.Vue不存在(還沒安裝) install(window.Vue); //執行install()方法進行安裝 ;從這里看出在瀏覽器環境下不需要執行Vue.use(vuex),在執行new Vuex.Store()會自己安裝 } { assert(Vue, "must call Vue.use(Vuex) before creating a store instance."); assert(typeof Promise !== 'undefined', "vuex requires a Promise polyfill in this browser."); assert(this instanceof Store, "store must be called with the new operator."); } var plugins = options.plugins; if ( plugins === void 0 ) plugins = []; var strict = options.strict; if ( strict === void 0 ) strict = false; // store internal state this._committing = false; this._actions = Object.create(null); this._actionSubscribers = []; this._mutations = Object.create(null); this._wrappedGetters = Object.create(null); this._modules = new ModuleCollection(options); //初始化modules,ModuleCollection對象是收集所有模塊信息的 this._modulesNamespaceMap = Object.create(null); this._subscribers = []; this._watcherVM = new Vue(); // bind commit and dispatch to self var store = this; var ref = this; var dispatch = ref.dispatch; var commit = ref.commit; this.dispatch = function boundDispatch (type, payload) { //重寫dispatch方法,將上下文設置為當前的this實例 return dispatch.call(store, type, payload) }; this.commit = function boundCommit (type, payload, options) { //重寫commit方法,將上下文設置為當前的this實例 return commit.call(store, type, payload, options) }; // strict mode this.strict = strict; var state = this._modules.root.state; //獲取根倉庫的state信息 // init root module. // this also recursively registers all sub-modules // and collects all module getters inside this._wrappedGetters installModule(this, state, [], this._modules.root); //安裝根模塊,該函數會遞歸調用的安裝子模塊,並收集它們的getters到this._wrappendGetters屬性上 // initialize the store vm, which is responsible for the reactivity // (also registers _wrappedGetters as computed properties) resetStoreVM(this, state); //安裝vm,也就是這里會創建一個vue實例,並把state、getter作為響應式對象 // apply plugins plugins.forEach(function (plugin) { return plugin(this$1); }); //安裝插件 var useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools; if (useDevtools) { devtoolPlugin(this); } };
ModuleCollection模塊會收集根模塊和子模塊的的所有信息,例子里執行到這里時對應的this._modules如下:
然后會調用執行到installModule()會安裝每個模塊,也就是把每個模塊的getters、mutations、actions進行一系列處理,如果還有子模塊(module屬性)則遞歸調用installModule依次處理每個子模塊,如下:
function installModule (store, rootState, path, module, hot) { //安裝模塊 var isRoot = !path.length; //當前是否為根Module var namespace = store._modules.getNamespace(path); //獲取命名空間 // register in namespace map if (module.namespaced) { store._modulesNamespaceMap[namespace] = module; } // set state if (!isRoot && !hot) { var parentState = getNestedState(rootState, path.slice(0, -1)); var moduleName = path[path.length - 1]; store._withCommit(function () { Vue.set(parentState, moduleName, module.state); }); } var local = module.context = makeLocalContext(store, namespace, path); module.forEachMutation(function (mutation, key) { //遍歷module模塊的mutations對象 var namespacedType = namespace + key; registerMutation(store, namespacedType, mutation, local); //調用registerMutation注冊mutation }); module.forEachAction(function (action, key) { //遍歷module模塊的actions對象 var type = action.root ? key : namespace + key; var handler = action.handler || action; registerAction(store, type, handler, local); //調用registerAction注冊action }); module.forEachGetter(function (getter, key) { //遍歷module模塊的getter對象 var namespacedType = namespace + key; registerGetter(store, namespacedType, getter, local); //調用registerGetter注冊getter }); module.forEachChild(function (child, key) { //如果有定義了module(存在子模塊的情況) installModule(store, rootState, path.concat(key), child, hot); //則遞歸調用installModule }); }
最后會執行resetStoreVM()函數,該函數內部會創建一個vue實例,這樣state和getters就是響應式數據了,如下:
function resetStoreVM (store, state, hot) { //重新存儲數據 var oldVm = store._vm; // bind store public getters store.getters = {}; var wrappedGetters = store._wrappedGetters; //獲取store的所有getter數組信息 var computed = {}; forEachValue(wrappedGetters, function (fn, key) { //遍歷wrappedGetters // use computed to leverage its lazy-caching mechanism computed[key] = function () { return fn(store); }; //將getter保存到computed里面 Object.defineProperty(store.getters, key, { get: function () { return store._vm[key]; }, enumerable: true // for local getters }); }); // use a Vue instance to store the state tree // suppress warnings just in case the user has added // some funky global mixins var silent = Vue.config.silent; //保存Vue.config.silent的配置 Vue.config.silent = true; //設置Vue.config.silent配置屬性為true(先關閉警告) store._vm = new Vue({ //創建new Vue()實例把$$state和computed變成響應式的 data: { $$state: state }, computed: computed }); Vue.config.silent = silent; //將Vue.config.silent復原回去 // enable strict mode for new vm if (store.strict) { enableStrictMode(store); } if (oldVm) { if (hot) { // dispatch changes in all subscribed watchers // to force getter re-evaluation for hot reloading. store._withCommit(function () { oldVm._data.$$state = null; }); } Vue.nextTick(function () { return oldVm.$destroy(); }); } }
這樣整個流程就跑完了,就是內部創建一個vue實例,利用vue的響應式做數據動態響應。