摘要:深入學習作為配合使用的數據狀態管理庫,針對解決兄弟組件或多層級組件共享數據狀態的痛點問題來說,非常好用。至此,構造函數部分已經過了一遍了。
深入學習Vuex
vuex作為配合vue使用的數據狀態管理庫,針對解決兄弟組件或多層級組件共享數據狀態的痛點問題來說,非常好用。本文以使用者的角度,結合源碼來學習vuex。其中也參考了許多前輩的文章,參見最后的Reference
Vue加載Vuex(Vue.use(Vuex))Vue加載Vuex還是很簡單的,讓我們以官方文檔上實例為切入點來開始認識Vuex
import Vue from "vue" import Vuex from "vuex" import cart from "./modules/cart" import products from "./modules/products" import createLogger from "../../../src/plugins/logger" Vue.use(Vuex) const debug = process.env.NODE_ENV !== "production" export default new Vuex.Store({ modules: { cart, products }, strict: debug, plugins: debug ? [createLogger()] : [] })
這段代碼我們再熟悉不過了,就是Vue加載Vuex插件,然后new了一個Vuex實例。
我們一步一步來看,首先看一下Vue如何加載的Vuex,也就是Vue.use(Vuex)發生了什么。
Vue.use = function (plugin: Function | Object) { /* istanbul ignore if */ /*標識位檢測該插件是否已經被安裝*/ if (plugin.installed) { return } // additional parameters const args = toArray(arguments, 1) /*將this(Vue構造函數)加入數組頭部*/ args.unshift(this) if (typeof plugin.install === "function") { /*install執行插件安裝*/ plugin.install.apply(plugin, args) } else if (typeof plugin === "function") { plugin.apply(null, args) } //標記插件已安裝 plugin.installed = true return this }
主要做了幾件事:
驗證是否已安裝,避免重復
如果插件提供install方法,則執行否則把插件當作function執行
最后標記插件已安裝
那么Vuex提供install方法了嗎?答案是肯定的
let Vue // bind on install export function install (_Vue) { if (Vue) { /*避免重復安裝(Vue.use內部也會檢測一次是否重復安裝同一個插件)*/ if (process.env.NODE_ENV !== "production") { console.error( "[vuex] already installed. Vue.use(Vuex) should be called only once." ) } return } /*保存Vue,同時用于檢測是否重復安裝*/ Vue = _Vue//Vue構造函數 /*將vuexInit混淆進Vue的beforeCreate(Vue2.0)或_init方法(Vue1.0)*/ applyMixin(Vue) }
看mixin之前我們可以先思考一個問題,我們在訪問Vuex的數據的時候基本都是這樣訪問的,比如this.user = this.$store.state.global.user,this.$store是什么時候加到Vue實例上的?applyMixin會給出答案,讓我們繼續看applyMixin發生了什么
// applyMixin: export default function (Vue) { /*獲取Vue版本,鑒別Vue1.0還是Vue2.0*/ const version = Number(Vue.version.split(".")[0]) if (version >= 2) { /*通過mixin將vuexInit混淆到Vue實例的beforeCreate鉤子中*/ Vue.mixin({ beforeCreate: vuexInit }) } else { // override init and inject vuex init procedure // for 1.x backwards compatibility. /*將vuexInit放入_init中調用*/ const _init = Vue.prototype._init Vue.prototype._init = function (options = {}) { options.init = options.init ? [vuexInit].concat(options.init) : vuexInit _init.call(this, options) } } /** * Vuex init hook, injected into each instances init hooks list. */ /*Vuex的init鉤子,會存入每一個Vue實例等鉤子列表*/ function vuexInit () { // this = vue object const options = this.$options // store injection if (options.store) { /*存在store其實代表的就是Root節點,直接執行store(function時)或者使用store(非function)*/ this.$store = typeof options.store === "function" ? options.store() : options.store } else if (options.parent && options.parent.$store) { /*子組件直接從父組件中獲取$store,這樣就保證了所有組件都公用了全局的同一份store*/ this.$store = options.parent.$store } } }
我們這里就只看2.0了,思路就是通過Vue.mixin把掛載$store的動作放在beforeCreate鉤子上,由此實現了每個組件實例都可以通過this.$store來直接訪問數據。
注意:mixin的細節
同名鉤子函數將混合為一個數組,因此都將被調用。另外,混入對象的鉤子將在組件自身鉤子之前調用。
使用全局混入對象,將會影響到 所有 之后創建的 Vue 實例。
至此,Vue.use(Vuex)我們已經了解完了。
Vuex結構總覽順著我們實例代碼的思路,接下來我們應該開始看構造器了,不過開始看之前,我們先看一下Vuex.store Class都定義了些什么。
export class Store { constructor (options = {}) { } // state 取值函數(getter) get state () { } //存值函數(setter) set state (v) { } /* 調用mutation的commit方法 */ commit (_type, _payload, _options) { } /* 調用action的dispatch方法 */ dispatch (_type, _payload) { } /* 注冊一個訂閱函數,返回取消訂閱的函數 */ subscribe (fn) { } /* 觀察一個getter方法 */ watch (getter, cb, options) { } /* 重置state */ replaceState (state) { } /* 注冊一個動態module,當業務進行異步加載的時候,可以通過該接口進行注冊動態module */ registerModule (path, rawModule) { } /* 注銷一個動態module */ unregisterModule (path) { } /* 熱更新 */ hotUpdate (newOptions) { } /* 保證通過mutation修改store的數據 */ // 內部使用,比如當外部強行改變state的數據時直接報錯 _withCommit (fn) { } }
以上就是定義的接口了,官方文檔上實例屬性和方法都在這里找得到。
來看一張大神畫的圖有助于理解思路
出處見最后。
接下來我們繼續實例思路
export default new Vuex.Store({ modules: { cart, products }, strict: debug, plugins: debug ? [createLogger()] : [] })
來看一下構造函數
constructor (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 /* 在瀏覽器環境下,如果插件還未安裝(!Vue即判斷是否未安裝),則它會自動安裝。 它允許用戶在某些情況下避免自動安裝。 */ if (!Vue && typeof window !== "undefined" && window.Vue) { install(window.Vue) // 將store注冊到實例或conponent } if (process.env.NODE_ENV !== "production") { assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`) assert(typeof Promise !== "undefined", `vuex requires a Promise polyfill in this browser.`) //檢查是不是new 操作符調用的 assert(this instanceof Store, `Store must be called with the new operator.`) } const { /*一個數組,包含應用在 store 上的插件方法。這些插件直接接收 store 作為唯一參數,可以監聽 mutation(用于外部地數據持久化、記錄或調試)或者提交 mutation (用于內部數據,例如 websocket 或 某些觀察者)*/ plugins = [], /*使 Vuex store 進入嚴格模式,在嚴格模式下,任何 mutation 處理函數以外修改 Vuex state 都會拋出錯誤。*/ strict = false } = options /*從option中取出state,如果state是function則執行,最終得到一個對象*/ let { state = {} } = options if (typeof state === "function") { state = state() } // store internal state /* 用來判斷嚴格模式下是否是用mutation修改state的 */ this._committing = false /* 存放action */ this._actions = Object.create(null) /* 存放mutation */ this._mutations = Object.create(null) /* 存放getter */ //包裝后的getter this._wrappedGetters = Object.create(null) /* module收集器 */ this._modules = new ModuleCollection(options) /* 根據namespace存放module */ this._modulesNamespaceMap = Object.create(null) /* 存放訂閱者 外部插件使用 */ this._subscribers = [] /* 用以實現Watch的Vue實例 */ this._watcherVM = new Vue() // bind commit and dispatch to self /*將dispatch與commit調用的this綁定為store對象本身,否則在組件內部this.dispatch時的this會指向組件的vm*/ const store = this const {dispatch, commit} = this /* 為dispatch與commit綁定this(Store實例本身) */ this.dispatch = function boundDispatch (type, payload) { return dispatch.call(store, type, payload) } this.commit = function boundCommit (type, payload, options) { return commit.call(store, type, payload, options) } // strict mode /*嚴格模式(使 Vuex store 進入嚴格模式,在嚴格模式下,任何 mutation 處理函數以外修改 Vuex state 都會拋出錯誤)*/ this.strict = strict // init root module. // this also recursively registers all sub-modules // and collects all module getters inside this._wrappedGetters /*初始化根module,這也同時遞歸注冊了所有子modle,收集所有module的getter到_wrappedGetters中去,this._modules.root代表根module才獨有保存的Module對象*/ installModule(this, state, [], this._modules.root) // initialize the store vm, which is responsible for the reactivity // (also registers _wrappedGetters as computed properties) /* 通過vm重設store,新建Vue對象使用Vue內部的響應式實現注冊state以及computed */ resetStoreVM(this, state) // apply plugins /* 調用插件 */ plugins.forEach(plugin => plugin(this)) /* devtool插件 */ if (Vue.config.devtools) { devtoolPlugin(this) } }
Vuex的源碼一共就一千行左右,構造函數吃透基本掌握至少一半了,構造函數中主要是初始化各種屬性。簡單的詳見注釋,這里我們主要看如何解析處理modules,首先來看this._modules = new ModuleCollection(options),ModuleCollection結構如下
import Module from "./module" import { assert, forEachValue } from "../util" /*module收集類*/ export default class ModuleCollection { constructor (rawRootModule) { // new store(options) // register root module (Vuex.Store options) this.register([], rawRootModule, false) } /*獲取父級module*/ get (path) { } /* 獲取namespace,當namespaced為true的時候會返回"moduleName/name" 默認情況下,模塊內部的 action、mutation 和 getter 是注冊在全局命名空間的——這樣使得多個模塊能夠對同一 mutation 或 action 作出響應。 如果希望你的模塊更加自包含或提高可重用性,你可以通過添加 namespaced: true 的方式使其成為命名空間模塊。 當模塊被注冊后,它的所有 getter、action 及 mutation 都會自動根據模塊注冊的路徑調整命名。 */ getNamespace (path) { } update (rawRootModule) { } /*注冊*/ register (path, rawModule, runtime = true) { if (process.env.NODE_ENV !== "production") { assertRawModule(path, rawModule) } /*新建一個Module對象*/ const newModule = new Module(rawModule, runtime) if (path.length === 0) { /*path為空數組的代表跟節點*/ this.root = newModule } else { /*獲取父級module*/ const parent = this.get(path.slice(0, -1))//排除倒數第一個元素的數組, /*在父module中插入一個子module*/ parent.addChild(path[path.length - 1], newModule) } // register nested modules /*遞歸注冊module*/ if (rawModule.modules) { forEachValue(rawModule.modules, (rawChildModule, key) => { // concat不改變源數組,返回合并后的數組 this.register(path.concat(key), rawChildModule, runtime) }) } } /*注銷*/ unregister (path) { } } /*Module構造類*/ export default class Module { constructor (rawModule, runtime) { this.runtime = runtime this._children = Object.create(null) /*保存module*/ this._rawModule = rawModule /*保存modele的state*/ const rawState = rawModule.state this.state = (typeof rawState === "function" ? rawState() : rawState) || {} } /* 獲取namespace */ get namespaced () { } /*插入一個子module,存入_children中*/ addChild (key, module) { this._children[key] = module } /*移除一個子module*/ removeChild (key) { } /*根據key獲取子module*/ getChild (key) { return this._children[key] } /* 更新module */ update (rawModule) { } /* 遍歷child */ forEachChild (fn) { } /* 遍歷getter */ forEachGetter (fn) { } /* 遍歷action */ forEachAction (fn) { } /* 遍歷matation */ forEachMutation (fn) { } }
ModuleCollection的作用就是收集和管理Module,構造函數中對我們傳入的options的module進行了注冊(register 是重點,詳見注釋,注冊方法中使用了遞歸,以此來注冊所有的module)。我們給出的實例經過ModuleCollection的收集之后變成了什么樣子呢?
//this._modules簡單結構示例 { root: { state: {}, _children: { cart: { ... }, products: { ... } } } }
收集完了之后,我們看一下是如何初始化這些module的installModule(this, state, [], this._modules.root)
function installModule (store, rootState, path, module, hot) { /* 是否是根module */ const isRoot = !path.length /* 獲取module的namespace */ const namespace = store._modules.getNamespace(path) // register in namespace map /* 如果有namespace則在_modulesNamespaceMap中注冊 */ if (module.namespaced) { store._modulesNamespaceMap[namespace] = module } // set state if (!isRoot && !hot) { /* 獲取父級的state */ const parentState = getNestedState(rootState, path.slice(0, -1))//深度取值,并返回取到的值 /* module的name */ const moduleName = path[path.length - 1] store._withCommit(() => {// 添加響應式屬性 /* 將子module設置稱響應式的 */ Vue.set(parentState, moduleName, module.state) }) } // 當前模塊上下文信息 const local = module.context = makeLocalContext(store, namespace, path) /* 遍歷注冊mutation */ //mutation:key對應的handler module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(store, namespacedType, mutation, local) }) /* 遍歷注冊action */ module.forEachAction((action, key) => { const namespacedType = namespace + key registerAction(store, namespacedType, action, local) }) /* 遍歷注冊getter */ module.forEachGetter((getter, key) => { const namespacedType = namespace + key registerGetter(store, namespacedType, getter, local) }) /* 遞歸安裝mudule */ module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot) }) }
在這里構造了各個module的信息也就是localConext,包括各個模塊的mutation,action ,getter ,mudule ,其中運用到了許多包裝的技巧(主要為了計算模塊的路徑),還有代理的方式訪問數據,詳見注釋
resetStoreVM方法思路就是借助Vue響應式來實現Vuex的響應式
/* 通過vm重設store,新建Vue對象使用Vue內部的響應式實現注冊state以及computed */ function resetStoreVM (store, state, hot) { /* 存放之前的vm對象 */ const oldVm = store._vm // bind store public getters store.getters = {} const wrappedGetters = store._wrappedGetters const computed = {} /* 通過Object.defineProperty為每一個getter方法設置get方法,比如獲取this.$store.getters.test的時候獲取的是store._vm.test,也就是Vue對象的computed屬性 */ // key = wrappedGetters的key,fn = wrappedGetters的key對應的value forEachValue(wrappedGetters, (fn, key) => { // use computed to leverage its lazy-caching mechanism computed[key] = () => fn(store) // store.getter并沒有像state一樣在class直接注冊了getter,setter,而是在這里定義的 // 用過代理的方式借助Vue計算屬性實現了Vuex的計算屬性 Object.defineProperty(store.getters, key, { get: () => 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 const silent = Vue.config.silent /* Vue.config.silent暫時設置為true的目的是在new一個Vue實例的過程中不會報出一切警告 */ Vue.config.silent = true /* 這里new了一個Vue對象,運用Vue內部的響應式實現注冊state以及computed*/ //通過Vue的數據劫持,創造了dep,在Vue實例中使用的話Watcher會收集依賴,以達到響應式的目的 store._vm = new Vue({ data: { $$state: state }, computed }) Vue.config.silent = silent // enable strict mode for new vm /* 使能嚴格模式,保證修改store只能通過mutation */ if (store.strict) { enableStrictMode(store) } if (oldVm) { /* 解除舊vm的state的引用,以及銷毀舊的Vue對象 */ if (hot) { // dispatch changes in all subscribed watchers // to force getter re-evaluation for hot reloading. store._withCommit(() => { oldVm._data.$$state = null }) } Vue.nextTick(() => oldVm.$destroy()) } }
在這里只要是把Vuex也構造為響應式的,store._vm指向一個Vue的實例,借助Vue數據劫持,創造了dep,在組件實例中使用的話Watcher會收集依賴,以達到響應式的目的。
至此,構造函數部分已經過了一遍了。
本文主要是學習了Vuex的初始化部分,實際的Vuex的api接口的實現也有相關的中文注釋,我已經把主要部分中文注釋代碼放在這里,需者自取中文注釋代碼
學習過程中參考了許多大神的文章,一并感謝。
Vue.js 源碼解析(參考了大神的許多注釋,大神寫的太詳盡了,我只補充了一部分)
Vuex框架原理與源碼分析
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/95889.html
摘要:前言本文內容講解的內容一張思維導圖輔助你深入了解源碼架構。總結以上內容是筆者最近學習源碼時的收獲與所做的筆記,本文內容大多是開源項目技術揭秘的內容,只不過是以思維導圖的形式來展現,內容有省略,還加入了筆者的一點理解。1.前言 本文內容講解的內容:一張思維導圖輔助你深入了解 Vue | Vue-Router | Vuex 源碼架構。 項目地址:github.com/biaochenxuy… 文...
摘要:深入學習作為配合使用的數據狀態管理庫,針對解決兄弟組件或多層級組件共享數據狀態的痛點問題來說,非常好用。至此,構造函數部分已經過了一遍了。 深入學習Vuex vuex作為配合vue使用的數據狀態管理庫,針對解決兄弟組件或多層級組件共享數據狀態的痛點問題來說,非常好用。本文以使用者的角度,結合源碼來學習vuex。其中也參考了許多前輩的文章,參見最后的Reference Vue加載Vuex...
閱讀 1252·2021-09-01 10:30
閱讀 2126·2021-07-23 10:38
閱讀 901·2019-08-29 15:06
閱讀 3159·2019-08-29 13:53
閱讀 3281·2019-08-26 11:54
閱讀 1834·2019-08-26 11:38
閱讀 2376·2019-08-26 10:29
閱讀 3132·2019-08-23 18:15