实现一个简单的Vuex

Vuex是什么

Vuex是专为Vue.js应用程序开发的全局状态管理器。这里我们可以把它拆成两部分来理解:一个是“全局状态管理”,一个是“专为Vue开发”。

首先来看全局状态管理。假设我们有一个购物网站的需求,在商品推荐、购物车等许多页面都需要显示商品的价格,并且同一商品在不同页面显示的价格应该是相同的,同时价格的变动也需要是同步变化。在这种场景下,我们往往会把商品的价格定义为一个全局变量,不管哪个页面需要获取商品的价格都去读取这个变量,不管因为什么原因导致了价格变动都去修改这个变量,这样我们就实现了多个页面价格数据的同步。不过,在实际开发中我们不会简单地定义一个全局变量,越来越多的全局变量会导致我们的代码难以维护;我们往往会给这些需要放到全局的数据定义一个命名空间,并且按照一些约定对操作这些数据的行为进行封装。这样,我们就实现了对全局状态的管理。

“专为Vue开发”是因为Vuex使用了Vue对数据的“变化侦测”系统。上面我们实现了全局状态管理,但是全局状态的变化并不会主动通知使用它的地方,比如它不会触发dom重新渲染,如果购物车页面想要在商品价格变动时实时渲染页面,则它需要通过一定的机制(比如轮询、全局状态修改的回调等)来获取全局状态的改变,然后在得到全局状态发生改变的消息后“拉取”最新的价格数据,从而实现dom的更新。Vuex正是通过Vue对数据的“变化侦测”系统实现这个过程的,在初始化存储在Vuex中的state时,会使用Vue初始化data时一样的机制来给state收集依赖,在state的属性发生变化时,会触发相应依赖的更新事件,从而实现渲染dom等操作。

Vuex的使用方法

接下来我们来看一下Vuex的使用方法,然后再根据它的使用方法反推它的内部实现,手写一个简单的Vuex。

使用Vuex首先需要创建一个Vuex示例:

new Vuex.store({
  // Vuex模块,如果需要存储的全局状态非常多,可以分成不同的模块,然后以模块的方式进行注册
  modules: {},

  // 需要保存的全局状态
  state: {
    value: 'abc'
  },

  // 类似于计算属性,可以对数据进行格式化
  getters: {
    formatValue (state, getters) {
      return `currentValue: ${state.value}`
    }
  }

  // 修改全局状态的方法,所有全局状态的修改,都应该定义为方法写在这里
  // 此处注册的方法,第一个参数是存储的全局状态数据对象,后面的参数是提交修改时传递的参数
  mutations: {
    updateValue (state, newValue) {
      state.value = newValue
    }
  },

  // 异步修改全局状态的方法,该方法主要封装异步逻辑,不能直接修改state,需要通过commit调用mutations中的方法进行修改
  // 此处注册的方法,第一个参数是当前的Vuex实例,后面的参数是提交修改时传递的参数
  actions: {
    asyncUpdateValue (context, newValue) {
      setTimeout(() => {
        context.commit('updateValue', newValue)
      }, 10)
    }
  }
})

在页面上使用Vuex存储的全局状态:

/************** 获取值 **************/

// 直接取值
console.log(this.$store.state.value) // 'abc'

// 为了防止Vuex存储的值被意外修改,一般会把它转换为计算属性使用
computed: {
  value () {
    return this.$store.state.value
  }
}

// 通过getters获取被处理过的值
console.log(this.$store.getters.formatValue) // 'currentValue: abc'

/************** 修改值 *************/

// 执行mutations中的方法修改值
// commit方法的第一个参数是注册在mutations中的方法名,其他参数会在执行该方法时传给它
// 如果需要传递多个参数,建议通过对象字面量的方式进行传参
this.$store.commit('updateValue', 'ABC')

// 执行actions中的方法修改值
// dispatch方法的第一个参数是注册在actions中的方法名,其他参数会在执行该方法时传给它
this.$store.dispatch('asyncUpdateValue', 'AAA')

实现一个简单的Vuex

首先说明一下,笔者并没有看过Vuex源码,以下代码是根据Vuex的使用方法反推的代码实现;如果代码有问题或者有不合理的地方,欢迎在评论区留言。另外,本文只实现Vuex的核心功能,对state进行的数据侦测、Vuex模块、以及Vuex的一些工具方法暂时不作讨论。

首先,初始化Vuex是用new Vuex.store()操实例化出一个对象,并且这个对象具有commit和dispatch方法。所以我们推断Vuex.store是一个构造函数,并且它的prototype上有commit、dispatch方法:

// 注:为了代码简洁,我们用Store来代替Vuex.store,后面的代码一样

class Store {
  constructor (options) {},

  commit () {},

  dispatch () {}
}

初始化对象时,需要对传入的选项对象进行初始化。 

// 注:为了代码简洁,我们用Store来代替Vuex.store,后面的代码一样

class Store {
  constructor (options) {
    // 给state赋值后,需要调用Vue的数据监听方法,将其转换成响应式数据,这里省略了这部分逻辑
    this.state = options.state || {}

    // 保存mutations方法
    this.mutations = options.mutations || {}

    // 保存actions方法
    this.actions = options.actions || {}

    // getters比较特殊,我们放到后面讨论
  },

  commit () {},

  dispatch () {}
}

现在我们已经可以通过$store.state.value来取到当前状态的值了,接下来我们实现commit和dispatch方法。从调用方式来看,这两个方法非常相似,都是通过一个字符串值来从已注册的方法中找到需要调用的那个,在调用的时候他们都会把第2个及以后的参数原封不动地传给回调;不同的是commit会把state作为第一个参数、dispatch会把当前的Vuex实例作为第一个参数传给回调。

// 注:为了代码简洁,我们用Store来代替Vuex.store,后面的代码一样

class Store {
  constructor (options) {
    // 给state赋值后,需要调用Vue的数据监听方法,将其转换成响应式数据,这里省略了这部分逻辑
    this.state = options.state || {}

    // 保存mutations方法
    this.mutations = options.mutations || {}

    // 保存actions方法
    this.actions = options.actions || {}

    // getters比较特殊,我们放到后面讨论
  },

  commit (callbackName, ...options) {
    return this.mutations[callbackName] && this.mutations[callbackName](this.state, ...options)
  },

  dispatch (callbackName, ...options) {
    return this.actions[callbackName] && this.actions[callbackName](this, options)
  }
}

现在我们可以通过$store.commit()、$store.dispatch()方法来修改状态了,接下来我们看一下getters。getters之所以特殊,是因为它是以方法的形式进行注册的,但确却是以对象属性的方式进行取值的。对于这个需求,我的第一想法是通过对象属性的get来实现:

// 注:为了代码简洁,我们用Store来代替Vuex.store,后面的代码一样

class Store {
  constructor (options) {
    // 给state赋值后,需要调用Vue的数据监听方法,将其转换成响应式数据,这里省略了这部分逻辑
    this.state = options.state || {}

    // 保存mutations方法
    this.mutations = options.mutations || {}

    // 保存actions方法
    this.actions = options.actions || {}

    // 处理getters
    const getters = options.getters || {}
    this.getters = Object.create(null)
    for (const key in getters) {
      Object.defineProperty(this.getters, key, {
        enumerable: true,
        get: () => {
          return getters[key] && getters[key](this.state, this.getters)
        }
      })
    }
  },

  commit (callbackName, ...options) {
    return this.mutations[callbackName] && this.mutations[callbackName](this.state, ...options)
  },

  dispatch (callbackName, ...options) {
    return this.actions[callbackName] && this.actions[callbackName](this, options)
  }
}

上面的代码看上去实现了getters,可以通过$store.getters.formatValue来取值了,但它存在两个问题:

  • 上面是通过属性属性的get来实现的,在Vue 3.0之前,数据的变化侦测也是通过对象属性的get、set来做的,上面代码中简单粗暴的方法肯定会跟Vue的变化侦测过程发生冲突,Vuex中是怎样处理这个冲突的呢?
  • vuex中注册的getters方法第二个参数是可以接收其他getters的,上面的代码虽然把已经注册的getters作为参数传递给了注册的方法,但是它没有过滤掉当前属性的getters,如果在当前的getters中调用了自身的getters,则会触发死循环。

现在,我们已经实现了一个并不完美的简易版Vuex。关于最后的两个问题,以及前面代码实现中的问题,欢迎大家留言讨论。

你可能感兴趣的:(vue.js,前端)