|
@@ -1,75 +1,63 @@
|
|
|
-import {
|
|
|
- mergeObjects, isObject,
|
|
|
- getNestedState, getWatcher
|
|
|
-} from './util'
|
|
|
import devtoolPlugin from './plugins/devtool'
|
|
|
-import override from './override'
|
|
|
+import strictPlugin from './plugins/strict'
|
|
|
+import applyMixin from './mixin'
|
|
|
|
|
|
-let Vue
|
|
|
-let uid = 0
|
|
|
+let Vue // bind on install
|
|
|
|
|
|
class Store {
|
|
|
+ constructor (options = {}) {
|
|
|
+ if (!Vue) {
|
|
|
+ throw new Error(
|
|
|
+ '[vuex] must call Vue.use(Vuex) before creating a store instance.'
|
|
|
+ )
|
|
|
+ }
|
|
|
|
|
|
- /**
|
|
|
- * @param {Object} options
|
|
|
- * - {Object} state
|
|
|
- * - {Object} actions
|
|
|
- * - {Object} mutations
|
|
|
- * - {Array} plugins
|
|
|
- * - {Boolean} strict
|
|
|
- */
|
|
|
+ if (typeof Promise === 'undefined') {
|
|
|
+ throw new Error(
|
|
|
+ '[vuex] vuex requires a Promise polyfill in this browser.'
|
|
|
+ )
|
|
|
+ }
|
|
|
|
|
|
- constructor ({
|
|
|
- state = {},
|
|
|
- mutations = {},
|
|
|
- modules = {},
|
|
|
- plugins = [],
|
|
|
- strict = false
|
|
|
- } = {}) {
|
|
|
- this._getterCacheId = 'vuex_store_' + uid++
|
|
|
+ const {
|
|
|
+ state = {},
|
|
|
+ modules = {},
|
|
|
+ plugins = [],
|
|
|
+ getters = {},
|
|
|
+ strict = false
|
|
|
+ } = options
|
|
|
+
|
|
|
+ // store internal state
|
|
|
+ this._options = options
|
|
|
this._dispatching = false
|
|
|
- this._rootMutations = this._mutations = mutations
|
|
|
- this._modules = modules
|
|
|
this._events = Object.create(null)
|
|
|
- // bind dispatch to self
|
|
|
- const dispatch = this.dispatch
|
|
|
- this.dispatch = (...args) => {
|
|
|
- dispatch.apply(this, args)
|
|
|
- }
|
|
|
+ this._actions = Object.create(null)
|
|
|
+ this._mutations = Object.create(null)
|
|
|
+ this._subscribers = []
|
|
|
+
|
|
|
+ // bind dispatch and call to self
|
|
|
+ this.call = bind(this.call, this)
|
|
|
+ this.dispatch = bind(this.dispatch, this)
|
|
|
+
|
|
|
// use a Vue instance to store the state tree
|
|
|
// suppress warnings just in case the user has added
|
|
|
// some funky global mixins
|
|
|
- if (!Vue) {
|
|
|
- throw new Error(
|
|
|
- '[vuex] must call Vue.use(Vuex) before creating a store instance.'
|
|
|
- )
|
|
|
- }
|
|
|
const silent = Vue.config.silent
|
|
|
Vue.config.silent = true
|
|
|
- this._vm = new Vue({
|
|
|
- data: {
|
|
|
- state
|
|
|
- }
|
|
|
- })
|
|
|
+ this._vm = new Vue({ data: { state }})
|
|
|
Vue.config.silent = silent
|
|
|
- this._setupModuleState(state, modules)
|
|
|
- this._setupModuleMutations(modules)
|
|
|
- // add extra warnings in strict mode
|
|
|
- if (strict) {
|
|
|
- this._setupMutationCheck()
|
|
|
- }
|
|
|
+
|
|
|
+ // apply root module
|
|
|
+ this.module([], options)
|
|
|
+
|
|
|
// apply plugins
|
|
|
- devtoolPlugin(this)
|
|
|
+ plugins = plugins.concat(
|
|
|
+ strict
|
|
|
+ ? [devtoolPlugin, strictPlugin]
|
|
|
+ : [devtoolPlugin]
|
|
|
+ )
|
|
|
plugins.forEach(plugin => plugin(this))
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * Getter for the entire state tree.
|
|
|
- * Read only.
|
|
|
- *
|
|
|
- * @return {Object}
|
|
|
- */
|
|
|
-
|
|
|
get state () {
|
|
|
return this._vm.state
|
|
|
}
|
|
@@ -78,192 +66,157 @@ class Store {
|
|
|
throw new Error('[vuex] Use store.replaceState() to explicit replace store state.')
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * Replace root state.
|
|
|
- *
|
|
|
- * @param {Object} state
|
|
|
- */
|
|
|
-
|
|
|
replaceState (state) {
|
|
|
this._dispatching = true
|
|
|
this._vm.state = state
|
|
|
this._dispatching = false
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * Dispatch an action.
|
|
|
- *
|
|
|
- * @param {String} type
|
|
|
- */
|
|
|
-
|
|
|
- dispatch (type, ...payload) {
|
|
|
- let silent = false
|
|
|
- let isObjectStyleDispatch = false
|
|
|
- // compatibility for object actions, e.g. FSA
|
|
|
- if (typeof type === 'object' && type.type && arguments.length === 1) {
|
|
|
- isObjectStyleDispatch = true
|
|
|
- payload = type
|
|
|
- if (type.silent) silent = true
|
|
|
- type = type.type
|
|
|
- }
|
|
|
- const handler = this._mutations[type]
|
|
|
- const state = this.state
|
|
|
- if (handler) {
|
|
|
- this._dispatching = true
|
|
|
- // apply the mutation
|
|
|
- if (Array.isArray(handler)) {
|
|
|
- handler.forEach(h => {
|
|
|
- isObjectStyleDispatch
|
|
|
- ? h(state, payload)
|
|
|
- : h(state, ...payload)
|
|
|
- })
|
|
|
- } else {
|
|
|
- isObjectStyleDispatch
|
|
|
- ? handler(state, payload)
|
|
|
- : handler(state, ...payload)
|
|
|
- }
|
|
|
- this._dispatching = false
|
|
|
- if (!silent) {
|
|
|
- const mutation = isObjectStyleDispatch
|
|
|
- ? payload
|
|
|
- : { type, payload }
|
|
|
- this.emit('mutation', mutation, state)
|
|
|
- }
|
|
|
- } else {
|
|
|
- console.warn(`[vuex] Unknown mutation: ${type}`)
|
|
|
+ module (path, module, hot) {
|
|
|
+ if (typeof path === 'string') path = [path]
|
|
|
+ if (!Array.isArray(path)) {
|
|
|
+ throw new Error('[vuex] module path must be a string or an Array.')
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- /**
|
|
|
- * Watch state changes on the store.
|
|
|
- * Same API as Vue's $watch, except when watching a function,
|
|
|
- * the function gets the state as the first argument.
|
|
|
- *
|
|
|
- * @param {Function} fn
|
|
|
- * @param {Function} cb
|
|
|
- * @param {Object} [options]
|
|
|
- */
|
|
|
+ const isRoot = !path.length
|
|
|
+ const {
|
|
|
+ state,
|
|
|
+ actions,
|
|
|
+ mutations,
|
|
|
+ modules
|
|
|
+ } = module
|
|
|
+
|
|
|
+ // set state
|
|
|
+ if (!isRoot && !hot) {
|
|
|
+ const parentState = get(this.state, path.slice(-1))
|
|
|
+ const moduleName = path[path.length - 1]
|
|
|
+ Vue.set(parentState, moduleName, state || {})
|
|
|
+ }
|
|
|
|
|
|
- watch (fn, cb, options) {
|
|
|
- if (typeof fn !== 'function') {
|
|
|
- console.error('Vuex store.watch only accepts function.')
|
|
|
- return
|
|
|
+ if (mutations) {
|
|
|
+ Object.keys(mutations).forEach(key => {
|
|
|
+ this.mutation(key, mutations[key], path)
|
|
|
+ })
|
|
|
}
|
|
|
- return this._vm.$watch(() => fn(this.state), cb, options)
|
|
|
- }
|
|
|
|
|
|
- /**
|
|
|
- * Hot update mutations & modules.
|
|
|
- *
|
|
|
- * @param {Object} options
|
|
|
- * - {Object} [mutations]
|
|
|
- * - {Object} [modules]
|
|
|
- */
|
|
|
+ if (actions) {
|
|
|
+ Object.keys(actions).forEach(key => {
|
|
|
+ this.action(key, actions[key], path)
|
|
|
+ })
|
|
|
+ }
|
|
|
|
|
|
- hotUpdate ({ mutations, modules } = {}) {
|
|
|
- this._rootMutations = this._mutations = mutations || this._rootMutations
|
|
|
- this._setupModuleMutations(modules || this._modules)
|
|
|
+ if (modules) {
|
|
|
+ Object.keys(modules).forEach(key => {
|
|
|
+ this.module(path.concat(key), modules[key], hot)
|
|
|
+ })
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * Attach sub state tree of each module to the root tree.
|
|
|
- *
|
|
|
- * @param {Object} state
|
|
|
- * @param {Object} modules
|
|
|
- */
|
|
|
-
|
|
|
- _setupModuleState (state, modules) {
|
|
|
- if (!isObject(modules)) return
|
|
|
-
|
|
|
- Object.keys(modules).forEach(key => {
|
|
|
- const module = modules[key]
|
|
|
-
|
|
|
- // set this module's state
|
|
|
- Vue.set(state, key, module.state || {})
|
|
|
-
|
|
|
- // retrieve nested modules
|
|
|
- this._setupModuleState(state[key], module.modules)
|
|
|
+ mutation (type, handler, path = []) {
|
|
|
+ const entry = this._mutations[type] || (this._mutations[type] = [])
|
|
|
+ entry.push(payload => {
|
|
|
+ handler(getNestedState(this.state, path), payload)
|
|
|
})
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Bind mutations for each module to its sub tree and
|
|
|
- * merge them all into one final mutations map.
|
|
|
- *
|
|
|
- * @param {Object} updatedModules
|
|
|
- */
|
|
|
-
|
|
|
- _setupModuleMutations (updatedModules) {
|
|
|
- const modules = this._modules
|
|
|
- Object.keys(updatedModules).forEach(key => {
|
|
|
- modules[key] = updatedModules[key]
|
|
|
+ },
|
|
|
+
|
|
|
+ action (type, handler, path = []) {
|
|
|
+ const entry = this._actions[type] || (this._actions[type] = [])
|
|
|
+ entry.push((payload, cb) => {
|
|
|
+ let res = handler({
|
|
|
+ call: this.call,
|
|
|
+ dispatch: this.dispatch,
|
|
|
+ state: getNestedState(this.state, path)
|
|
|
+ }, payload, cb)
|
|
|
+ if (!isPromise(res)) {
|
|
|
+ res = Promise.resolve(res)
|
|
|
+ }
|
|
|
+ return res.catch(err => {
|
|
|
+ console.warn(`[vuex] error in Promise returned from action ${type}`)
|
|
|
+ console.warn(err)
|
|
|
+ })
|
|
|
})
|
|
|
- const updatedMutations = this._createModuleMutations(modules, [])
|
|
|
- this._mutations = mergeObjects([this._rootMutations, ...updatedMutations])
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * Helper method for _setupModuleMutations.
|
|
|
- * The method retrieve nested sub modules and
|
|
|
- * bind each mutations to its sub tree recursively.
|
|
|
- *
|
|
|
- * @param {Object} modules
|
|
|
- * @param {Array<String>} nestedKeys
|
|
|
- * @return {Array<Object>}
|
|
|
- */
|
|
|
-
|
|
|
- _createModuleMutations (modules, nestedKeys) {
|
|
|
- if (!isObject(modules)) return []
|
|
|
+ dispatch (type, payload) {
|
|
|
+ const entry = this._mutations[type]
|
|
|
+ if (!entry) {
|
|
|
+ console.warn(`[vuex] unknown mutation type: ${type}`)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // check object-style dispatch
|
|
|
+ let mutation
|
|
|
+ if (isObject(type)) {
|
|
|
+ payload = mutation = type
|
|
|
+ } else {
|
|
|
+ mutation = { type, payload }
|
|
|
+ }
|
|
|
+ this._dispatching = true
|
|
|
+ entry.forEach(handler => handler(payload))
|
|
|
+ this._dispatching = false
|
|
|
+ this._subscribers.forEach(sub => sub(mutation, state))
|
|
|
+ }
|
|
|
|
|
|
- return Object.keys(modules).map(key => {
|
|
|
- const module = modules[key]
|
|
|
- const newNestedKeys = nestedKeys.concat(key)
|
|
|
+ call (type, payload, cb) {
|
|
|
+ const entry = this._actions[type]
|
|
|
+ if (!entry) {
|
|
|
+ console.warn(`[vuex] unknown action type: ${type}`)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (typeof payload === 'function') {
|
|
|
+ cb = payload
|
|
|
+ payload = undefined
|
|
|
+ }
|
|
|
+ return entry.length > 1
|
|
|
+ ? Promise.all(entry.map(handler => handler(payload)))
|
|
|
+ : entry[0](payload)
|
|
|
+ }
|
|
|
|
|
|
- // retrieve nested modules
|
|
|
- const nestedMutations = this._createModuleMutations(module.modules, newNestedKeys)
|
|
|
+ subscribe (fn) {
|
|
|
+ const subs = this._subscribers
|
|
|
+ if (subs.indexOf(fn) < 0) {
|
|
|
+ subs.push(fn)
|
|
|
+ }
|
|
|
+ return () => {
|
|
|
+ let i = subs.indexOf(fn)
|
|
|
+ if (i > -1) {
|
|
|
+ subs.splice(i, 1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if (!module || !module.mutations) {
|
|
|
- return mergeObjects(nestedMutations)
|
|
|
+ update (newOptions) {
|
|
|
+ this._actions = Object.create(null)
|
|
|
+ this._mutations = Object.create(null)
|
|
|
+ const options = this._options
|
|
|
+ if (newOptions.actions) {
|
|
|
+ options.actions = newOptions.actions
|
|
|
+ }
|
|
|
+ if (newOptions.mutations) {
|
|
|
+ options.mutations = newOptions.mutations
|
|
|
+ }
|
|
|
+ if (newOptions.modules) {
|
|
|
+ for (const key in newOptions.modules) {
|
|
|
+ options.modules[key] = newOptions.modules[key]
|
|
|
}
|
|
|
+ }
|
|
|
+ this.module([], options, true)
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- // bind mutations to sub state tree
|
|
|
- const mutations = {}
|
|
|
- Object.keys(module.mutations).forEach(name => {
|
|
|
- const original = module.mutations[name]
|
|
|
- mutations[name] = (state, ...args) => {
|
|
|
- original(getNestedState(state, newNestedKeys), ...args)
|
|
|
- }
|
|
|
- })
|
|
|
+function bind (fn, ctx) {
|
|
|
+ return () => fn.apply(ctx, arguments)
|
|
|
+}
|
|
|
|
|
|
- // merge mutations of this module and nested modules
|
|
|
- return mergeObjects([
|
|
|
- mutations,
|
|
|
- ...nestedMutations
|
|
|
- ])
|
|
|
- })
|
|
|
- }
|
|
|
+function isObject (obj) {
|
|
|
+ return obj !== null && typeof obj === 'object'
|
|
|
+}
|
|
|
|
|
|
- /**
|
|
|
- * Setup mutation check: if the vuex instance's state is mutated
|
|
|
- * outside of a mutation handler, we throw en error. This effectively
|
|
|
- * enforces all mutations to the state to be trackable and hot-reloadble.
|
|
|
- * However, this comes at a run time cost since we are doing a deep
|
|
|
- * watch on the entire state tree, so it is only enalbed with the
|
|
|
- * strict option is set to true.
|
|
|
- */
|
|
|
+function isPromise (val) {
|
|
|
+ return val && typeof val.then === 'function'
|
|
|
+}
|
|
|
|
|
|
- _setupMutationCheck () {
|
|
|
- const Watcher = getWatcher(this._vm)
|
|
|
- /* eslint-disable no-new */
|
|
|
- new Watcher(this._vm, 'state', () => {
|
|
|
- if (!this._dispatching) {
|
|
|
- throw new Error(
|
|
|
- '[vuex] Do not mutate vuex store state outside mutation handlers.'
|
|
|
- )
|
|
|
- }
|
|
|
- }, { deep: true, sync: true })
|
|
|
- /* eslint-enable no-new */
|
|
|
- }
|
|
|
+function getNestedState (state, path) {
|
|
|
+ return path.reduce((state, key) => state[key], state)
|
|
|
}
|
|
|
|
|
|
function install (_Vue) {
|
|
@@ -274,11 +227,7 @@ function install (_Vue) {
|
|
|
return
|
|
|
}
|
|
|
Vue = _Vue
|
|
|
- // reuse Vue's event system
|
|
|
- ;['on', 'off', 'once', 'emit'].forEach(e => {
|
|
|
- Store.prototype[e] = Store.prototype['$' + e] = Vue.prototype['$' + e]
|
|
|
- })
|
|
|
- override(Vue)
|
|
|
+ applyMixin(Vue)
|
|
|
}
|
|
|
|
|
|
// auto install in dist mode
|