摘要:源碼閱讀分析是專為開發(fā)的統(tǒng)一狀態(tài)管理工具。本文將會分析的整個實現(xiàn)思路,當(dāng)是自己讀完源碼的一個總結(jié)。再次回到構(gòu)造函數(shù),接下來是各類插件的注冊插件注冊到這里的初始化工作已經(jīng)完成。
Vuex源碼閱讀分析
Vuex是專為Vue開發(fā)的統(tǒng)一狀態(tài)管理工具。當(dāng)我們的項目不是很復(fù)雜時,一些交互可以通過全局事件總線解決,但是這種觀察者模式有些弊端,開發(fā)時可能沒什么感覺,但是當(dāng)項目變得復(fù)雜,維護(hù)時往往會摸不著頭腦,如果是后來加入的伙伴更會覺得很無奈。這時候可以采用Vuex方案,它可以使得我們的項目的數(shù)據(jù)流變得更加清晰。本文將會分析Vuex的整個實現(xiàn)思路,當(dāng)是自己讀完源碼的一個總結(jié)。
目錄結(jié)構(gòu)module:提供對module的處理,主要是對state的處理,最后構(gòu)建成一棵module tree
plugins:和devtools配合的插件,提供像時空旅行這樣的調(diào)試功能。
helpers:提供如mapActions、mapMutations這樣的api
index、index.esm:源碼的主入口,拋出Store和mapActions等api,一個用于commonjs的打包、一個用于es module的打包
mixin:提供install方法,用于注入$store
store:vuex的核心代碼
util:一些工具函數(shù),如deepClone、isPromise、assert
源碼分析先從一個簡單的示例入手,一步一步分析整個代碼的執(zhí)行過程,下面是官方提供的簡單示例
// store.js import Vue from "vue" import Vuex from "vuex" Vue.use(Vuex)1. Vuex的注冊
Vue官方建議的插件使用方法是使用Vue.use方法,這個方法會調(diào)用插件的install方法,看看install方法都做了些什么,從index.js中可以看到install方法在store.js中拋出,部分代碼如下
let Vue // bind on install export function install (_Vue) { if (Vue && _Vue === Vue) { if (process.env.NODE_ENV !== "production") { console.error( "[vuex] already installed. Vue.use(Vuex) should be called only once." ) } return } Vue = _Vue applyMixin(Vue) }
聲明了一個Vue變量,這個變量在install方法中會被賦值,這樣可以給當(dāng)前作用域提供Vue,這樣做的好處是不需要額外import Vue from "vue" 不過我們也可以這樣寫,然后讓打包工具不要將其打包,而是指向開發(fā)者所提供的Vue,比如webpack的externals,這里就不展開了。執(zhí)行install會先判斷Vue是否已經(jīng)被賦值,避免二次安裝。然后調(diào)用applyMixin方法,代碼如下
// applyMixin export default function (Vue) { const version = Number(Vue.version.split(".")[0]) if (version >= 2) { Vue.mixin({ beforeCreate: vuexInit }) } else { // override init and inject vuex init procedure // for 1.x backwards compatibility. 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. */ function vuexInit () { const options = this.$options // store injection // 當(dāng)我們在執(zhí)行new Vue的時候,需要提供store字段 if (options.store) { // 如果是root,將store綁到this.$store this.$store = typeof options.store === "function" ? options.store() : options.store } else if (options.parent && options.parent.$store) { // 否則拿parent上的$store // 從而實現(xiàn)所有組件共用一個store實例 this.$store = options.parent.$store } } }
這里會區(qū)分vue的版本,2.x和1.x的鉤子是不一樣的,如果是2.x使用beforeCreate,1.x即使用_init。當(dāng)我們在執(zhí)行new Vue啟動一個Vue應(yīng)用程序時,需要給上store字段,根組件從這里拿到store,子組件從父組件拿到,這樣一層一層傳遞下去,實現(xiàn)所有組件都有$store屬性,這樣我們就可以在任何組件中通過this.$store訪問到store
接下去繼續(xù)看例子
// store.js export default new Vuex.Store({ state: { count: 0 }, getters: { evenOrOdd: state => state.count % 2 === 0 ? "even" : "odd" }, actions: { increment: ({ commit }) => commit("increment"), decrement: ({ commit }) => commit("decrement") }, mutations: { increment (state) { state.count++ }, decrement (state) { state.count-- } } })
// app.js new Vue({ el: "#app", store, // 傳入store,在beforeCreate鉤子中會用到 render: h => h(Counter) })2.store初始化
這里是調(diào)用Store構(gòu)造函數(shù),傳入一個對象,包括state、actions等等,接下去看看Store構(gòu)造函數(shù)都做了些什么
export class Store { constructor (options = {}) { if (!Vue && typeof window !== "undefined" && window.Vue) { // 掛載在window上的自動安裝,也就是通過script標(biāo)簽引入時不需要手動調(diào)用Vue.use(Vuex) install(window.Vue) } if (process.env.NODE_ENV !== "production") { // 斷言必須使用Vue.use(Vuex),在install方法中會給Vue賦值 // 斷言必須存在Promise // 斷言必須使用new操作符 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.`) } const { plugins = [], strict = false } = options // store internal state this._committing = false this._actions = Object.create(null) this._actionSubscribers = [] this._mutations = Object.create(null) this._wrappedGetters = Object.create(null) // 這里進(jìn)行module收集,只處理了state this._modules = new ModuleCollection(options) // 用于保存namespaced的模塊 this._modulesNamespaceMap = Object.create(null) // 用于監(jiān)聽mutation this._subscribers = [] // 用于響應(yīng)式地監(jiān)測一個 getter 方法的返回值 this._watcherVM = new Vue() // 將dispatch和commit方法的this指針綁定到store上,防止被修改 const store = this const { dispatch, commit } = this 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 this.strict = strict const state = this._modules.root.state // 這里是module處理的核心,包括處理根module、action、mutation、getters和遞歸注冊子module installModule(this, state, [], this._modules.root) // 使用vue實例來保存state和getter resetStoreVM(this, state) // 插件注冊 plugins.forEach(plugin => plugin(this)) if (Vue.config.devtools) { devtoolPlugin(this) } } }
首先會判斷Vue是不是掛載在window上,如果是的話,自動調(diào)用install方法,然后進(jìn)行斷言,必須先調(diào)用Vue.use(Vuex)。必須提供Promise,這里應(yīng)該是為了讓Vuex的體積更小,讓開發(fā)者自行提供Promise的polyfill,一般我們可以使用babel-runtime或者babel-polyfill引入。最后斷言必須使用new操作符調(diào)用Store函數(shù)。
接下去是一些內(nèi)部變量的初始化
_committing提交狀態(tài)的標(biāo)志,在_withCommit中,當(dāng)使用mutation時,會先賦值為true,再執(zhí)行mutation,修改state后再賦值為false,在這個過程中,會用watch監(jiān)聽state的變化時是否_committing為true,從而保證只能通過mutation來修改state
_actions用于保存所有action,里面會先包裝一次
_actionSubscribers用于保存訂閱action的回調(diào)
_mutations用于保存所有的mutation,里面會先包裝一次
_wrappedGetters用于保存包裝后的getter
_modules用于保存一棵module樹
_modulesNamespaceMap用于保存namespaced的模塊
接下去的重點是
this._modules = new ModuleCollection(options)
接下去看看ModuleCollection函數(shù)都做了什么,部分代碼如下
// module-collection.js export default class ModuleCollection { constructor (rawRootModule) { // 注冊 root module (Vuex.Store options) this.register([], rawRootModule, false) } get (path) { // 根據(jù)path獲取module return path.reduce((module, key) => { return module.getChild(key) }, this.root) } /* * 遞歸注冊module path是路徑 如 * { * modules: { * a: { * state: {} * } * } * } * a模塊的path => ["a"] * 根模塊的path => [] */ register (path, rawModule, runtime = true) { if (process.env.NODE_ENV !== "production") { // 斷言 rawModule中的getters、actions、mutations必須為指定的類型 assertRawModule(path, rawModule) } // 實例化一個module const newModule = new Module(rawModule, runtime) if (path.length === 0) { // 根module 綁定到root屬性上 this.root = newModule } else { // 子module 添加其父module的_children屬性上 const parent = this.get(path.slice(0, -1)) parent.addChild(path[path.length - 1], newModule) } // 如果當(dāng)前模塊存在子模塊(modules字段) // 遍歷子模塊,逐個注冊,最終形成一個樹 if (rawModule.modules) { forEachValue(rawModule.modules, (rawChildModule, key) => { this.register(path.concat(key), rawChildModule, runtime) }) } } } // module.js export default class Module { constructor (rawModule, runtime) { // 初始化時runtime為false this.runtime = runtime // Store some children item // 用于保存子模塊 this._children = Object.create(null) // Store the origin module object which passed by programmer // 保存原來的moudle,在Store的installModule中會處理actions、mutations等 this._rawModule = rawModule const rawState = rawModule.state // Store the origin module"s state // 保存state this.state = (typeof rawState === "function" ? rawState() : rawState) || {} } addChild (key, module) { // 將子模塊添加到_children中 this._children[key] = module } }
這里調(diào)用ModuleCollection構(gòu)造函數(shù),通過path的長度判斷是否為根module,首先進(jìn)行根module的注冊,然后遞歸遍歷所有的module,子module 添加其父module的_children屬性上,最終形成一棵樹
接著,還是一些變量的初始化,然后
// 綁定commit和dispatch的this指針 const store = this const { dispatch, commit } = this 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) }
這里會將dispath和commit方法的this指針綁定為store,比如下面這樣的騷操作,也不會影響到程序的運行
this.$store.dispatch.call(this, "someAction", payload)
接著是store的核心代碼
// 這里是module處理的核心,包括處理根module、命名空間、action、mutation、getters和遞歸注冊子module installModule(this, state, [], this._modules.root)
function installModule (store, rootState, path, module, hot) { const isRoot = !path.length /* * { * // ... * modules: { * moduleA: { * namespaced: true * }, * moduleB: {} * } * } * moduleA的namespace -> "moduleA/" * moduleB的namespace -> "" */ const namespace = store._modules.getNamespace(path) // register in namespace map if (module.namespaced) { // 保存namespaced模塊 store._modulesNamespaceMap[namespace] = module } // set state if (!isRoot && !hot) { // 非根組件設(shè)置state // 根據(jù)path獲取父state const parentState = getNestedState(rootState, path.slice(0, -1)) // 當(dāng)前的module const moduleName = path[path.length - 1] store._withCommit(() => { // 使用Vue.set將state設(shè)置為響應(yīng)式 Vue.set(parentState, moduleName, module.state) }) console.log("end", store) } // 設(shè)置module的上下文,從而保證mutation和action的第一個參數(shù)能拿到對應(yīng)的state getter等 const local = module.context = makeLocalContext(store, namespace, path) // 逐一注冊mutation module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(store, namespacedType, mutation, local) }) // 逐一注冊action module.forEachAction((action, key) => { const type = action.root ? key : namespace + key const handler = action.handler || action registerAction(store, type, handler, local) }) // 逐一注冊getter module.forEachGetter((getter, key) => { const namespacedType = namespace + key registerGetter(store, namespacedType, getter, local) }) // 逐一注冊子module module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot) }) }
首先保存namespaced模塊到store._modulesNamespaceMap,再判斷是否為根組件且不是hot,得到父級module的state和當(dāng)前module的name,調(diào)用Vue.set(parentState, moduleName, module.state)將當(dāng)前module的state掛載到父state上。接下去會設(shè)置module的上下文,因為可能存在namespaced,需要額外處理
// 設(shè)置module的上下文,綁定對應(yīng)的dispatch、commit、getters、state function makeLocalContext (store, namespace, path) { // namespace 如"moduleA/" const noNamespace = namespace === "" const local = { // 如果沒有namespace,直接使用原來的 dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => { // 統(tǒng)一格式 因為支持payload風(fēng)格和對象風(fēng)格 const args = unifyObjectStyle(_type, _payload, _options) const { payload, options } = args let { type } = args // 如果root: true 不會加上namespace 即在命名空間模塊里提交根的 action if (!options || !options.root) { // 加上命名空間 type = namespace + type if (process.env.NODE_ENV !== "production" && !store._actions[type]) { console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`) return } } // 觸發(fā)action return store.dispatch(type, payload) }, commit: noNamespace ? store.commit : (_type, _payload, _options) => { // 統(tǒng)一格式 因為支持payload風(fēng)格和對象風(fēng)格 const args = unifyObjectStyle(_type, _payload, _options) const { payload, options } = args let { type } = args // 如果root: true 不會加上namespace 即在命名空間模塊里提交根的 mutation if (!options || !options.root) { // 加上命名空間 type = namespace + type if (process.env.NODE_ENV !== "production" && !store._mutations[type]) { console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`) return } } // 觸發(fā)mutation store.commit(type, payload, options) } } // getters and state object must be gotten lazily // because they will be changed by vm update // 這里的getters和state需要延遲處理,需要等數(shù)據(jù)更新后才進(jìn)行計算,所以使用getter函數(shù),當(dāng)訪問的時候再進(jìn)行一次計算 Object.defineProperties(local, { getters: { get: noNamespace ? () => store.getters : () => makeLocalGetters(store, namespace) // 獲取namespace下的getters }, state: { get: () => getNestedState(store.state, path) } }) return local } function makeLocalGetters (store, namespace) { const gettersProxy = {} const splitPos = namespace.length Object.keys(store.getters).forEach(type => { // 如果getter不在該命名空間下 直接return if (type.slice(0, splitPos) !== namespace) return // 去掉type上的命名空間 const localType = type.slice(splitPos) // Add a port to the getters proxy. // Define as getter property because // we do not want to evaluate the getters in this time. // 給getters加一層代理 這樣在module中獲取到的getters不會帶命名空間,實際返回的是store.getters[type] type是有命名空間的 Object.defineProperty(gettersProxy, localType, { get: () => store.getters[type], enumerable: true }) }) return gettersProxy }
這里會判斷module的namespace是否存在,不存在不會對dispatch和commit做處理,如果存在,給type加上namespace,如果聲明了{root: true}也不做處理,另外getters和state需要延遲處理,需要等數(shù)據(jù)更新后才進(jìn)行計算,所以使用Object.defineProperties的getter函數(shù),當(dāng)訪問的時候再進(jìn)行計算
再回到上面的流程,接下去是逐步注冊mutation action getter 子module,先看注冊mutation
/* * 參數(shù)是store、mutation的key(namespace處理后的)、handler函數(shù)、當(dāng)前module上下文 */ function registerMutation (store, type, handler, local) { // 首先判斷store._mutations是否存在,否則給空數(shù)組 const entry = store._mutations[type] || (store._mutations[type] = []) // 將mutation包一層函數(shù),push到數(shù)組中 entry.push(function wrappedMutationHandler (payload) { // 包一層,commit執(zhí)行時只需要傳入payload // 執(zhí)行時讓this指向store,參數(shù)為當(dāng)前module上下文的state和用戶額外添加的payload handler.call(store, local.state, payload) }) }
mutation的注冊比較簡單,主要是包一層函數(shù),然后保存到store._mutations里面,在這里也可以知道,mutation可以重復(fù)注冊,不會覆蓋,當(dāng)用戶調(diào)用this.$store.commit(mutationType, payload)時會觸發(fā),接下去看看commit函數(shù)
// 這里的this已經(jīng)被綁定為store commit (_type, _payload, _options) { // 統(tǒng)一格式,因為支持對象風(fēng)格和payload風(fēng)格 const { type, payload, options } = unifyObjectStyle(_type, _payload, _options) const mutation = { type, payload } // 獲取當(dāng)前type對應(yīng)保存下來的mutations數(shù)組 const entry = this._mutations[type] if (!entry) { // 提示不存在該mutation if (process.env.NODE_ENV !== "production") { console.error(`[vuex] unknown mutation type: ${type}`) } return } // 包裹在_withCommit中執(zhí)行mutation,mutation是修改state的唯一方法 this._withCommit(() => { entry.forEach(function commitIterator (handler) { // 執(zhí)行mutation,只需要傳入payload,在上面的包裹函數(shù)中已經(jīng)處理了其他參數(shù) handler(payload) }) }) // 執(zhí)行mutation的訂閱者 this._subscribers.forEach(sub => sub(mutation, this.state)) if ( process.env.NODE_ENV !== "production" && options && options.silent ) { // 提示silent參數(shù)已經(jīng)移除 console.warn( `[vuex] mutation type: ${type}. Silent option has been removed. ` + "Use the filter functionality in the vue-devtools" ) } }
首先對參數(shù)進(jìn)行統(tǒng)一處理,因為是支持對象風(fēng)格和載荷風(fēng)格的,然后拿到當(dāng)前type對應(yīng)的mutation數(shù)組,使用_withCommit包裹逐一執(zhí)行,這樣我們執(zhí)行this.$store.commit的時候會調(diào)用對應(yīng)的mutation,而且第一個參數(shù)是state,然后再執(zhí)行mutation的訂閱函數(shù)
接下去看action的注冊
/* * 參數(shù)是store、type(namespace處理后的)、handler函數(shù)、module上下文 */ function registerAction (store, type, handler, local) { // 獲取_actions數(shù)組,不存在即賦值為空數(shù)組 const entry = store._actions[type] || (store._actions[type] = []) // push到數(shù)組中 entry.push(function wrappedActionHandler (payload, cb) { // 包一層,執(zhí)行時需要傳入payload和cb // 執(zhí)行action let res = handler.call( store, { dispatch: local.dispatch, commit: local.commit, getters: local.getters, state: local.state, rootGetters: store.getters, rootState: store.state }, payload, cb ) // 如果action的執(zhí)行結(jié)果不是promise,將他包裹為promise,這樣就支持promise的鏈?zhǔn)秸{(diào)用 if (!isPromise(res)) { res = Promise.resolve(res) } if (store._devtoolHook) { // 使用devtool處理一次error return res.catch(err => { store._devtoolHook.emit("vuex:error", err) throw err }) } else { return res } }) }
和mutation很類似,使用函數(shù)包一層然后push到store._actions中,有些不同的是執(zhí)行時參數(shù)比較多,這也是為什么我們在寫action時可以解構(gòu)拿到commit等的原因,然后再將返回值promisify,這樣可以支持鏈?zhǔn)秸{(diào)用,但實際上用的時候最好還是自己返回promise,因為通常action是異步的,比較多見是發(fā)起ajax請求,進(jìn)行鏈?zhǔn)秸{(diào)用也是想當(dāng)異步完成后再執(zhí)行,具體根據(jù)業(yè)務(wù)需求來。接下去再看看dispatch函數(shù)的實現(xiàn)
// this已經(jīng)綁定為store dispatch (_type, _payload) { // 統(tǒng)一格式 const { type, payload } = unifyObjectStyle(_type, _payload) const action = { type, payload } // 獲取actions數(shù)組 const entry = this._actions[type] // 提示不存在action if (!entry) { if (process.env.NODE_ENV !== "production") { console.error(`[vuex] unknown action type: ${type}`) } return } // 執(zhí)行action的訂閱者 this._actionSubscribers.forEach(sub => sub(action, this.state)) // 如果action大于1,需要用Promise.all包裹 return entry.length > 1 ? Promise.all(entry.map(handler => handler(payload))) : entry[0](payload) }
這里和commit也是很類似的,對參數(shù)統(tǒng)一處理,拿到action數(shù)組,如果長度大于一,用Promise.all包裹,不過直接執(zhí)行,然后返回執(zhí)行結(jié)果。
接下去是getters的注冊和子module的注冊
/* * 參數(shù)是store、type(namesapce處理后的)、getter函數(shù)、module上下文 */ function registerGetter (store, type, rawGetter, local) { // 不允許重復(fù)定義getters if (store._wrappedGetters[type]) { if (process.env.NODE_ENV !== "production") { console.error(`[vuex] duplicate getter key: ${type}`) } return } // 包一層,保存到_wrappedGetters中 store._wrappedGetters[type] = function wrappedGetter (store) { // 執(zhí)行時傳入store,執(zhí)行對應(yīng)的getter函數(shù) return rawGetter( local.state, // local state local.getters, // local getters store.state, // root state store.getters // root getters ) } }
首先對getters進(jìn)行判斷,和mutation是不同的,這里是不允許重復(fù)定義的,然后包裹一層函數(shù),這樣在調(diào)用時只需要給上store參數(shù),而用戶的函數(shù)里會包含local.state local.getters store.state store.getters
// 遞歸注冊子module installModule(store, rootState, path.concat(key), child, hot)
接著再繼續(xù)執(zhí)行resetStoreVM(this, state),將state和getters存放到一個vue實例中,
// initialize the store vm, which is responsible for the reactivity // (also registers _wrappedGetters as computed properties) resetStoreVM(this, state)
function resetStoreVM (store, state, hot) { // 保存舊vm const oldVm = store._vm // bind store public getters store.getters = {} const wrappedGetters = store._wrappedGetters const computed = {} // 循環(huán)所有g(shù)etters,通過Object.defineProperty方法為getters對象建立屬性,這樣就可以通過this.$store.getters.xxx訪問 forEachValue(wrappedGetters, (fn, key) => { // use computed to leverage its lazy-caching mechanism // getter保存在computed中,執(zhí)行時只需要給上store參數(shù),這個在registerGetter時已經(jīng)做處理 computed[key] = () => fn(store) Object.defineProperty(store.getters, key, { get: () => store._vm[key], enumerable: true // for local getters }) }) // 使用一個vue實例來保存state和getter // silent設(shè)置為true,取消所有日志警告等 const silent = Vue.config.silent Vue.config.silent = true store._vm = new Vue({ data: { $$state: state }, computed }) // 恢復(fù)用戶的silent設(shè)置 Vue.config.silent = silent // enable strict mode for new vm // strict模式 if (store.strict) { enableStrictMode(store) } // 若存在oldVm,解除對state的引用,等dom更新后把舊的vue實例銷毀 if (oldVm) { 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()) } }
這里會重新設(shè)置一個新的vue實例,用來保存state和getter,getters保存在計算屬性中,會給getters加一層代理,這樣可以通過this.$store.getters.xxx訪問到,而且在執(zhí)行g(shù)etters時只傳入了store參數(shù),這個在上面的registerGetter已經(jīng)做了處理,也是為什么我們的getters可以拿到state getters rootState rootGetters的原因。然后根據(jù)用戶設(shè)置開啟strict模式,如果存在oldVm,解除對state的引用,等dom更新后把舊的vue實例銷毀
function enableStrictMode (store) { store._vm.$watch( function () { return this._data.$$state }, () => { if (process.env.NODE_ENV !== "production") { // 不允許在mutation之外修改state assert( store._committing, `Do not mutate vuex store state outside mutation handlers.` ) } }, { deep: true, sync: true } ) }
使用$watch來觀察state的變化,如果此時的store._committing不會true,便是在mutation之外修改state,報錯。
再次回到構(gòu)造函數(shù),接下來是各類插件的注冊
// apply plugins plugins.forEach(plugin => plugin(this)) if (Vue.config.devtools) { devtoolPlugin(this) }
到這里store的初始化工作已經(jīng)完成。大概長這個樣子
看到這里,相信已經(jīng)對store的一些實現(xiàn)細(xì)節(jié)有所了解,另外store上還存在一些api,但是用到的比較少,可以簡單看看都有些啥
watch (getter, cb, options)
用于監(jiān)聽一個getter值的變化
watch (getter, cb, options) { if (process.env.NODE_ENV !== "production") { assert( typeof getter === "function", `store.watch only accepts a function.` ) } return this._watcherVM.$watch( () => getter(this.state, this.getters), cb, options ) }
首先判斷getter必須是函數(shù)類型,使用$watch方法來監(jiān)控getter的變化,傳入state和getters作為參數(shù),當(dāng)值變化時會執(zhí)行cb回調(diào)。調(diào)用此方法返回的函數(shù)可停止偵聽。
replaceState(state)
用于修改state,主要用于devtool插件的時空穿梭功能,代碼也相當(dāng)簡單,直接修改_vm.$$state
replaceState (state) { this._withCommit(() => { this._vm._data.$$state = state }) }
registerModule (path, rawModule, options = {})
用于動態(tài)注冊module
registerModule (path, rawModule, options = {}) { if (typeof path === "string") path = [path] if (process.env.NODE_ENV !== "production") { assert(Array.isArray(path), `module path must be a string or an Array.`) assert( path.length > 0, "cannot register the root module by using registerModule." ) } this._modules.register(path, rawModule) installModule( this, this.state, path, this._modules.get(path), options.preserveState ) // reset store to update getters... resetStoreVM(this, this.state) }
首先統(tǒng)一path的格式為Array,接著是斷言,path只接受String和Array類型,且不能注冊根module,然后調(diào)用store._modules.register方法收集module,也就是上面的module-collection里面的方法。再調(diào)用installModule進(jìn)行模塊的安裝,最后調(diào)用resetStoreVM更新_vm
unregisterModule (path)
根據(jù)path注銷module
unregisterModule (path) { if (typeof path === "string") path = [path] if (process.env.NODE_ENV !== "production") { assert(Array.isArray(path), `module path must be a string or an Array.`) } this._modules.unregister(path) this._withCommit(() => { const parentState = getNestedState(this.state, path.slice(0, -1)) Vue.delete(parentState, path[path.length - 1]) }) resetStore(this) }
和registerModule一樣,首先統(tǒng)一path的格式為Array,接著是斷言,path只接受String和Array類型,接著調(diào)用store._modules.unregister方法注銷module,然后在store._withCommit中將該module的state通過Vue.delete移除,最后調(diào)用resetStore方法,需要再看看resetStore的實現(xiàn)
function resetStore (store, hot) { store._actions = Object.create(null) store._mutations = Object.create(null) store._wrappedGetters = Object.create(null) store._modulesNamespaceMap = Object.create(null) const state = store.state // init all modules installModule(store, state, [], store._modules.root, true) // reset vm resetStoreVM(store, state, hot) }
這里是將_actions _mutations _wrappedGetters _modulesNamespaceMap都清空,然后調(diào)用installModule和resetStoreVM重新進(jìn)行全部模塊安裝和_vm的設(shè)置
_withCommit (fn)
用于執(zhí)行mutation
_withCommit (fn) { const committing = this._committing this._committing = true fn() this._committing = committing }
在執(zhí)行mutation的時候,會將_committing設(shè)置為true,執(zhí)行完畢后重置,在開啟strict模式時,會監(jiān)聽state的變化,當(dāng)變化時_committing不為true時會給出警告
3. 輔助函數(shù)為了避免每次都需要通過this.$store來調(diào)用api,vuex提供了mapState mapMutations mapGetters mapActions createNamespacedHelpers 等api,接著看看各api的具體實現(xiàn),存放在src/helpers.js
下面這些工具函數(shù)是輔助函數(shù)內(nèi)部會用到的,可以先看看功能和實現(xiàn),主要做的工作是數(shù)據(jù)格式的統(tǒng)一、和通過namespace獲取module
/** * 統(tǒng)一數(shù)據(jù)格式 * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ] * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: "a", val: 1 }, { key: "b", val: 2 }, { key: "c", val: 3 } ] * @param {Array|Object} map * @return {Object} */ function normalizeMap (map) { return Array.isArray(map) ? map.map(key => ({ key, val: key })) : Object.keys(map).map(key => ({ key, val: map[key] })) } /** * 返回一個函數(shù),接受namespace和map參數(shù),判斷是否存在namespace,統(tǒng)一進(jìn)行namespace處理 * @param {Function} fn * @return {Function} */ function normalizeNamespace (fn) { return (namespace, map) => { if (typeof namespace !== "string") { map = namespace namespace = "" } else if (namespace.charAt(namespace.length - 1) !== "/") { namespace += "/" } return fn(namespace, map) } } /** * 根據(jù)namespace獲取module * @param {Object} store * @param {String} helper * @param {String} namespace * @return {Object} */ function getModuleByNamespace (store, helper, namespace) { const module = store._modulesNamespaceMap[namespace] if (process.env.NODE_ENV !== "production" && !module) { console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`) } return module }
為組件創(chuàng)建計算屬性以返回 store 中的狀態(tài)
export const mapState = normalizeNamespace((namespace, states) => { const res = {} normalizeMap(states).forEach(({ key, val }) => { // 返回一個對象,值都是函數(shù) res[key] = function mappedState () { let state = this.$store.state let getters = this.$store.getters if (namespace) { // 如果存在namespace,拿該namespace下的module const module = getModuleByNamespace(this.$store, "mapState", namespace) if (!module) { return } // 拿到當(dāng)前module的state和getters state = module.context.state getters = module.context.getters } // Object類型的val是函數(shù),傳遞過去的參數(shù)是state和getters return typeof val === "function" ? val.call(this, state, getters) : state[val] } // mark vuex getter for devtools res[key].vuex = true }) return res })
mapState是normalizeNamespace的返回值,從上面的代碼可以看到normalizeNamespace是進(jìn)行參數(shù)處理,如果存在namespace便加上命名空間,對傳入的states進(jìn)行normalizeMap處理,也就是數(shù)據(jù)格式的統(tǒng)一,然后遍歷,對參數(shù)里的所有state都包裹一層函數(shù),最后返回一個對象
大概是這么回事吧
export default { // ... computed: { ...mapState(["stateA"]) } // ... }
等價于
export default { // ... computed: { stateA () { return this.$store.stateA } } // ... }
將store 中的 getter 映射到局部計算屬性中
export const mapGetters = normalizeNamespace((namespace, getters) => { const res = {} normalizeMap(getters).forEach(({ key, val }) => { // this namespace has been mutate by normalizeNamespace val = namespace + val res[key] = function mappedGetter () { if (namespace && !getModuleByNamespace(this.$store, "mapGetters", namespace)) { return } if (process.env.NODE_ENV !== "production" && !(val in this.$store.getters)) { console.error(`[vuex] unknown getter: ${val}`) return } return this.$store.getters[val] } // mark vuex getter for devtools res[key].vuex = true }) return res })
同樣的處理方式,遍歷getters,只是這里需要加上命名空間,這是因為在注冊時_wrapGetters中的getters是有加上命名空間的
創(chuàng)建組件方法提交 mutation
export const mapMutations = normalizeNamespace((namespace, mutations) => { const res = {} normalizeMap(mutations).forEach(({ key, val }) => { // 返回一個對象,值是函數(shù) res[key] = function mappedMutation (...args) { // Get the commit method from store let commit = this.$store.commit if (namespace) { const module = getModuleByNamespace(this.$store, "mapMutations", namespace) if (!module) { return } commit = module.context.commit } // 執(zhí)行mutation, return typeof val === "function" ? val.apply(this, [commit].concat(args)) : commit.apply(this.$store, [val].concat(args)) } }) return res })
和上面都是一樣的處理方式,這里在判斷是否存在namespace后,commit是不一樣的,上面可以知道每個module都是保存了上下文的,這里如果存在namespace就需要使用那個另外處理的commit等信息,另外需要注意的是,這里不需要加上namespace,這是因為在module.context.commit中會進(jìn)行處理,忘記的可以往上翻,看makeLocalContext對commit的處理
創(chuàng)建組件方法分發(fā) action
export const mapActions = normalizeNamespace((namespace, actions) => { const res = {} normalizeMap(actions).forEach(({ key, val }) => { res[key] = function mappedAction (...args) { // get dispatch function from store let dispatch = this.$store.dispatch if (namespace) { const module = getModuleByNamespace(this.$store, "mapActions", namespace) if (!module) { return } dispatch = module.context.dispatch } return typeof val === "function" ? val.apply(this, [dispatch].concat(args)) : dispatch.apply(this.$store, [val].concat(args)) } }) return res })
和mapMutations基本一樣的處理方式
4. 插件Vuex中可以傳入plguins選項來安裝各種插件,這些插件都是函數(shù),接受store作為參數(shù),Vuex中內(nèi)置了devtool和logger兩個插件,
// 插件注冊,所有插件都是一個函數(shù),接受store作為參數(shù) plugins.forEach(plugin => plugin(this)) // 如果開啟devtools,注冊devtool if (Vue.config.devtools) { devtoolPlugin(this) }
// devtools.js const devtoolHook = typeof window !== "undefined" && window.__VUE_DEVTOOLS_GLOBAL_HOOK__ export default function devtoolPlugin (store) { if (!devtoolHook) return store._devtoolHook = devtoolHook // 觸發(fā)vuex:init devtoolHook.emit("vuex:init", store) // 時空穿梭功能 devtoolHook.on("vuex:travel-to-state", targetState => { store.replaceState(targetState) }) // 訂閱mutation,當(dāng)觸發(fā)mutation時觸發(fā)vuex:mutation方法,傳入mutation和state store.subscribe((mutation, state) => { devtoolHook.emit("vuex:mutation", mutation, state) }) }總結(jié)
到這里基本vuex的流程源碼已經(jīng)分析完畢,分享下自己看源碼的思路或者過程,在看之前先把官網(wǎng)的文檔再仔細(xì)過一遍,然后帶著問題來看源碼,這樣效率會比較高,利用chrome在關(guān)鍵點打開debugger,一步一步執(zhí)行,看源碼的執(zhí)行過程,數(shù)據(jù)狀態(tài)的變換。而且可以暫時屏蔽一些沒有副作用的代碼,比如assert,這些函數(shù)一般都不會影響流程的理解,這樣也可以盡量減少源碼行數(shù)。剩下的就是耐心了,前前后后斷斷續(xù)續(xù)看了很多次,總算也把這份分享寫完了,由于水平關(guān)系,一些地方可能理解錯誤或者不到位,歡迎指出。
原文地址
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/94211.html
摘要:繼續(xù)看后面的首先遍歷的,通過和首先獲取然后調(diào)用方法,我們來看看是不是感覺這個函數(shù)有些熟悉,表示當(dāng)前實例,表示類型,表示執(zhí)行的回調(diào)函數(shù),表示本地化后的一個變量。必須是一個函數(shù),如果返回的值發(fā)生了變化,那么就調(diào)用回調(diào)函數(shù)。 這幾天忙啊,有絕地求生要上分,英雄聯(lián)盟新賽季需要上分,就懶著什么也沒寫,很慚愧。這個vuex,vue-router,vue的源碼我半個月前就看的差不多了,但是懶,哈哈。...
摘要:繼續(xù)看后面的首先遍歷的,通過和首先獲取然后調(diào)用方法,我們來看看是不是感覺這個函數(shù)有些熟悉,表示當(dāng)前實例,表示類型,表示執(zhí)行的回調(diào)函數(shù),表示本地化后的一個變量。必須是一個函數(shù),如果返回的值發(fā)生了變化,那么就調(diào)用回調(diào)函數(shù)。 這幾天忙啊,有絕地求生要上分,英雄聯(lián)盟新賽季需要上分,就懶著什么也沒寫,很慚愧。這個vuex,vue-router,vue的源碼我半個月前就看的差不多了,但是懶,哈哈。...
摘要:而鉆研最好的方式,就是閱讀的源代碼。整個的源代碼,核心內(nèi)容包括兩部分。逃而動手腳的代碼,就存在于源代碼的中。整個源代碼讀下來一遍,雖然有些部分不太理解,但是對和一些代碼的使用的理解又加深了一步。 筆記中的Vue與Vuex版本為1.0.21和0.6.2,需要閱讀者有使用Vue,Vuex,ES6的經(jīng)驗。 起因 俗話說得好,沒有無緣無故的愛,也沒有無緣無故的恨,更不會無緣無故的去閱讀別人的源...
摘要:哪吒別人的看法都是狗屁,你是誰只有你自己說了才算,這是爹教我的道理。哪吒去他個鳥命我命由我,不由天是魔是仙,我自己決定哪吒白白搭上一條人命,你傻不傻敖丙不傻誰和你做朋友太乙真人人是否能夠改變命運,我不曉得。我只曉得,不認(rèn)命是哪吒的命。 showImg(https://segmentfault.com/img/bVbwiGL?w=900&h=378); 出處 查看github最新的Vue...
摘要:為了防止某些文檔或腳本加載別的域下的未知內(nèi)容,防止造成泄露隱私,破壞系統(tǒng)等行為發(fā)生。模式構(gòu)建函數(shù)響應(yīng)式前端架構(gòu)過程中學(xué)到的經(jīng)驗?zāi)J降牟煌幵谟冢饕獙W⒂谇‘?dāng)?shù)貙崿F(xiàn)應(yīng)用程序狀態(tài)突變。嚴(yán)重情況下,會造成惡意的流量劫持等問題。 今天是編輯周刊的日子。所以文章很多和周刊一樣。微信不能發(fā)鏈接,點了也木有用,所以請記得閱讀原文~ 發(fā)個動圖娛樂下: 使用 SVG 動畫制作游戲 使用 GASP ...
閱讀 3152·2021-09-30 09:47
閱讀 2003·2021-09-22 16:04
閱讀 2274·2021-09-22 15:44
閱讀 2534·2021-08-25 09:38
閱讀 540·2019-08-26 13:23
閱讀 1221·2019-08-26 12:20
閱讀 2808·2019-08-26 11:59
閱讀 1077·2019-08-23 18:40