store.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. import applyMixin from './mixin'
  2. import devtoolPlugin from './plugins/devtool'
  3. import ModuleCollection from './module/module-collection'
  4. import { forEachValue, isObject, isPromise, assert } from './util'
  5. let Vue // bind on install
  6. export class Store {
  7. constructor (options = {}) {
  8. assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
  9. assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
  10. const {
  11. state = {},
  12. plugins = [],
  13. strict = false
  14. } = options
  15. // store internal state
  16. this._committing = false
  17. this._actions = Object.create(null)
  18. this._mutations = Object.create(null)
  19. this._wrappedGetters = Object.create(null)
  20. this._modules = new ModuleCollection(options)
  21. this._modulesNamespaceMap = Object.create(null)
  22. this._subscribers = []
  23. this._watcherVM = new Vue()
  24. // bind commit and dispatch to self
  25. const store = this
  26. const { dispatch, commit } = this
  27. this.dispatch = function boundDispatch (type, payload) {
  28. return dispatch.call(store, type, payload)
  29. }
  30. this.commit = function boundCommit (type, payload, options) {
  31. return commit.call(store, type, payload, options)
  32. }
  33. // strict mode
  34. this.strict = strict
  35. // init root module.
  36. // this also recursively registers all sub-modules
  37. // and collects all module getters inside this._wrappedGetters
  38. installModule(this, state, [], this._modules.root)
  39. // initialize the store vm, which is responsible for the reactivity
  40. // (also registers _wrappedGetters as computed properties)
  41. resetStoreVM(this, state)
  42. // apply plugins
  43. plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
  44. }
  45. get state () {
  46. return this._vm._data.$$state
  47. }
  48. set state (v) {
  49. assert(false, `Use store.replaceState() to explicit replace store state.`)
  50. }
  51. commit (_type, _payload, _options) {
  52. // check object-style commit
  53. const {
  54. type,
  55. payload,
  56. options
  57. } = unifyObjectStyle(_type, _payload, _options)
  58. const mutation = { type, payload }
  59. const entry = this._mutations[type]
  60. if (!entry) {
  61. console.error(`[vuex] unknown mutation type: ${type}`)
  62. return
  63. }
  64. this._withCommit(() => {
  65. entry.forEach(function commitIterator (handler) {
  66. handler(payload)
  67. })
  68. })
  69. this._subscribers.forEach(sub => sub(mutation, this.state))
  70. if (options && options.silent) {
  71. console.warn(
  72. `[vuex] mutation type: ${type}. Silent option has been removed. ` +
  73. 'Use the filter functionality in the vue-devtools'
  74. )
  75. }
  76. }
  77. dispatch (_type, _payload) {
  78. // check object-style dispatch
  79. const {
  80. type,
  81. payload
  82. } = unifyObjectStyle(_type, _payload)
  83. const entry = this._actions[type]
  84. if (!entry) {
  85. console.error(`[vuex] unknown action type: ${type}`)
  86. return
  87. }
  88. return entry.length > 1
  89. ? Promise.all(entry.map(handler => handler(payload)))
  90. : entry[0](payload)
  91. }
  92. subscribe (fn) {
  93. const subs = this._subscribers
  94. if (subs.indexOf(fn) < 0) {
  95. subs.push(fn)
  96. }
  97. return () => {
  98. const i = subs.indexOf(fn)
  99. if (i > -1) {
  100. subs.splice(i, 1)
  101. }
  102. }
  103. }
  104. watch (getter, cb, options) {
  105. assert(typeof getter === 'function', `store.watch only accepts a function.`)
  106. return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
  107. }
  108. replaceState (state) {
  109. this._withCommit(() => {
  110. this._vm._data.$$state = state
  111. })
  112. }
  113. registerModule (path, rawModule) {
  114. if (typeof path === 'string') path = [path]
  115. assert(Array.isArray(path), `module path must be a string or an Array.`)
  116. this._modules.register(path, rawModule)
  117. installModule(this, this.state, path, this._modules.get(path))
  118. // reset store to update getters...
  119. resetStoreVM(this, this.state)
  120. }
  121. unregisterModule (path) {
  122. if (typeof path === 'string') path = [path]
  123. assert(Array.isArray(path), `module path must be a string or an Array.`)
  124. this._modules.unregister(path)
  125. this._withCommit(() => {
  126. const parentState = getNestedState(this.state, path.slice(0, -1))
  127. Vue.delete(parentState, path[path.length - 1])
  128. })
  129. resetStore(this)
  130. }
  131. hotUpdate (newOptions) {
  132. this._modules.update(newOptions)
  133. resetStore(this, true)
  134. }
  135. _withCommit (fn) {
  136. const committing = this._committing
  137. this._committing = true
  138. fn()
  139. this._committing = committing
  140. }
  141. }
  142. function resetStore (store, hot) {
  143. store._actions = Object.create(null)
  144. store._mutations = Object.create(null)
  145. store._wrappedGetters = Object.create(null)
  146. store._modulesNamespaceMap = Object.create(null)
  147. const state = store.state
  148. // init all modules
  149. installModule(store, state, [], store._modules.root, true)
  150. // reset vm
  151. resetStoreVM(store, state, hot)
  152. }
  153. function resetStoreVM (store, state, hot) {
  154. const oldVm = store._vm
  155. // bind store public getters
  156. store.getters = {}
  157. const wrappedGetters = store._wrappedGetters
  158. const computed = {}
  159. forEachValue(wrappedGetters, (fn, key) => {
  160. // use computed to leverage its lazy-caching mechanism
  161. computed[key] = () => fn(store)
  162. Object.defineProperty(store.getters, key, {
  163. get: () => store._vm[key],
  164. enumerable: true // for local getters
  165. })
  166. })
  167. // use a Vue instance to store the state tree
  168. // suppress warnings just in case the user has added
  169. // some funky global mixins
  170. const silent = Vue.config.silent
  171. Vue.config.silent = true
  172. store._vm = new Vue({
  173. data: {
  174. $$state: state
  175. },
  176. computed
  177. })
  178. Vue.config.silent = silent
  179. // enable strict mode for new vm
  180. if (store.strict) {
  181. enableStrictMode(store)
  182. }
  183. if (oldVm) {
  184. if (hot) {
  185. // dispatch changes in all subscribed watchers
  186. // to force getter re-evaluation for hot reloading.
  187. store._withCommit(() => {
  188. oldVm._data.$$state = null
  189. })
  190. }
  191. Vue.nextTick(() => oldVm.$destroy())
  192. }
  193. }
  194. function installModule (store, rootState, path, module, hot) {
  195. const isRoot = !path.length
  196. const namespace = store._modules.getNamespace(path)
  197. // register in namespace map
  198. if (namespace) {
  199. store._modulesNamespaceMap[namespace] = module
  200. }
  201. // set state
  202. if (!isRoot && !hot) {
  203. const parentState = getNestedState(rootState, path.slice(0, -1))
  204. const moduleName = path[path.length - 1]
  205. store._withCommit(() => {
  206. Vue.set(parentState, moduleName, module.state)
  207. })
  208. }
  209. const local = module.context = makeLocalContext(store, namespace, path)
  210. module.forEachMutation((mutation, key) => {
  211. const namespacedType = namespace + key
  212. registerMutation(store, namespacedType, mutation, local)
  213. })
  214. module.forEachAction((action, key) => {
  215. const namespacedType = namespace + key
  216. registerAction(store, namespacedType, action, local)
  217. })
  218. module.forEachGetter((getter, key) => {
  219. const namespacedType = namespace + key
  220. registerGetter(store, namespacedType, getter, local)
  221. })
  222. module.forEachChild((child, key) => {
  223. installModule(store, rootState, path.concat(key), child, hot)
  224. })
  225. }
  226. /**
  227. * make localized dispatch, commit, getters and state
  228. * if there is no namespace, just use root ones
  229. */
  230. function makeLocalContext (store, namespace, path) {
  231. const noNamespace = namespace === ''
  232. const local = {
  233. dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
  234. const args = unifyObjectStyle(_type, _payload, _options)
  235. const { payload, options } = args
  236. let { type } = args
  237. if (!options || !options.root) {
  238. type = namespace + type
  239. if (!store._actions[type]) {
  240. console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
  241. return
  242. }
  243. }
  244. return store.dispatch(type, payload)
  245. },
  246. commit: noNamespace ? store.commit : (_type, _payload, _options) => {
  247. const args = unifyObjectStyle(_type, _payload, _options)
  248. const { payload, options } = args
  249. let { type } = args
  250. if (!options || !options.root) {
  251. type = namespace + type
  252. if (!store._mutations[type]) {
  253. console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
  254. return
  255. }
  256. }
  257. store.commit(type, payload, options)
  258. }
  259. }
  260. // getters and state object must be gotten lazily
  261. // because they will be changed by vm update
  262. Object.defineProperties(local, {
  263. getters: {
  264. get: noNamespace
  265. ? () => store.getters
  266. : () => makeLocalGetters(store, namespace)
  267. },
  268. state: {
  269. get: () => getNestedState(store.state, path)
  270. }
  271. })
  272. return local
  273. }
  274. function makeLocalGetters (store, namespace) {
  275. const gettersProxy = {}
  276. const splitPos = namespace.length
  277. Object.keys(store.getters).forEach(type => {
  278. // skip if the target getter is not match this namespace
  279. if (type.slice(0, splitPos) !== namespace) return
  280. // extract local getter type
  281. const localType = type.slice(splitPos)
  282. // Add a port to the getters proxy.
  283. // Define as getter property because
  284. // we do not want to evaluate the getters in this time.
  285. Object.defineProperty(gettersProxy, localType, {
  286. get: () => store.getters[type],
  287. enumerable: true
  288. })
  289. })
  290. return gettersProxy
  291. }
  292. function registerMutation (store, type, handler, local) {
  293. const entry = store._mutations[type] || (store._mutations[type] = [])
  294. entry.push(function wrappedMutationHandler (payload) {
  295. handler(local.state, payload)
  296. })
  297. }
  298. function registerAction (store, type, handler, local) {
  299. const entry = store._actions[type] || (store._actions[type] = [])
  300. entry.push(function wrappedActionHandler (payload, cb) {
  301. let res = handler({
  302. dispatch: local.dispatch,
  303. commit: local.commit,
  304. getters: local.getters,
  305. state: local.state,
  306. rootGetters: store.getters,
  307. rootState: store.state
  308. }, payload, cb)
  309. if (!isPromise(res)) {
  310. res = Promise.resolve(res)
  311. }
  312. if (store._devtoolHook) {
  313. return res.catch(err => {
  314. store._devtoolHook.emit('vuex:error', err)
  315. throw err
  316. })
  317. } else {
  318. return res
  319. }
  320. })
  321. }
  322. function registerGetter (store, type, rawGetter, local) {
  323. if (store._wrappedGetters[type]) {
  324. console.error(`[vuex] duplicate getter key: ${type}`)
  325. return
  326. }
  327. store._wrappedGetters[type] = function wrappedGetter (store) {
  328. return rawGetter(
  329. local.state, // local state
  330. local.getters, // local getters
  331. store.state, // root state
  332. store.getters // root getters
  333. )
  334. }
  335. }
  336. function enableStrictMode (store) {
  337. store._vm.$watch(function () { return this._data.$$state }, () => {
  338. assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
  339. }, { deep: true, sync: true })
  340. }
  341. function getNestedState (state, path) {
  342. return path.length
  343. ? path.reduce((state, key) => state[key], state)
  344. : state
  345. }
  346. function unifyObjectStyle (type, payload, options) {
  347. if (isObject(type) && type.type) {
  348. options = payload
  349. payload = type
  350. type = type.type
  351. }
  352. assert(typeof type === 'string', `Expects string as the type, but found ${typeof type}.`)
  353. return { type, payload, options }
  354. }
  355. export function install (_Vue) {
  356. if (Vue) {
  357. console.error(
  358. '[vuex] already installed. Vue.use(Vuex) should be called only once.'
  359. )
  360. return
  361. }
  362. Vue = _Vue
  363. applyMixin(Vue)
  364. }
  365. // auto install in dist mode
  366. if (typeof window !== 'undefined' && window.Vue) {
  367. install(window.Vue)
  368. }