diff --git a/dist/vuex.common.js b/dist/vuex.common.js index d30eab0ec..0e3bffcdf 100644 --- a/dist/vuex.common.js +++ b/dist/vuex.common.js @@ -1,6 +1,6 @@ /** * vuex v3.1.2 - * (c) 2019 Evan You + * (c) 2020 Evan You * @license MIT */ 'use strict'; @@ -72,6 +72,47 @@ function devtoolPlugin (store) { * @param {Function} f * @return {*} */ +function find (list, f) { + return list.filter(f)[0] +} + +/** + * Deep copy the given object considering circular structure. + * This function caches all nested objects and its copies. + * If it detects circular structure, use cached copy to avoid infinite loop. + * + * @param {*} obj + * @param {Array} cache + * @return {*} + */ +function deepCopy (obj, cache) { + if ( cache === void 0 ) cache = []; + + // just return if obj is immutable value + if (obj === null || typeof obj !== 'object') { + return obj + } + + // if obj is hit, it is in circular structure + var hit = find(cache, function (c) { return c.original === obj; }); + if (hit) { + return hit.copy + } + + var copy = Array.isArray(obj) ? [] : {}; + // put the copy into cache at first + // because we want to refer it in recursive deepCopy + cache.push({ + original: obj, + copy: copy + }); + + Object.keys(obj).forEach(function (key) { + copy[key] = deepCopy(obj[key], cache); + }); + + return copy +} /** * forEach for object @@ -109,6 +150,9 @@ var Module = function Module (rawModule, runtime) { // Store the origin module's state this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}; + + // Preserving orignal module's state + this.originalState = deepCopy(this.state); }; var prototypeAccessors = { namespaced: { configurable: true } }; @@ -394,7 +438,10 @@ Store.prototype.commit = function commit (_type, _payload, _options) { handler(payload); }); }); - this._subscribers.forEach(function (sub) { return sub(mutation, this$1.state); }); + + this._subscribers + .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe + .forEach(function (sub) { return sub(mutation, this$1.state); }); if ( process.env.NODE_ENV !== 'production' && @@ -426,6 +473,7 @@ Store.prototype.dispatch = function dispatch (_type, _payload) { try { this._actionSubscribers + .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe .filter(function (sub) { return sub.before; }) .forEach(function (sub) { return sub.before(action, this$1.state); }); } catch (e) { @@ -525,6 +573,11 @@ Store.prototype._withCommit = function _withCommit (fn) { this._committing = committing; }; +Store.prototype.reset = function reset () { + var originalState = getOriginalState(this._modules.root); + this.replaceState(originalState); +}; + Object.defineProperties( Store.prototype, prototypeAccessors$1 ); function genericSubscribe (fn, subs) { @@ -794,9 +847,7 @@ function enableStrictMode (store) { } function getNestedState (state, path) { - return path.length - ? path.reduce(function (state, key) { return state[key]; }, state) - : state + return path.reduce(function (state, key) { return state[key]; }, state) } function unifyObjectStyle (type, payload, options) { @@ -813,6 +864,15 @@ function unifyObjectStyle (type, payload, options) { return { type: type, payload: payload, options: options } } +function getOriginalState (module) { + var state = module.originalState || {}; + module.forEachChild(function (child, key) { + state[key] = getOriginalState(child); + }); + + return state +} + function install (_Vue) { if (Vue && _Vue === Vue) { if (process.env.NODE_ENV !== 'production') { diff --git a/src/module/module.js b/src/module/module.js index e856333d0..411eb2463 100644 --- a/src/module/module.js +++ b/src/module/module.js @@ -1,4 +1,4 @@ -import { forEachValue } from '../util' +import { forEachValue, deepCopy } from '../util' // Base data struct for store's module, package with some attribute and method export default class Module { @@ -12,6 +12,9 @@ export default class Module { // Store the origin module's state this.state = (typeof rawState === 'function' ? rawState() : rawState) || {} + + // Preserving orignal module's state + this.originalState = deepCopy(this.state) } get namespaced () { diff --git a/src/store.js b/src/store.js index 8b89d8187..7ba720560 100644 --- a/src/store.js +++ b/src/store.js @@ -226,6 +226,11 @@ export class Store { fn() this._committing = committing } + + reset () { + const originalState = getOriginalState(this._modules.root) + this.replaceState(originalState) + } } function genericSubscribe (fn, subs) { @@ -510,6 +515,15 @@ function unifyObjectStyle (type, payload, options) { return { type, payload, options } } +function getOriginalState (module) { + const state = module.originalState || {} + module.forEachChild((child, key) => { + state[key] = getOriginalState(child) + }) + + return state +} + export function install (_Vue) { if (Vue && _Vue === Vue) { if (process.env.NODE_ENV !== 'production') { diff --git a/test/unit/store.spec.js b/test/unit/store.spec.js index 561496e60..43da32ab0 100644 --- a/test/unit/store.spec.js +++ b/test/unit/store.spec.js @@ -434,5 +434,53 @@ describe('Store', () => { }) }) }) + + it('reset: Store resets to initial', done => { + const store = new Vuex.Store({ + state: { + count: 0, + arr: [], + obj: { + name: 0 + } + }, + mutations: { + [TEST]: state => { + state.count++ + state.arr.push(1) + state.obj.name = 1 + } + }, + modules: { + sub: { + namespaced: true, + state: { + count: 0 + }, + mutations: { + [TEST]: state => state.count++ + } + } + } + }) + store.commit(TEST) + store.commit('sub/' + TEST) + + Vue.nextTick(() => { + expect(store.state.count).toBe(1) + expect(store.state.arr.length).toBe(1) + expect(store.state.obj.name).toBe(1) + expect(store.state.sub.count).toBe(1) + + store.reset() + Vue.nextTick(() => { + expect(store.state.count).toBe(0) + expect(store.state.arr.length).toBe(0) + expect(store.state.obj.name).toBe(0) + expect(store.state.sub.count).toBe(0) + done() + }) + }) + }) } })