From 5053356706c4f3609a55b88e2559da0e52efdebd Mon Sep 17 00:00:00 2001 From: jack <1395093509@qq.com> Date: Mon, 27 May 2019 14:58:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AD=A6=E4=B9=A0=20vuex?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.js | 25 +++ src/module/module.js | 1 + src/store.js | 13 +- t/README.md | 109 +++++++++++++ t/helpers.js | 53 +++++++ t/index.js | 18 +++ t/module/module-collection.js | 80 ++++++++++ t/module/module.js | 74 +++++++++ t/store.js | 289 ++++++++++++++++++++++++++++++++++ t/util.js | 43 +++++ 10 files changed, 704 insertions(+), 1 deletion(-) create mode 100644 t/README.md create mode 100644 t/helpers.js create mode 100644 t/index.js create mode 100644 t/module/module-collection.js create mode 100644 t/module/module.js create mode 100644 t/store.js create mode 100644 t/util.js diff --git a/src/index.js b/src/index.js index ea28ccc1c..ca9d6f072 100644 --- a/src/index.js +++ b/src/index.js @@ -11,3 +11,28 @@ export default { mapActions, createNamespacedHelpers } + +// usage: +// const store = new Vuex.Store({ +// state: { +// count: 0, +// }, +// getters: {}, +// mutations: { +// increment (state) { +// state.count++ +// } +// }, +// actions: {}, +// modules: { +// errorLog, +// app, +// config, +// user, +// }, +// // plugins: [myPlugin], +// }); + +// store.commit('increment') +// console.log(store.state.count) // -> 1 +// or this.$store.state diff --git a/src/module/module.js b/src/module/module.js index e856333d0..df4d40127 100644 --- a/src/module/module.js +++ b/src/module/module.js @@ -3,6 +3,7 @@ import { forEachValue } from '../util' // Base data struct for store's module, package with some attribute and method export default class Module { constructor (rawModule, runtime) { + // 传入的参数rawModule就是{state,mutations,actions,getters}对象 this.runtime = runtime // Store some children item this._children = Object.create(null) diff --git a/src/store.js b/src/store.js index dfcc755f8..54ea32275 100644 --- a/src/store.js +++ b/src/store.js @@ -26,14 +26,17 @@ export class Store { } = options // store internal state + // this._committing 表示提交状态,作用是保证对 Vuex 中 state 的修改只能在 mutation 的回调函数中,而不能在外部随意修改state this._committing = false - this._actions = Object.create(null) + this._actions = Object.create(null) // {} this._actionSubscribers = [] this._mutations = Object.create(null) this._wrappedGetters = Object.create(null) this._modules = new ModuleCollection(options) this._modulesNamespaceMap = Object.create(null) this._subscribers = [] + + // 利用 Vue 实例方法 $watch 来观测变化的 this._watcherVM = new Vue() // bind commit and dispatch to self @@ -54,6 +57,7 @@ export class Store { // init root module. // this also recursively registers all sub-modules // and collects all module getters inside this._wrappedGetters + // 安装根模块 installModule(this, state, [], this._modules.root) // initialize the store vm, which is responsible for the reactivity @@ -96,6 +100,7 @@ export class Store { return } this._withCommit(() => { + // 遍历触发事件队列 entry.forEach(function commitIterator (handler) { handler(payload) }) @@ -254,6 +259,9 @@ function resetStoreVM (store, state, hot) { store.getters = {} const wrappedGetters = store._wrappedGetters const computed = {} + + // 通过Object.defineProperty为每一个getter方法设置get方法, 比如获取this.$store.getters.test的时候 + // 获取的是store._vm.test,对应Vue对象的computed属性 forEachValue(wrappedGetters, (fn, key) => { // use computed to leverage its lazy-caching mechanism computed[key] = () => fn(store) @@ -313,6 +321,7 @@ function installModule (store, rootState, path, module, hot) { const local = module.context = makeLocalContext(store, namespace, path) + // 注册mutation事件队列 module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(store, namespacedType, mutation, local) @@ -394,12 +403,14 @@ function makeLocalContext (store, namespace, path) { function makeLocalGetters (store, namespace) { const gettersProxy = {} + // account/ const splitPos = namespace.length Object.keys(store.getters).forEach(type => { // skip if the target getter is not match this namespace if (type.slice(0, splitPos) !== namespace) return // extract local getter type + // account/userinfo slice account/ => userinfo const localType = type.slice(splitPos) // Add a port to the getters proxy. diff --git a/t/README.md b/t/README.md new file mode 100644 index 000000000..f32c4d391 --- /dev/null +++ b/t/README.md @@ -0,0 +1,109 @@ +# 小程序支持 store(使用 vuex) + +支持小程序使用状态管理模式 + +- 暂不支持 computed 即 getter +- 暂不支持 namespace +- 暂不支持 plugin + - logger + - devtool +- 不支持严格模式 + +参考: + +- https://github.com/vuejs/vuex +- https://github.com/tinajs/tinax + +State + +```js +// 支持三种格式书写 +data: { + ...mapState([ + 'user', + ]) + ...mapState({ + countAlias: 'count', + // 箭头函数可使代码更简练 + count: state => state.count, + userInfo: state => state.user.userInfo, + // 为了能够使用 `this` 获取局部状态,必须使用常规函数 TODO: 待定 + countPlusLocalState (state) { + return state.count + this.localCount + }, + }), +} +``` + +Getter + +```js +// 可以认为是 store 的计算属性。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来, +// 且只有当它的依赖值发生了改变才会被重新计算。 +getters: { + add(state, getters, rootState) { + return state.count + rootState.count + } +} +``` + +Mutation + +```js +mutations: { + add(state, payload) { + state.count += payload.amount + }, +} + +// 三种格式,由 unifyObjectStyle 统一处理 +store.commit('add', 10) + +// 标准格式 +store.commit('add', { + amount: 10 +}) + +store.commit({ + type: 'add', + amount: 10 +}) +``` + +Action + +```js +actions: { + // checkout(context, payload, cb) {} + checkout({ commit, state }, products) { + commit(types.CHECKOUT_REQUEST) + + api.buyProducts( + products, + // 成功操作 + () => commit(types.CHECKOUT_SUCCESS), + // 失败操作 + () => commit(types.CHECKOUT_FAILURE, savedCartItems) + ) + }, +} + +// 三种格式,由 unifyObjectStyle 统一处理 +// 以载荷形式分发 +store.dispatch('incrementAsync', { + amount: 10 +}) + +// 以对象形式分发 +store.dispatch({ + type: 'incrementAsync', + amount: 10 +}) +``` + +参考资料 + +- https://vuex.vuejs.org/zh/guide/mutations.html +- https://github.com/dwqs/blog/issues/58 +- https://blog.kaolafed.com/2017/05/23/Vuex%20%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/ +- https://github.com/answershuto/learnVue/blob/master/docs/Vuex%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.MarkDown diff --git a/t/helpers.js b/t/helpers.js new file mode 100644 index 000000000..2787f6c47 --- /dev/null +++ b/t/helpers.js @@ -0,0 +1,53 @@ +export const mapState = states => { + const res = {} + normalizeMap(states).forEach(({ key, val }) => { + res[key] = function mappedState () { + let state = this.$store.state + return typeof val === 'function' + ? val.call(this, state) + : state[val] + } + }) + return res +} + +export const mapMutations = mutations => { + const res = {} + normalizeMap(mutations).forEach(({ key, val }) => { + res[key] = function mappedMutation (...args) { + // Get the commit method from store + let commit = this.$store.commit; + return typeof val === 'function' + ? val.apply(this, [commit].concat(args)) + : commit.apply(this.$store, [val].concat(args)) + } + }) + return res +} + +export const mapActions = actions => { + const res = {} + normalizeMap(actions).forEach(({ key, val }) => { + res[key] = function mappedAction (...args) { + // get dispatch function from store + let dispatch = this.$store.dispatch; + return typeof val === 'function' + ? val.apply(this, [dispatch].concat(args)) + : dispatch.apply(this.$store, [val].concat(args)) + } + }) + return res +} + +/** + * Normalize the map + * 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] })) +} diff --git a/t/index.js b/t/index.js new file mode 100644 index 000000000..9ae29231d --- /dev/null +++ b/t/index.js @@ -0,0 +1,18 @@ +import { Store, install } from './store'; +import { mapState, mapMutations, mapActions } from './helpers'; + +export default { + Store, + install, + mapState, + mapMutations, + mapActions, +} + +export { + Store, + install, + mapState, + mapMutations, + mapActions, +} diff --git a/t/module/module-collection.js b/t/module/module-collection.js new file mode 100644 index 000000000..e28c5efe7 --- /dev/null +++ b/t/module/module-collection.js @@ -0,0 +1,80 @@ +import Module from './module'; +import { forEachValue } from '../util'; + +export default class ModuleCollection { + constructor (rawRootModule) { + // register root module (Vuex.Store options) + this.register([], rawRootModule, false) + } + + // example: + // -> ['account', 'user'] 获取到对应的 module + get(path) { + return path.reduce((module, key) => { + return module.getChild(key) + }, this.root); + } + + // ['account'] -> account/ + // getNamespace (path) { + // let module = this.root + // return path.reduce((namespace, key) => { + // module = module.getChild(key) + // return namespace + (module.namespaced ? key + '/' : '') + // }, '') + // } + + update (rawRootModule) { + update([], this.root, rawRootModule) + } + + register(path, rawModule, runtime = true) { + + const newModule = new Module(rawModule, runtime) + if (path.length === 0) { + this.root = newModule + } else { + // arr.slice(?start, ?end) + const parent = this.get(path.slice(0, -1)) + parent.addChild(path[path.length - 1], newModule) + } + + // register nested modules + // key: errorLog, user... + if (rawModule.modules) { + forEachValue(rawModule.modules, (rawChildModule, key) => { + this.register(path.concat(key), rawChildModule, runtime) + }) + } + } + + unregister (path) { + const parent = this.get(path.slice(0, -1)) + const key = path[path.length - 1] + if (!parent.getChild(key).runtime) return + + parent.removeChild(key) + } +} + + +function update(path, targetModule, newModule) { + // update target module + targetModule.update(newModule); + + // update nested modules + if (newModule.modules) { + for (const key in newModule.modules) { + if (!targetModule.getChild(key)) return; + update( + path.concat(key), + targetModule.getChild(key), + newModule.modules[key] + ) + } + } +} + +// function assertRawModule (path, rawModule) {} + +// function makeAssertionMessage (path, key, type, value, expected) {} diff --git a/t/module/module.js b/t/module/module.js new file mode 100644 index 000000000..0262dbd3d --- /dev/null +++ b/t/module/module.js @@ -0,0 +1,74 @@ +import { forEachValue } from '../util'; + +// 传入的参数rawModule就是对象 +// { +// state, +// mutations, +// actions, +// } +export default class Module { + constructor(rawModule, runtime) { + this.runtime = runtime; + this._children = Object.create(null); + this._rawModule = rawModule; + + const rawState = rawModule.state; + + this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}; + } + + // get namespaced() { + // return !!this._rawModule.namespaced; + // } + + addChild(key, module) { + this._children[key] = module; + } + + removeChild(key) { + delete this._children[key]; + } + + getChild(key) { + return this._children[key]; + } + + update(rawModule) { + this._rawModule.namespaced = rawModule.namespaced; + + if (rawModule.actions) { + this._rawModule.actions = rawModule.actions; + } + if (rawModule.mutations) { + this._rawModule.mutations = rawModule.mutations; + } + // if (rawModule.getters) { + // this._rawModule.getters = rawModule.getters; + // } + } + + forEachChild(fn) { + forEachValue(this._children, fn); + } + + // forEachGetter(fn) { + // if (this._rawModule.getters) { + // forEachValue(this._rawModule.getters, fn) + // } + // } + + forEachAction(fn) { + if (this._rawModule.actions) { + forEachValue(this._rawModule.actions, fn) + } + } + + forEachMutation(fn) { + if (this._rawModule.mutations) { + forEachValue(this._rawModule.mutations, fn) + } + } +} + +// Object.create(null); +// https://stackoverflow.com/questions/15518328/creating-js-object-with-object-createnull diff --git a/t/store.js b/t/store.js new file mode 100644 index 000000000..459c43523 --- /dev/null +++ b/t/store.js @@ -0,0 +1,289 @@ +import ModuleCollection from './module/module-collection'; +import { forEachValue, isObject, isPromise } from './util'; + +// - 不支持 computed 即 getter +// - 不支持 namespace +// - 不支持 plugin +// - logger +// - devtool +// - 不支持 严格模式警告 + +export class Store { + constructor(options = {}) { + // store internal state + this._committing = false + this._actions = Object.create(null) // {} + this._actionSubscribers = [] + this._mutations = Object.create(null) + this._modules = new ModuleCollection(options) + this._subscribers = [] + + // bind commit and dispatch to self + 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) + } + + const state = this._modules.root.state + + // 安装根模块 + installModule(this, state, [], this._modules.root) + + resetStoreVM(this, state) + } + + get state () { + return this._vm.$$state + } + + set state (v) { + console.warn(`use store.replaceState() to explicit replace store state.`); + } + + // 触发对应 type 的 mutation + commit (_type, _payload, _options) { + // check object-style commit + const { + type, + payload, + options + } = unifyObjectStyle(_type, _payload, _options); + + const mutation = { type, payload }; + const entry = this._mutations[type]; + if (!entry) { + return; + } + this._withCommit(() => { + // 遍历触发事件队列 + entry.forEach(function commitIterator (handler) { + handler(payload); + }) + }) + this._subscribers.forEach(sub => sub(mutation, this.state)) + } + + dispatch (_type, _payload) { + // check object-style dispatch + const { + type, + payload + } = unifyObjectStyle(_type, _payload); + + const action = { type, payload }; + const entry = this._actions[type]; + if (!entry) { + return; + } + + try { + this._actionSubscribers + .filter(sub => sub.before) + .forEach(sub => sub.before(action, this.state)) + } catch (e) { + } + + const result = entry.length > 1 + ? Promise.all(entry.map(handler => handler(payload))) + : entry[0](payload); + + return result.then(res => { + try { + this._actionSubscribers + .filter(sub => sub.after) + .forEach(sub => sub.after(action, this.state)) + } catch (e) { + } + return res + }) + } + + subscribe (fn) { + return genericSubscribe(fn, this._subscribers) + } + + subscribeAction (fn) { + const subs = typeof fn === 'function' ? { before: fn } : fn + return genericSubscribe(subs, this._actionSubscribers) + } + + replaceState (state) { + this._withCommit(() => { + this._vm.$$state = state + }) + } + + registerModule (path, rawModule, options = {}) { + if (typeof path === 'string') path = [path] + + this._modules.register(path, rawModule) + installModule(this, this.state, path, this._modules.get(path), options.preserveState) + // reset store + resetStoreVM(this, this.state) + } + + unregisterModule (path) { + if (typeof path === 'string') path = [path] + + this._modules.unregister(path) + this._withCommit(() => { + const parentState = getNestedState(this.state, path.slice(0, -1)) + delete parentState[path[path.length - 1]]; + // Vue.delete(parentState, path[path.length - 1]) + }) + resetStore(this) + } + + hotUpdate (newOptions) { + this._modules.update(newOptions) + resetStore(this, true) + } + + _withCommit (fn) { + const committing = this._committing + this._committing = true + fn() + this._committing = committing + } +} + +function genericSubscribe (fn, subs) { + if (subs.indexOf(fn) < 0) { + subs.push(fn) + } + return () => { + const i = subs.indexOf(fn) + if (i > -1) { + subs.splice(i, 1) + } + } +} + +function resetStore (store, hot) { + store._actions = Object.create(null) + store._mutations = Object.create(null) + const state = store.state + // init all modules + installModule(store, state, [], store._modules.root, true) + // reset vm + resetStoreVM(store, state, hot) +} + +function resetStoreVM (store, state, hot) { + store._vm = { + $$state: state, + // computed, // 这里利用 Vue 才支持,否则暂不支持 + }; +} + +function installModule(store, rootState, path, module, hot) { + const isRoot = !path.length; + + // set state + if (!isRoot && !hot) { + const parentState = getNestedState(rootState, path.slice(0, -1)) + const moduleName = path[path.length - 1] + store._withCommit(() => { + parentState[moduleName] = module.state; + // Vue.set(parentState, moduleName, module.state) + }) + } + + // ['account'] -> account/ + const local = module.context = makeLocalContext(store, path) + + // 注册mutation事件队列 + module.forEachMutation((mutation, key) => { + registerMutation(store, key, mutation, local) + }) + + module.forEachAction((action, key) => { + const type = key; + const handler = action.handler || action + registerAction(store, type, handler, local) + }) + + module.forEachChild((child, key) => { + installModule(store, rootState, path.concat(key), child, hot) + }) +} + +/** + * make localized dispatch, commit, and state + */ +function makeLocalContext (store, path) { + const local = { + dispatch: store.dispatch, + commit: store.commit, + } + + Object.defineProperties(local, { + // getters: { + // get() {return store.getters} + // }, + state: { + get: () => getNestedState(store.state, path) + } + }) + + return local +} + +function registerMutation (store, type, handler, local) { + const entry = store._mutations[type] || (store._mutations[type] = []) + entry.push(function wrappedMutationHandler (payload) { + handler.call(store, local.state, payload) + // 这里修改数据时,回调 setData,改变页面数据 + store.$callback = function (fn){ + if (typeof fn === 'function') { + fn(JSON.parse(JSON.stringify(local.state || null))); + } + } + }) +} + +function registerAction (store, type, handler, local) { + const entry = store._actions[type] || (store._actions[type] = []) + entry.push(function wrappedActionHandler (payload, cb) { + let res = handler.call(store, { + dispatch: local.dispatch, + commit: local.commit, + state: local.state, + rootState: store.state + }, payload, cb) + if (!isPromise(res)) { + res = Promise.resolve(res) + } + return res + }) +} + +function getNestedState (state, path) { + return path.length + ? path.reduce((state, key) => state[key], state) + : state +} + +function unifyObjectStyle (type, payload, options) { + if (isObject(type) && type.type) { + options = payload + payload = type + type = type.type + } + + return { type, payload, options } +} + +export function install (xmini) { + const mixins = { + $store: that.$store, + }; + + // xmini.addMixin('app', mixins); + xmini.addMixin('page', mixins); + xmini.addMixin('component', mixins); +} diff --git a/t/util.js b/t/util.js new file mode 100644 index 000000000..a8313debd --- /dev/null +++ b/t/util.js @@ -0,0 +1,43 @@ + +export function find (list, f) { + return list.filter(f)[0] +} + +export function deepCopy (obj, 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 + const hit = find(cache, c => c.original === obj) + if (hit) { + return hit.copy + } + + const 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 + }) + + Object.keys(obj).forEach(key => { + copy[key] = deepCopy(obj[key], cache) + }) + + return copy +} + +export function forEachValue (obj, fn) { + Object.keys(obj).forEach(key => fn(obj[key], key)) +} + +export function isObject (obj) { + return obj !== null && typeof obj === 'object' +} + +export function isPromise (val) { + return val && typeof val.then === 'function' +}