欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

手写vuex及原理解析

程序员文章站 2022-03-06 08:09:20
...

参考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下目录

手写vuex及原理解析

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>

渲染展示

手写vuex及原理解析

分析:首先组件都有一个 $store 的属性, 然后可以拿到 new Vuex.store({}) 方法传入的这个对象的数据

 

手写vuex及原理解析

分析上边的代码: 使用 了 Vue.use() 方法,官方文档是这样解释的, 可以看到使用 use() 方法,必须提供一个 install 方法

该方法 有两个 参数, 一个 是 Vue 的构造器, 另一个是可选项

手写vuex及原理解析

 

手写vuex及原理解析

好了, 接着 手写 自己的vuex 实现state , 再src 目录下创建 vuex.js 文件,分个屏展示一下,然后把引入的vuex文件换成自己的文件

手写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
}

可以看到已经可以打印出来了

手写vuex及原理解析

 

由于,数据是共享的, 所以每个组件都需要拿到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
}

手写vuex及原理解析

 

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 等可选属性

手写vuex及原理解析

所以  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 类 的 代码

手写vuex及原理解析

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及原理解析

手写 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>

渲染为:

手写vuex及原理解析

 

我们再组件中 打印 this.$store.getters, 可以发现是一个对象

手写vuex及原理解析

而传入的时候再 getters 里 是个方法, 所以 需要把对象 的方法,变成 对象的属性返回, 并接受一个state 参数

手写vuex及原理解析

好了, 接着来写自己的 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)
        }
      })
    })
  }
}

手写vuex及原理解析

刷新页面 

手写vuex及原理解析

到这里基本实现了 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文档

手写vuex及原理解析

代码改写为下边:

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 方法 , 进行修改数据数据,先看原生的

手写vuex及原理解析

$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 的提交)

手写vuex及原理解析

 

实现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)
    }
  },
})

手写vuex及原理解析

相关标签: vuex原理