# 手写一个vuex

# 一句话总结原理

new了一个vue的实例,通过beforeCreate这个mixin, 把这个实例挂在每个组件上面,因此在组件里面可以通过this.$store访问vuex的方法等。

# 上代码

(function(w, _Vue) {
    let Vue;
    let store;

    function install(_Vue) {
        if (Vue && Vue === _Vue) {
            console.log("重复安装");
            return;
        }
        Vue = _Vue;
        applyMixin(Vue);
    }

    function applyMixin(Vue) {
        Vue.mixin({
            beforeCreate: vuexInit
        });
    }

    function vuexInit() {
        // this 指向每一个vue实例
        const { store, parent } = this.$options;
        if (store) {
            this.$store = typeof store === "function" ? store() : store;
        } else if (parent && parent.$store) {
            this.$store = parent.$store;
        }
    }

    function Store({
        state = {},
        getters = {},
        actions = {},
        mutations = {}
    } = {}) {
        store = this;
        let self = this;
        let computed = {};
        Object.keys(getters).forEach(key => {
            computed[key] = function() {
                return getters[key](self._vm.state);
            }
        });
        this._vm = new Vue({
            data: {
                state: state
            },
            methods: {
                ...actions
            },
            computed,
        });

        this.state = {};
        this.getters = {};
        this.actions = {};
        this.commit = function(type, ...args) {
            if (!mutations[type] || typeof mutations[type] !== "function") {
                throw new Error(`unknow mutation type ${type}`);
            }
            mutations[type](self.state, ...args)
        }
        proxyState(this, state);
        proxyGetters(this, getters);
        proxyActions(this, actions);

    }

    function proxyState(store, state) {
        Object.keys(state).forEach(key => {
            Object.defineProperty(store.state, key, {
                get() {
                    return store._vm.state[key]
                },
                set(val) {
                    store._vm.state[key] = val;
                }
            });
        });
    }

    function proxyGetters(store, getters) {
        Object.keys(getters).forEach(key => {
            Object.defineProperty(store.getters, key, {
                get() {
                    return store._vm[key];
                }
            });
        });
    }

    function proxyActions(store, actions) {
        Object.keys(actions).forEach(key => {
            Object.defineProperty(store.actions, key, {
                get() {
                    return function() {
                        return store._vm[key]({ commit: store.commit }, ...arguments)
                    }
                }
            })

        });
    }

    function toMap(arr) {
        let ret = {};
        if (Array.isArray(arr)) {
            arr.forEach(item => {
                ret[item] = item;
            });
        } else if (typeof arr === "object") {
            Object.keys(arr).forEach(key => {
                ret[key] = arr[key];
            });
        } else if (typeof arr === "string") {
            ret[arr] = arr;
        }
        return ret;
    }
    // ...mapState(['firstName', 'lastName'])
    // ...mapState({firstName: "localFirstName"})
    function mapState(state) {
        let ret = {};
        state = toMap(state);
        Object.keys(state).forEach(key => {
            ret[key] = function() {
                return store.state[state[key]]
            }
        });
        return ret;
    }

    function mapGetters(getters) {
        let ret = {};
        getters = toMap(getters);
        Object.keys(getters).forEach(key => {
            ret[key] = typeof store.getters[key] === "function" ? store.getters[key] : (function() {
                return store.getters[key]
            })
        });
        return ret;
    }

    function mapActions(actions) {
        let ret = {};
        actions = toMap(actions);
        Object.keys(actions).forEach(key => {
            ret[key] = store.actions[actions[key]];
        });
        return ret;
    }

    function mapMutations(mutations) {
        let ret = {};
        mutations = toMap(mutations);
        Object.keys(mutations).forEach(key => {
            ret[key] = function(...args) {
                store.commit(mutations[key], args)
            };
        });
        return ret;
    }

    const Vuex = {
        install,
        Store,
        mapState,
        mapGetters,
        mapActions,
        mapMutations,
    }
    window.Vuex = Vuex;
})(window, Vue);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174

# vuex中为什么把把异步操作封装在action,把同步操作放在mutations?

官方文档说明:“在 mutation 中混合异步调用会导致你的程序很难调试。例如,当你能调用了两个包含异步回调的 mutation 来改变状态,你怎么知道什么时候回调和哪个先回调呢?这就是为什么我们要区分这两个概念。在 Vuex 中,我们将全部的改变都用同步方式实现。我们将全部的异步操作都放在Actions中。” 但我不明白的是,我如果同时出发了两个异步的actions,那么这两个回调的时间不一样,那么对于state的更新还是存在竞态的。所以我觉得这样进行区分并不能解决“那么先回调,哪个后回调”来更新state的问题。

中文翻译可能有些偏差(不是我翻的)。区分 actions 和 mutations 并不是为了解决竞态问题,而是为了能用 devtools 追踪状态变化。事实上在 vuex 里面 actions 只是一个架构性的概念,并不是必须的,说到底只是一个函数,你在里面想干嘛都可以,只要最后触发 mutation 就行。异步竞态怎么处理那是用户自己的事情。vuex 真正限制你的只有 mutation 必须是同步的这一点(在 redux 里面就好像 reducer 必须同步返回下一个状态一样)。同步的意义在于这样每一个 mutation 执行完成后都可以对应到一个新的状态(和 reducer 一样),这样 devtools 就可以打个 snapshot 存下来,然后就可以随便 time-travel 了。如果你开着 devtool 调用一个异步的 action,你可以清楚地看到它所调用的 mutation 是何时被记录下来的,并且可以立刻查看它们对应的状态。其实我有个点子一直没时间做,那就是把记录下来的 mutations 做成类似 rx-marble 那样的时间线图,对于理解应用的异步状态变化很有帮助

作者:尤雨溪

链接:https://www.zhihu.com/question/48759748/answer/112823337 来源:知乎

# Mutation 必须是同步函数

一条重要的原则就是要记住 mutation 必须是同步函数。为什么?请参考下面的例子:

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}
1
2
3
4
5
6
7

现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。

上次更新: 2020-03-03 11:42:44