vuex的实现——使用插件及Mixin混入添加全局状态管理(二)

lizuncong (lizuncong) · GitHubI am a strong believer in reverse engineering. lizuncong has 42 repositories available. Follow their code on GitHub.https://github.com/lizuncong

这一节主要是介绍如何使用插件及混入开发全局单例模式管理状态。通过根组件注入这个全局单例对象,使得后代组件能够直接读取状态。

一、options的使用。options选项可以往组件中添加自定义属性。并可通过this.$options.xxx访问。我们在main.js定义并实例化一个Store类。通过options注入到根组件中,这样后代组件都能够通过this.$options.xxx访问到状态。

main.js代码:

import Vue from 'vue'
import App from './App'

Vue.config.productionTip = false

class Store {
  constructor (options = {}) {
    this.state = options.state || {}
  }
}

const state = {
  count: 1
}
const store = new Store({ state })
new Vue({
  el: '#app',
  store,
  components: { App },
  template: '<App/>'
})

然后在main.js中我们可以通过this.$options.store.state.count来访问共享的状态。

在App.vue中,我们可以通过this.$options.parent.$options.store.state.count来访问共享状态。

App.vue源码:

<template>
  <div id="app">
    app.vue
    <Counter/>
  </div>
</template>

<script>
import Counter from './components/counter'
export default {
  components: {
    Counter
  },
  mounted () {
    console.log(this.$options.parent.$options.store.state.count)
  }
}
</script>

在counter.vue中,我们可以通过this.$options.parent.$options.parent.$options.store.state.count访问共享状态。

counter.vue源码:

<template>
  <div id="counter">
    counter.vue
    <CounterItem/>
  </div>
</template>

<script>
import CounterItem from './counterItem'
export default {
  components: {
    CounterItem
  },
  mounted () {
    console.log(this.$options.parent.$options.parent.$options.store.state.count)
  }
}
</script>

在counterItem.vue中,我们可以通过this.$options.parent.$options.parent.$options.parent.$options.store.state.count来访问共享的状态。通过options,我们可以在任何组件中直接访问到共享的状态,而不需要一层一层从根组件往下传。这样如果App.vue没有使用到count,那么就不需要在App.vue中写任何关于count的代。但是,这个也带来了一个问题,通过this.$options.parent.xxxxx这种方式访问,写法繁琐并且难以阅读。如果组件树层级很深,那么这样写并不比一层一层传递props简便。那有没有什么方法可以在任何组件中都可以直接通过类似于this.$store.state的方式访问呢?比如,如果我们能够在每个组件初始化时,都把this.$options.xxx.store注入到this.$store中,那么我们就可以直接在组件中通过this.$store访问到共享状态,而不需要写一大串的名称。

二、Vue.mixin。全局混入。在Vue.js的api文档中,我们可以看到有这么一段话:全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。插件作者可以使用混入,向组件注入自定义的行为。不推荐在应用代码中使用。也就是说我们可以通过Vue.mixin来向所有组件自定义的行为。那么我们是不是可以在每个组件初始化的时候,将this.$options.xxx.store注入到this.$store属性中呢?往main.js中添加如下代码:

function vuexInit () {
  const options = this.$options
  // store injection
  if (options.store) {
    this.$store = typeof options.store === 'function'
      ? options.store()
      : options.store
  } else if (options.parent && options.parent.$store) {
    this.$store = options.parent.$store
  }
}
Vue.mixin({ beforeCreate: vuexInit })

这里我们使用Vue.mixin为每个组件全局注入了自定义行为,即在每个组件实例化后调用beforeCreate(vue组件生命周期钩子函数)方法。我们给beforeCreate传入了自定义的方法vuexInit。这个方法就是为了实现将this.$options.xxxx.store注入到每个组件的this.$store属性中。

在main.js实例化后,执行vuexInit方法,if(options.store)为真,this.$store 为全局单例对象store。

在App.vue实例化后,执行vuexInit方法,if(options.store)为假,执行第二个语句块this.$store = options.parent.$store。options.parent其实就是main.js组件实例,因此options.parent.$store就是store。这样我们就将store注入到App.vue组件中的this.$store属性中,我们现在可以在App.vue中直接通过this.$store.state.count来访问到共享状态了。

在counter.vue实例化后,执行vuexInit方法。if(options.store)为假,执行第二个语句块,options.parent为App.vue实例,因此options.parent.store将store注入到this.$store,即counter.vue组件实例中。同理,我们现在也可以在counter.vue中直接通过this.$store.state.count访问全局共享状态了。

到这里,我们已经可以全局为每个组件注入this.$store,达到直接访问全局状态的目的。这也是vuex源码中mixin.js的核心代码。

三、插件化。现在的main.js混杂了太多的逻辑,我们需要把store剥离出来。Vue.js 的插件应当有一个公开方法 install 。使用Vue.use安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。当 install 方法被同一个插件多次调用,插件将只会被安装一次。也就是说当我们使用Vue.use安装插件时,会自动调用插件里面的install方法,因此我们可以在install方法里调用Vue.mixin全局混入this.$store。

新建mixin.js文件,代码如下:

export default function (Vue) {
  Vue.mixin({ beforeCreate: vuexInit })

  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      // 注意这里的options.store(),有时候为了模块重用,以及避免状态单例,可以使用工厂函数
      // 返回store实例。参考https://ssr.vuejs.org/zh/guide/structure.html
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

新建store.js文件,代码如下:


import applyMixin from './mixin'
let Vue

class Store {
  constructor (options = {}) {
    this.state = options.state || {}
  }
} // Store

function install (_Vue) {
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}

export default {
  install,
  Store
}

修改main.js文件为:

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import MyVuex from './store.js'
Vue.use(MyVuex)

Vue.config.productionTip = false

const state = {
  count: 1
}

const store = new MyVuex.Store({
  state
})
/* eslint-disable no-new */
new Vue({
  el: '#app',
  store,
  components: { App },
  template: '<App/>'
})

这样我们就把逻辑剥离出来了,现在还没有往里面添加改变store状态的方法。

四、给store添加响应式属性。现在组件通过this.$store.state.count访问到共享的状态,然而现在的store并不能响应数据变化。在counterItem.vue中添加一个方法,改变count的值: this.$store.state.count ++。此时count的值改变了,然而视图却没有相应地刷新。例如,我们在App.vue中通过计算属性访问count,如:

<template>
  <div id="app">
    app.vue
    <div>{{ count }}</div>
    <Counter/>
  </div>
</template>

<script>
import Counter from './components/counter'
export default {
  components: {
    Counter
  },
  computed: {
    count () {
      return this.$store.state.count
    }
  }
}
</script>

在counterItem.vue中添加改变count的方法,如:

<template>
  <div id="counter-item">
    counter-item
    <div @click="increment">increment</div>
  </div>
</template>

<script>
export default {
  methods: {
    increment () {
      this.$store.state.count++
      console.log(this.$store.state.count)
    }
  }
}
</script>

点击increment按钮,发现console.log输出的state值已经改变了,然而视图却没有刷新。

我们知道vue实例中,data中的属性能够响应数据的变化并能够将变化响应到后代组件,因此我们可以在store中构造一个vue实例,将store的state状态存到该vue实例的data属性中。修改store.js如下:


import applyMixin from './mixin'
let Vue

class Store {
  constructor (options = {}) {
    const state = options.state || {}
    this._vm = new Vue({
      data: {
        $$state: state
      }
    })
  }
  get state () {
    return this._vm._data.$$state
  }
} // Store

function install (_Vue) {
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}

export default {
  install,
  Store
}

此时点击counterItem中的increment,可见视图刷新了,count的值也改变了。

现在已经实现了一个简单的store了,并且能够响应数据的变化。现在还没有往store里添加操作全局状态的方法。显然在每个组件中通过this.$store.state.count是不科学的。这种操作无异于直接操作全局变量,不利于debug。

下一节我会继续往Store类里面添加操作方法。

未完待续。。。。。。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值