手寫vuex及原理解析

參考vuex代碼倉庫,  vuex官方文檔

關於vuex 的怎麼使用,就不說了,  這次主要是通過自己實現一個vuex,來理解vuex 的原理及內部實現

關於vuex 的工作流程, 就不再贅述了,官方的流程圖(在vuex是什麼?)這一節中畫的很清楚(具體位置基本在最底部了)

具體工作流程:

1. 每個組件都共享Store中的數據, 以及每個組件都可以通過 $store.state  或者 getters 拿到傳入的數據,

2. 通過事件 或者 回調函數 觸發  muation,進行同步更新數據, 從而觸發視圖更新

3. 通過 提交 action  進行異步操作數據,

注: 如果想做到同步更新視圖, 必須在 action 裏的計算,再提交到 muaction裏去更改數據

下邊就自己vuex 中的 getters, state,  muations,  actions ,

module 有些複雜, 就不再做實現了,有興趣自己實現下, 就是大量遞歸處理數據 ,

關於遞歸,很耗性能, 所以源碼中的遞歸都是 使用 reduce 代替的

使用腳手架創建一個 vue 項目, 修改src下目錄

App.vue

<template>
  <div id="app">
    <h2>vuex</h2>
  </div>
</template>
<script>
export default {}
</script>

main.js

import Vue from "vue"
import App from "./App.vue"
import store from "./store.js"
Vue.config.productionTip = false
new Vue({
  store,
  render: h => h(App)
}).$mount("#app")

store.js

import Vue from "vue"
import Vuex from "vuex"
Vue.use(Vuex) 
export default new Vuex.Store({
  state: {},
  getters: {},
  mutations: {},
  actions: {},
  modules: {}
})

先看原生庫的 state 使用

修改store.js

import Vue from "vue"
import Vuex from "vuex"
Vue.use(Vuex) 
export default new Vuex.Store({
  state: {
    name: "我是原生的state數據"
  },
  getters: {},
  mutations: {},
  actions: {},
  modules: {}
})

修改App.vue

<template>
  <div id="app">
    <h2>vuex</h2>
    <p>{{$store.state.name}}</p>
  </div>
</template>
<script>
export default {}
</script>

渲染展示

分析:首先組件都有一個 $store 的屬性, 然後可以拿到 new Vuex.store({}) 方法傳入的這個對象的數據

 

分析上邊的代碼: 使用 了 Vue.use() 方法,官方文檔是這樣解釋的, 可以看到使用 use() 方法,必須提供一個 install 方法

該方法 有兩個 參數, 一個 是 Vue 的構造器, 另一個是可選項

 

好了, 接着 手寫 自己的vuex 實現state , 再src 目錄下創建 vuex.js 文件,分個屏展示一下,然後把引入的vuex文件換成自己的文件

既然說了, 當使用 Vue.use() 必須提供一個 install 方法,第一個參數 是Vue 構造器,所以說可以拿到 vue的一切方法和屬性,

const install = _Vue => {
  console.log("install")
}

export default {
  install
}

然後又 new Vuex.Store()方法 所以還需要提供一個 Store的 class 

下邊代碼 使用了 Vue.mixin({}) ,混入 參考文檔 Vue-use ,  傳入一個對象, 附加了一個 vue的生命週期鉤子 beforeCreate

全局混入 參考  全局混入用法  ,可以在這裏 添加一些 擴展方法,註冊一些 全局屬性

let Vue
class Store {}

const install = _Vue => {
  console.log("install")
  Vue = _Vue // 用一個變量接收 _Vue 構造器
    // 全局註冊一個混入,影響註冊之後所有創建的每個 Vue 實例
  Vue.mixin({
    beforeCreate() {
      console.log(1)
    }
  })
}

export default {
  install,
  Store
}

可以看到已經可以打印出來了

 

由於,數據是共享的, 所以每個組件都需要拿到store中的數據, 我們在 組件中使用的時候, 是通過  this.$store 可以拿到數據,也就是每個組件都需要 有$store 屬性,  所以我們可以從 $options 拿到所有的屬性,打印如下:

let Vue
class Store {}

const install = _Vue => {
  console.log("install")
  Vue = _Vue // 用一個變量接收 _Vue 構造器
  Vue.mixin({
    beforeCreate() {
      console.log(this.$options)
    }
  })
}

export default {
  install,
  Store
}

 

let Vue
class Store {}

const install = _Vue => {
  console.log("install")
  Vue = _Vue // 用一個變量接收 _Vue 構造器
  Vue.mixin({
    beforeCreate() {
      //判斷 根 實例 有木有傳入store 數據源,
      //如果傳入了, 就把它放到實例的 $store上
      if (this.$options && this.$options.store) {
        this.$store = this.$options.store
      } else {
        // 2. 子組件去取父級組件的$store屬性
        this.$store = this.$parent && this.$parent.$store
      }
    }
  })
}

export default {
  install,
  Store
}

接着看 new Vuex.Store() 方法,該方法傳入一個對象,包含state, getters, muations, action 等可選屬性

所以  Store 類 必須接受一個參數, 代碼如下:

let Vue
class Store {
  constructor(options = {}) {
    console.log(options)
  }
}

const install = _Vue => {
  console.log("install")
  Vue = _Vue // 用一個變量接收 _Vue 構造器
  Vue.mixin({
    beforeCreate() {
      //判斷 根 實例 有木有傳入store 數據源,
      //如果傳入了, 就把它放到實例的 $store上
      if (this.$options && this.$options.store) {
        this.$store = this.$options.store
      } else {
        // 2. 子組件去取父級組件的$store屬性
        this.$store = this.$parent && this.$parent.$store
      }
    }
  })
}

export default {
  install,
  Store
}

打印如下,已經可以拿到數據了,所以 繼續改寫 Store 類 的 代碼

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

App.vue文件

<template>
  <div id="app">
    <h2>vuex</h2>
    <p>{{$store.state.name}}</p>
  </div>
</template>

<script>
export default {}
</script>

刷新頁面 ,已經可以拿到數據了, 這樣 vuex 中的 state,已經 初步 實現了

手寫 vuex 中的 getters , 先看原生的 getters是怎麼使用的

import Vue from "vue"
import Vuex from "vuex"

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    name: "我是原生的state數據",
    mill: "我是getters用的"
  },
  getters: {
    getMill(state) {
      return state.mill
    }
  },
  mutations: {},
  actions: {},
  modules: {}
})

App.vue

<template>
  <div id="app">
    <h2>vuex</h2>
    <p>{{mill}}</p>
  </div>
</template>

<script>
export default {
  computed: {
    mill() {
      return this.$store.getters.getMill
    }
  }
}
</script>

渲染爲:

 

我們再組件中 打印 this.$store.getters, 可以發現是一個對象

而傳入的時候再 getters 裏 是個方法, 所以 需要把對象 的方法,變成 對象的屬性返回, 並接受一個state 參數

好了, 接着來寫自己的 vuex.js中的 getters

class Store {
  constructor(options = {}) {
    this.state = options.state
    //   定義實例上的 getters
    this.getters = {}
    //   遍歷所有的對象中的方法名
    Object.keys(options.getters).forEach(key => {
      // 重新構造 this.getters 對象
      Object.defineProperty(this.getters, key, {
        get: () => {
          return options.getters[key](this.state)
        }
      })
    })
  }
}

刷新頁面 

到這裏基本實現了 state, getters 這兩個功能, 但是問題來了, 我們都知道vue 是響應式的, 所以 直接改 vuex 的數據是可以變化的

下邊用一個錯誤語法演示

 App.vue

<template>
  <div id="app">
    <h2>vuex</h2>
    <p>{{mill}}</p>
    <p>{{$store.state.num}}</p>
  </div>
</template>

<script>
export default {
  computed: {
    mill() {
      console.log(this.$store.getters)
      return this.$store.getters.getMill
    }
  },
  mounted() {
    setInterval(() => {
      this.$store.state.num += 1
    }, 1000)
  }
}
</script>

store.js

import Vue from "vue"
import Vuex from "vuex"

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    name: "我是原生的state數據",
    mill: "我是getters用的",
    num: 1
  },
  getters: {
    getMill(state) {
      return state.mill
    }
  },
  mutations: {},
  actions: {},
  modules: {}
})

然而使用 我們自己寫的 發現確不變化,  那麼需要來解決這個問題,

我們知道 ,vue 中的數據只要是寫在 data 中的,都是支持 響應式的, 所以 我們只有把 vuex 中的state 定義在 Vue中的 data中

改寫代碼:

class Store {
  constructor(options = {}) {
    this.state = new Vue({
      data() {
        return {
          state: options.state
        }
      }
    })
    //   定義實例上的 getters
    this.getters = {}
    //   遍歷所有的對象中的方法名
    Object.keys(options.getters).forEach(key => {
      // 重新構造 this.getters 對象
      Object.defineProperty(this.getters, key, {
        get: () => {
          return options.getters[key](this.state)
        }
      })
    })
  }
}

但如果這樣寫的 話, 就不能像原先那樣調用了,需要多調一層 state才能拿到數據,  (使用類的屬性訪問器setter, getter)解決這個問題,參考 es6文檔

代碼改寫爲下邊:

class Store {
  constructor(options = {}) {
    this.myState = new Vue({
      data() {
        return {
          state: options.state
        }
      }
    })
    //   定義實例上的 getters
    this.getters = {}
    //   遍歷所有的對象中的方法名
    Object.keys(options.getters).forEach(key => {
      // 重新構造 this.getters 對象
      Object.defineProperty(this.getters, key, {
        get: () => {
          return options.getters[key](this.state)
        }
      })
    })
  }
  get state() {
    return this.myState.state
  }
}

到這裏,已經基本實現了, 數據響應式變化, state, getters 了


整理只實現 state方法 和 每個組件中的 $store方法 的代碼段:

let Vue
class Store {
  constructor(options = {}) {
    //核心代碼: 保證數據都是響應式的
    this.myState = new Vue({
      data() {
        return {
          state: options.state
        }
      }
    })
  }
  // 類的屬性訪問器
  get state() {
    return this.myState.state
  }
}

const install = _Vue => {
  console.log("install")
  Vue = _Vue // 用一個變量接收 _Vue 構造器
  Vue.mixin({
    beforeCreate() {
      //判斷 根 實例 有木有傳入store 數據源,
      //如果傳入了, 就把它放到實例的 $store上
      if (this.$options && this.$options.store) {
        this.$store = this.$options.store
      } else {
        // 2. 子組件去取父級組件的$store屬性
        this.$store = this.$parent && this.$parent.$store
      }
    }
  })
}

export default {
  install,
  Store
}

整理實現 state 和 getters 和 每個組件中的 $store方法 的代碼段:

let Vue
class Store {
  constructor(options = {}) {
    // 核心代碼: 保證數據都是響應式的
    this.myState = new Vue({
      data() {
        return {
          state: options.state
        }
      }
    })
    //   定義實例上的 getters
    this.getters = {}
    //   遍歷所有的對象中的方法名
    Object.keys(options.getters).forEach(key => {
      // 重新構造 this.getters 對象
      Object.defineProperty(this.getters, key, {
        get: () => {
          return options.getters[key](this.state)
        }
      })
    })
  }
  get state() {
    return this.myState.state
  }
}

const install = _Vue => {
  console.log("install")
  Vue = _Vue // 用一個變量接收 _Vue 構造器
  Vue.mixin({
    beforeCreate() {
      //判斷 根 實例 有木有傳入store 數據源,
      //如果傳入了, 就把它放到實例的 $store上
      if (this.$options && this.$options.store) {
        this.$store = this.$options.store
      } else {
        // 2. 子組件去取父級組件的$store屬性
        this.$store = this.$parent && this.$parent.$store
      }
    }
  })
}

export default {
  install,
  Store
}

實現commit 方法 , 進行修改數據數據,先看原生的

$store 下 有個 commit 方法, commit() 方法 提供兩個參數, 一個 muation裏定義的函數名 , 一個是傳入的後續參數,其實呢,這就是個發佈訂閱者模式, 因爲 方法可能有多個, 只不過是按 屬性 名進行發佈訂閱的, 好了, 接着寫, 既然有 muations, 那我們先接收所有的muations, 對外只暴露commit 方法

  let mutations = {} // 定義一個對象收集所有傳入的 mutations 

  Object.keys(options.mutations).forEach(key => {
     mutations[key] = () => {
     
    }
  })

實現commit 方法, commit()

//  提供commit 方法
this.commit = (key, payload) => {
    mutations[key](payload)
}

所以上邊 mutations 的訂閱 ,它收集的是所有方法, 需要遍歷所有的key , 所以取出所有的key 進行 一次執行

 //  定義 muations
    let mutations = {}
    Object.keys(options.mutations).forEach(key => {
      mutations[key] = payload => {
        options.mutations[key](this.state, payload)
      }
    })

整理以上的 commit 實現的代碼

let Vue
class Store {
  constructor(options = {}) {
    // 核心代碼: 保證數據都是響應式的
    this.myState = new Vue({
      data() {
        return {
          state: options.state
        }
      }
    })
    //   定義實例上的 getters
    this.getters = {}
    //   遍歷所有的對象中的方法名
    Object.keys(options.getters).forEach(key => {
      // 重新構造 this.getters 對象
      Object.defineProperty(this.getters, key, {
        get: () => {
          return options.getters[key](this.state)
        }
      })
    })

    //  定義 muations
    let mutations = {}
    Object.keys(options.mutations).forEach(key => {
      mutations[key] = payload => {
        options.mutations[key](this.state, payload)
      }
    })
    //  提供commit 方法
    this.commit = (key, payload) => {
      mutations[key](payload)
    }
  }

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

const install = _Vue => {
  console.log("install")
  Vue = _Vue // 用一個變量接收 _Vue 構造器
  Vue.mixin({
    beforeCreate() {
      //判斷 根 實例 有木有傳入store 數據源,
      //如果傳入了, 就把它放到實例的 $store上
      if (this.$options && this.$options.store) {
        this.$store = this.$options.store
      } else {
        // 2. 子組件去取父級組件的$store屬性
        this.$store = this.$parent && this.$parent.$store
      }
    }
  })
}

export default {
  install,
  Store
}

整理代碼打印結果: (只暴露了一個 commit 的提交)

 

實現dispatch 方法 ,就跟 commit 差不多 了 

 // 收集 actions
    let actions = {}
    Object.keys(options.actions).forEach(key => {
      actions[key] = payload => {
        options.actions[key](this, payload)
      }
    })
    this.dispatch = (key, payload) => {
      actions[key](payload)
    }

整理最後的代碼

let Vue

class Store {
  constructor(options = {}) {
    // 核心代碼: 保證數據都是響應式的
    this.myState = new Vue({
      data() {
        return {
          state: options.state
        }
      }
    })
    //   定義實例上的 getters
    this.getters = {}
    //   遍歷所有的對象中的方法名
    Object.keys(options.getters).forEach(key => {
      // 重新構造 this.getters 對象
      Object.defineProperty(this.getters, key, {
        get: () => {
          return options.getters[key](this.state)
        }
      })
    })

    //  定義 muations
    let mutations = {}
    Object.keys(options.mutations).forEach(key => {
      mutations[key] = payload => {
        options.mutations[key](this.state, payload)
      }
    })
    //  提供commit 方法
    this.commit = (key, payload) => {
      mutations[key](payload)
    }
    // 收集 actions
    let actions = {}

    Object.keys(options.actions).forEach(key => {
      actions[key] = payload => {
        options.actions[key](this, payload)
      }
    })
    this.dispatch = (key, payload) => {
      actions[key](payload)
    }
  }

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

const install = _Vue => {
  console.log("install")
  Vue = _Vue // 用一個變量接收 _Vue 構造器
  Vue.mixin({
    beforeCreate() {
      //判斷 根 實例 有木有傳入store 數據源,
      //如果傳入了, 就把它放到實例的 $store上
      if (this.$options && this.$options.store) {
        this.$store = this.$options.store
      } else {
        // 2. 子組件去取父級組件的$store屬性
        this.$store = this.$parent && this.$parent.$store
      }
    }
  })
}

export default {
  install,
  Store
}

剩下的抽離公共代碼, 就自己做吧, 比如那個 forEach

const forEach = (obj, cb) => {
  // 迭代對象的 會將對象的 key 和value 拿到
  Object.keys(obj).forEach(key => {
    cb(key, obj[key])
  })
}

vuex 的聲明週期同cookie session, 瀏覽器打開到瀏覽器關閉, 聲明週期結束, 所以一旦刷新頁面, 個別數據就會丟失

可以採用插件的方式進行存儲全部的數據

1. 自己寫

import Vue from "vue"
import Vuex from "vuex"
Vue.use(Vuex)

const myPlugin = store => {
  // 當 store 初始化後調用
  store.subscribe(state => {
    localStorage.setItem("vuex-state", JSON.stringify(state))
  })
}

export default new Vuex.Store({
  plugins: [myPlugin],
  state: {
    name: "我是原生的state數據",
    mill: "我是getters用的",
    num: 1
  },
  getters: {
    getMill(state) {
      return state.mill
    }
  },
  mutations: {
    addNum(state, num) {
      return (state.num += num)
    },
    plusNum(state, num) {
      return (state.num -= num)
    }
  },
  actions: {
    jianNum({ commit }, num) {
      setTimeout(() => {
        commit("plusNum", num)
      }, 1000)
    }
  },
  modules: {}
})

2. 使用插件 vuex-persistedstate

yarn add vuex-persistedstate

import Vue from "vue"
import Vuex from "vuex"
import persistedState from "vuex-persistedstate"
Vue.use(Vuex)

export default new Vuex.Store({
  plugins: [persistedState()],
  state: {
    name: "我是原生的state數據",
    mill: "我是getters用的",
    num: 1
  },
  getters: {
    getMill(state) {
      return state.mill
    }
  },
  mutations: {
    addNum(state, num) {
      return (state.num += num)
    },
    plusNum(state, num) {
      return (state.num -= num)
    }
  },
  actions: {
    jianNum({ commit }, num) {
      setTimeout(() => {
        commit("plusNum", num)
      }, 1000)
    }
  },
})

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章