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' +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy