1. 首页

带你快速手写自己的Vuex

在阅读Vuex源码之前,因为Vuex的api和使用功能略微复杂,默认以为实现起来相当复杂,望而生畏。然而通过深入学习源码,发现核心功能结合vue实现起来非常巧妙,也就核心几行代码,直呼内行。本文也就100左右行代码就能快速手写自己的Vuex代码!

前言

Vuex 是⼀个专为 Vue.js应⽤程序开发的状态管理模式。它采⽤集中式存储管理应⽤的所有组件的状态,并以相应的规则保证状态以⼀种可预测的⽅式发⽣变化。那么Vuex和单纯的全局对象有什么不同呢?

Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发⽣变 化,那么相应的组件也会相应地得到⾼效更新。

不能直接改变 store 中的状态。改变 store 中的状态的唯⼀途径就是显式地提交 (commit) mutation。这样使得我们可以⽅便地跟踪每⼀个状态的变化,从⽽让我们能够实现⼀些⼯具帮助我 们更好地了解我们的应⽤。

通过以上两点认知我们来快速实现自己的Vuex!

Vuex 初始化

为什么在vue实例化的时候要传入store去实例化呢?那是为了让vue所有的组件中可以通过 this.$store来获取该对象,即 this.$store 指向 store 实例。

 /* Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/ */

// Store 待实现
const store = new Store({
  state: {
    count: 0,
    num: 10
})

new Vue({
  el: '#app',
  store: store // 此处的 store 为 this.$options.store
})


Vuex提供了install属性,通过Vue.use(Vuex)来注册。

 /* Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/ */

const install = function (Vue) {
  Vue.mixin({
    beforeCreate() {
      if (this.$options.store) {
        Vue.prototype.$store = this.$options.store
      }
    }
  })
}

Vue全局混⼊了⼀个 beforeCreated 钩⼦函数, options.store 保存在所有组件的 this.$store 中,这个 options.store 就是我们在实例化 Store 对象的实例。Store 对象的构造函数接收⼀个对象参数,它包含 actionsgettersstatemutations 等核⼼概念,接下来我们一一实现。

Vuex state

其实 state 是 vue 实例中的 data ,通过 Store 内部创建Vue实例,将 state 存储到 data 里,然后改变 state 就是触发了 data 数据的改变从而实现了视图的更新。

 /* Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/ */
// 实例化 Store
const store = new Store({
  state: {
    count: 0,
    num: 10
  }
})

// Store 实现
class Store {
  constructor({state = {}}) {
    this.vm = new Vue({
      data: {state} // state 添加到 data 中
    })
  }

  get state() {
    return this.vm.state // 将 state代理到 vue 实例中的 state
  }

  set state(v) {
    console.warn(`Use store.replaceState() to explicit replace store state.`)
  }
}

由上可知,store.state.count 等价于 store.vm.state。不论是获取或者改变state里面的数据都是间接的触发了vue中data数据的变化,从而触发视图更新。

Vuex getters

知道statevue实例中的data,那么同理,getters 就是 vue中的计算属性 computed。

 /* Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/ */
// 实例化 Store
const store = new Store({
  state: {
    count: 0,
    num: 10
  },
  getters: {
    total: state => {
      return state.num + state.count
    }
  },
})

// Store 实现
class Store {
  constructor({state = {}, getters = {}}) {
    this.getters = getters
    // 创建模拟 computed 对象
    const computed = {}
    Object.keys(getters).forEach(key => {
      const fn = getters[key]
      // 入参 state 和 getters
      computed[key] = () => fn(this.state, this.getters)
      // 代理 getters 到 vm 实例上
      Object.defineProperty(this.getters, key, {
        get: () => this.vm[key]
      })
    })
    // 赋值到 vue 中的 computed 计算属性中
    this.vm = new Vue({
      data: {
        state,
      },
      computed,
    })
  }

  get state() {
    return this.vm.state
  }

  set state(v) {
    console.warn(`Use store.replaceState() to explicit replace store state.`)
  }
}

使用 Object.defineProperty 将getters上的所有属性都代理到了vm实例上的computed计算属性中,也就是 store.getters.count 等价于 store.vm.count

Vuex mutations

mutations等同于发布订阅模式,先在mutations中订阅事件,然后再commit发布事件。

 /* Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/ */
// 实例化 Store
const store = new Store({
  state: {
    count: 0,
    num: 10
  },
  mutations: {
    INCREASE: (state, n) =>{
      state.count += n
    },
    DECREASE: (state, n) =>{
      state.count -= n
    }
  }
})

// Store 实现
class Store {
  constructor({state = {},  mutations = {}, strict = false}) {
    this.mutations = mutations
    // 严格摸索只能通过 commit 改变 state
    this.strict && this.enableStrictMode()
  }

  commit(key, payload) {
    // 获取事件
    const fn = this.mutations[key]
    // 开始 commit
    this.committing = true
    // 执行事件 并传参
    fn(this.state, payload)
    // 结束 commit  所以说明 commit 只能执行同步事件
    this.committing = false
  }

  enableStrictMode () {
    // vm实例观察 state 是否由 commit 触发改变
    this.vm.$watch('state', () => {
      !this.committing
      &&
      console.warn(`Do not mutate vuex store state outside mutation handlers.`)
    }, { deep: true, sync: true })
  }

  get state() {
    return this.vm.state
  }

  set state(v) {
    console.warn(`Use store.replaceState() to explicit replace store state.`)
  }

}

store.commit 执行 mutations中的事件,通过发布订阅实现起来并不难。 Vuex中的严格模式,只能在commit的时候改变state数据,不然提示错误。

Vuex actions

mutations 用于同步更新 state,而 actions 则是提交 mutations,并可进行异步操作,从而间接更新 state

 /* Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/ */

// 实例化 Store
const store = new Store({
  actions: {
    getToal({dispatch, commit, state, getters}, n){
      return new Promise(resolve => {
        setTimeout(() => {
          commit('DECREASE', n)
          resolve(getters.total)
        }, 1000)
      })
    }
  }
})

// Store 实现
class Store {
  constructor({actions = {}}) {
    this.actions = actions
  }

  dispatch(key, payload) {
    const fn = this.actions[key]
    const {state, getters, commit, dispatch} = this
    // 注意 this 指向
    const result = fn({state, getters, commit: commit.bind(this), dispatch: dispatch.bind(this)}, payload)
    // 返回 promise
    return this.isPromise(result) ? result :  Promise.resolve(result)
  }

  // 判断是否是 promise
  isPromise (val) {
    return val && typeof val.then === 'function'
  }

}


mutationsactions 的实现大同小异,actions 核心在于处理异步逻辑,并返回一个 promise

完整案例代码

这边把以上的代码统一归纳起来,可以根据这份完整代码来分析Vuex逻辑。

 /* Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/ */
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
</head>
<body>
  <div id="app">
    <child-a></child-a>
    <child-b></child-b>
  </div>
  <script>
    class Store {
      constructor({state = {}, getters = {}, mutations = {}, actions = {}, strict = false}) {
        this.strict = strict
        this.getters = getters
        this.mutations = mutations
        this.actions = actions
        this.committing = false
        this.init(state, getters)
      }

      get state() {
        return this.vm.state
      }

      set state(v) {
        console.warn(`Use store.replaceState() to explicit replace store state.`)
      }

      init (state,  getters) {
        const computed = {}
        Object.keys(getters).forEach(key => {
          const fn = getters[key]
          computed[key] = () => fn(this.state, this.getters)
          Object.defineProperty(this.getters, key, {
            get: () => this.vm[key]
          })
        })

        this.vm = new Vue({
          data: {state},
          computed,
        })

        this.strict && this.enableStrictMode()
      }

      commit(key, payload) {
        const fn = this.mutations[key]
        this.committing = true
        fn(this.state, payload)
        this.committing = false
      }

      dispatch(key, payload) {
        const fn = this.actions[key]
        const {state, getters, commit, dispatch} = this
        const res = fn({state, getters, commit: commit.bind(this), dispatch: dispatch.bind(this)}, payload)
        return this.isPromise(res) ? res :  Promise.resolve(res)
      }

      isPromise (val) {
        return val && typeof val.then === 'function'
      }

      enableStrictMode () {
        this.vm.$watch('state', () => {
          !this.committing && console.warn(`Do not mutate vuex store state outside mutation handlers.`)
        }, { deep: true, sync: true })
      }

    }

    const install = function () {
      Vue.mixin({
        beforeCreate() {
          if (this.$options.store) {
            Vue.prototype.$store = this.$options.store
          }
        }
      })
    }

    // 子组件 a
    const childA = {
      template: '<div><button @click="handleClick">click me</button> <button @click="handleIncrease">increase num</button> <button @click="handleDecrease">decrease num</button></div>',
      methods: {
        handleClick() {
          this.$store.state.count += 1
        },
        handleIncrease() {
          this.$store.commit('INCREASE', 5)
        },
        handleDecrease() {
          this.$store.dispatch('getToal', 5).then(data => {
            console.log('total', data)
          })
        }
      }
    }

    // 子组件 b
    const childB = {
      template: '<div><h1>count: {{ count }}</h1><h1>total: {{ total }}</h1></div>',
      mounted() {
        // 严格模式下修改state的值将警告
        // this.$store.state.count =  1
      },
      computed: {
        count() {
          return this.$store.state.count
        },
        total(){
          return this.$store.getters.total
        }
      }
    }

    const store = new Store({
      state: {
        count: 0,
        num: 10
      },
      getters: {
        total: state => {
          return state.num + state.count
        }
      },
      mutations: {
        INCREASE: (state, n) =>{
          state.count += n
        },
        DECREASE: (state, n) =>{
          state.count -= n
        }
      },
      actions: {
        getToal({dispatch, commit, state, getters}, n){
          return new Promise(resolve => {
            setTimeout(() => {
              commit('DECREASE', n)
              resolve(getters.total)
            }, 1000)
          })
        }
      }
    })

    Vue.use({install})

    new Vue({
      el: '#app',
      components: {
        'child-a': childA,
        'child-b': childB
      },
      store: store
    })
  </script>

</body>
</html>

总结

通过上面的完整案例可知,Vuex核心代码也就100行左右,但是他巧妙的结合了vuedatacomputeds属性,化繁为简,实现了复杂的功能,所以说vuex是不能脱离vue而独立运行的。
本文是结合官网源码提取核心思想手写自己的Vuex,而官网的Vuex,为了避免store结构臃肿,还实现了modules等功能,具体实现可以查看Vuex官网源码

作者:chinamasters
链接:https://segmentfault.com/a/1190000038174646

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:带你快速手写自己的Vuex

链接:https://www.javascriptc.com/4775.html

« 长知识!阿里用这样的组件开发大屏(二)
这一次,你一定能彻底搞懂nginx»
Flutter 中文教程资源

相关推荐

QR code