1. 程式人生 > >vuex的實現——使用外掛及Mixin混入新增全域性狀態管理(二)

vuex的實現——使用外掛及Mixin混入新增全域性狀態管理(二)

這一節主要是介紹如何使用外掛及混入開發全域性單例模式管理狀態。通過根元件注入這個全域性單例物件,使得後代元件能夠直接讀取狀態。

一、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類裡面新增操作方法。

未完待續。。。。。。