Evan You 9 gadi atpakaļ
vecāks
revīzija
1b2845c4d0
7 mainītis faili ar 211 papildinājumiem un 510 dzēšanām
  1. 168 219
      src/index.js
  2. 32 0
      src/mixin.js
  3. 0 166
      src/override.js
  4. 1 1
      src/plugins/devtool.js
  5. 0 52
      src/plugins/logger.js
  6. 10 0
      src/plugins/strict.js
  7. 0 72
      src/util.js

+ 168 - 219
src/index.js

@@ -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

+ 32 - 0
src/mixin.js

@@ -0,0 +1,32 @@
+export default function (Vue) {
+  const version = Number(Vue.version.split('.')[0])
+
+  if (version >= 2) {
+    const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
+    Vue.mixin(usesInit ? { init: vuexInit } : { 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
+    if (options.store) {
+      this.$store = options.store
+    } else if (options.parent && options.parent.$store) {
+      this.$store = options.parent.$store
+    }
+  }
+}

+ 0 - 166
src/override.js

@@ -1,166 +0,0 @@
-import { getWatcher, getDep } from './util'
-
-export default function (Vue) {
-  const version = Number(Vue.version.split('.')[0])
-
-  if (version >= 2) {
-    const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
-    Vue.mixin(usesInit ? { init: vuexInit } : { 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
-    const { store, vuex } = options
-    // store injection
-    if (store) {
-      this.$store = store
-    } else if (options.parent && options.parent.$store) {
-      this.$store = options.parent.$store
-    }
-    // vuex option handling
-    if (vuex) {
-      if (!this.$store) {
-        console.warn(
-          '[vuex] store not injected. make sure to ' +
-          'provide the store option in your root component.'
-        )
-      }
-      const { state, actions } = vuex
-      let { getters } = vuex
-      // handle deprecated state option
-      if (state && !getters) {
-        console.warn(
-          '[vuex] vuex.state option will been deprecated in 1.0. ' +
-          'Use vuex.getters instead.'
-        )
-        getters = state
-      }
-      // getters
-      if (getters) {
-        options.computed = options.computed || {}
-        for (const key in getters) {
-          defineVuexGetter(this, key, getters[key])
-        }
-      }
-      // actions
-      if (actions) {
-        options.methods = options.methods || {}
-        for (const key in actions) {
-          options.methods[key] = makeBoundAction(this.$store, actions[key], key)
-        }
-      }
-    }
-  }
-
-  /**
-   * Setter for all getter properties.
-   */
-
-  function setter () {
-    throw new Error('vuex getter properties are read-only.')
-  }
-
-  /**
-   * Define a Vuex getter on an instance.
-   *
-   * @param {Vue} vm
-   * @param {String} key
-   * @param {Function} getter
-   */
-
-  function defineVuexGetter (vm, key, getter) {
-    if (typeof getter !== 'function') {
-      console.warn(`[vuex] Getter bound to key 'vuex.getters.${key}' is not a function.`)
-    } else {
-      Object.defineProperty(vm, key, {
-        enumerable: true,
-        configurable: true,
-        get: makeComputedGetter(vm.$store, getter),
-        set: setter
-      })
-    }
-  }
-
-  /**
-   * Make a computed getter, using the same caching mechanism of computed
-   * properties. In addition, it is cached on the raw getter function using
-   * the store's unique cache id. This makes the same getter shared
-   * across all components use the same underlying watcher, and makes
-   * the getter evaluated only once during every flush.
-   *
-   * @param {Store} store
-   * @param {Function} getter
-   */
-
-  function makeComputedGetter (store, getter) {
-    const id = store._getterCacheId
-
-    // cached
-    if (getter[id]) {
-      return getter[id]
-    }
-    const vm = store._vm
-    const Watcher = getWatcher(vm)
-    const Dep = getDep(vm)
-    const watcher = new Watcher(
-      vm,
-      vm => getter(vm.state),
-      null,
-      { lazy: true }
-    )
-    const computedGetter = () => {
-      if (watcher.dirty) {
-        watcher.evaluate()
-      }
-      if (Dep.target) {
-        watcher.depend()
-      }
-      return watcher.value
-    }
-    getter[id] = computedGetter
-    return computedGetter
-  }
-
-  /**
-   * Make a bound-to-store version of a raw action function.
-   *
-   * @param {Store} store
-   * @param {Function} action
-   * @param {String} key
-   */
-
-  function makeBoundAction (store, action, key) {
-    if (typeof action !== 'function') {
-      console.warn(`[vuex] Action bound to key 'vuex.actions.${key}' is not a function.`)
-    }
-    return function vuexBoundAction (...args) {
-      return action.call(this, store, ...args)
-    }
-  }
-
-  // option merging
-  const merge = Vue.config.optionMergeStrategies.computed
-  Vue.config.optionMergeStrategies.vuex = (toVal, fromVal) => {
-    if (!toVal) return fromVal
-    if (!fromVal) return toVal
-    return {
-      getters: merge(toVal.getters, fromVal.getters),
-      state: merge(toVal.state, fromVal.state),
-      actions: merge(toVal.actions, fromVal.actions)
-    }
-  }
-}

+ 1 - 1
src/plugins/devtool.js

@@ -11,7 +11,7 @@ export default function devtoolPlugin (store) {
     store.replaceState(targetState)
   })
 
-  store.on('mutation', (mutation, state) => {
+  store.subscribe(mutation, state) => {
     hook.emit('vuex:mutation', mutation, state)
   })
 }

+ 0 - 52
src/plugins/logger.js

@@ -1,52 +0,0 @@
-// Credits: borrowed code from fcomb/redux-logger
-
-export default function createLogger ({
-  collapsed = true,
-  transformer = state => state,
-  mutationTransformer = mut => mut
-} = {}) {
-  return store => {
-    let prevState = JSON.parse(JSON.stringify(store.state))
-
-    store.on('mutation', (mutation, state) => {
-      if (typeof console === 'undefined') {
-        return
-      }
-      const nextState = JSON.parse(JSON.stringify(state))
-      const time = new Date()
-      const formattedTime = ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}`
-      const formattedMutation = mutationTransformer(mutation)
-      const message = `mutation ${mutation.type}${formattedTime}`
-      const startMessage = collapsed
-        ? console.groupCollapsed
-        : console.group
-
-      // render
-      try {
-        startMessage.call(console, message)
-      } catch (e) {
-        console.log(message)
-      }
-
-      console.log('%c prev state', 'color: #9E9E9E; font-weight: bold', prevState)
-      console.log('%c mutation', 'color: #03A9F4; font-weight: bold', formattedMutation)
-      console.log('%c next state', 'color: #4CAF50; font-weight: bold', nextState)
-
-      try {
-        console.groupEnd()
-      } catch (e) {
-        console.log('—— log end ——')
-      }
-
-      prevState = nextState
-    })
-  }
-}
-
-function repeat (str, times) {
-  return (new Array(times + 1)).join(str)
-}
-
-function pad (num, maxLength) {
-  return repeat('0', maxLength - num.toString().length) + num
-}

+ 10 - 0
src/plugins/strict.js

@@ -0,0 +1,10 @@
+export default store => {
+  const Watcher = getWatcher(store._vm)
+  store._vm.watch('state', () => {
+    if (!store._dispatching) {
+      throw new Error(
+        '[vuex] Do not mutate vuex store state outside mutation handlers.'
+      )
+    }
+  }, { deep: true, sync: true })
+}

+ 0 - 72
src/util.js

@@ -1,72 +0,0 @@
-/**
- * Merge an array of objects into one.
- *
- * @param {Array<Object>} arr
- * @return {Object}
- */
-
-export function mergeObjects (arr) {
-  return arr.reduce((prev, obj) => {
-    Object.keys(obj).forEach(key => {
-      const existing = prev[key]
-      if (existing) {
-        // allow multiple mutation objects to contain duplicate
-        // handlers for the same mutation type
-        if (Array.isArray(existing)) {
-          prev[key] = existing.concat(obj[key])
-        } else {
-          prev[key] = [existing].concat(obj[key])
-        }
-      } else {
-        prev[key] = obj[key]
-      }
-    })
-    return prev
-  }, {})
-}
-
-/**
- * Check whether the given value is Object or not
- *
- * @param {*} obj
- * @return {Boolean}
- */
-
-export function isObject (obj) {
-  return obj !== null && typeof obj === 'object'
-}
-
-/**
- * Get state sub tree by given keys.
- *
- * @param {Object} state
- * @param {Array<String>} nestedKeys
- * @return {Object}
- */
-export function getNestedState (state, nestedKeys) {
-  return nestedKeys.reduce((state, key) => state[key], state)
-}
-
-/**
- * Hacks to get access to Vue internals.
- * Maybe we should expose these...
- */
-
-let Watcher
-export function getWatcher (vm) {
-  if (!Watcher) {
-    const noop = function () {}
-    const unwatch = vm.$watch(noop, noop)
-    Watcher = vm._watchers[0].constructor
-    unwatch()
-  }
-  return Watcher
-}
-
-let Dep
-export function getDep (vm) {
-  if (!Dep) {
-    Dep = vm._data.__ob__.dep.constructor
-  }
-  return Dep
-}