手写Vuex源码

Vuex源码实现

1. Vuex核心概念State,Getters,Mutations, Actions, Modules

  • State 存放数据,(响应式:数据改变,视图也改变)

    age: 18

  • Getters 相当于Vue中计算属性,具有缓存机制

    changeAge:state=>{ return state.age + 2} // 将age+2

  • Mutations 修改数据(同步)

    addAge: (state, data) => { state.age += data }

    @click="$store.commit('addAge', 5)"

  • Actions 修改数据(异步)

    // actions:{}
    awaitAddAge: ({ commit }, data) => { 
        setTimeout(() => {
          commit('addAge', data)
        }, 2000);  
      }
    
    // 页面使用 延迟两秒执行
    @click="$store.dispatch('awaitAddAge', 2)"
    
  • Modules

Modules模块化可以理解为Store里嵌套Store

2. 手写Vuex分析

  • 分析:

    • Vuex首先用到了单例模式和发布订阅者模式两种设计思想

      • 单例模式,即Vuex的四个核心概念都在一个Store实例中完成
      • 发布订阅者模式:即mutations同步修改方法,和actions异步修改方法通过发布订阅者模式来实现state数据的修改。
    • Vuex是个插件,通过vue.use产生关联(一般通过vue.use注册插件)

        1. Vue.use(fnc) 执行方法

          function ac() { 
            console.log(1000);
          }
          Vue.use(ac) // 打印1000
          
        1. 如果这个方法中有一个install这个属性(方法),会执行install

          function ac() { 
            console.log(1000);
          }
          ac.install = function () {  
            console.log(300);
          }
          Vue.use(ac)
          
          Vue.use(Vuex); // 打印300,1000不打印
          
        1. 如果install有参数,第一个参数就是Vue的实例

          function ac() { 
            console.log(1000);
          }
          ac.install = function (_Vue) {  
            console.log(_Vue);
          }
          Vue.use(ac) // 打印出Vue的实例
          
    • 每一个使用的组件中都有store容器

    • Mutations通过Commit方法直接修改State

    • Actions通过Dispatch触发Commit执行Mutations方法

    • Vuex是个单线程的方法


      image.png
    • 写的每个方法要在每一个组件中使用,需要把方法放在根实例下。

    • 组件的渲染关系是父子关系

3. 创建Vuex入口文件

在src目录下创建Vuex文件夹并新建index.js(Vuex入口文件)(替换Vuex文件)

// 入口文件
import { Store, install } from './install'
export default {
  Store, // 容器
  install // 注册插件,与Vue产生关联
}
  • 创建install.js文件

    export class Store {
    
    }
    export const install = function () {  
      console.log(200);
    }
    
  • 替换官方Vuex为新建的Vuex插件,修改store/index.js

    // old 
    import Vuex from "vuex";
    // new 修改为
    import Vuex from "../Vuex"; // 自定义Vuex组件
    
  • 刷新页面,打印了200

    通过store/index.js Vue.use(Vuex) 调用install方法,执行install.js文件里的console.log(200);,打印出200

4. 将store放到每个实例上

获取Vue实例并把store放到每一个组件实例上

// 实现 store 放到每一使用的组件中
export const install = function (_Vue) {  
  Vue = _Vue // 获取Vue实例,以便使用Vue实例上的方法
  // 使用Vue提供的方法 Vue.mixin({})
  // Vue.mixin({})方法,混入方法和数据
  Vue.mixin({ // mixin 混入生命周期beforeCreate
    beforeCreate () {
      let options = this.$options
      if (options.store) { // 根实例,只有根实例才有store
        // 给根实例添加一个$store属性
        this.$store = options.store
      } else {   // 其他实例,除根实例外
        // 判断有没有父亲实例,如果有拿到父亲实例上的$store
        // 从根实例开始,逐步从根实例传到子组件实例上
        this.$store = this.$parent && this.$parent.$store
      }
      // console.log(this.$store); 
    }
  })
} 
  • 设置Store容器

    export class Store { // Store容器 
      constructor(options){ // options为用户配置项(包括state,getters,mutations等等)
        console.log(options);
      }
    }
    
  • 初始化Vuex.Store

    export default new Vuex.Store({
      state: state, // eslint-disable-next-line
      getters: getters,
      mutations: mutations,
      actions: actions,
      modules: {},
    });
    
  • 取值

    $store.state.age = 33 // 成功获取

  • 修改

    @click="$store.state.age = 66" // 页面未刷新,但是实例上age已变成66,也就是state.age未实现响应式。

5. 处理Vuex响应式

  • 处理Vuex响应式

    this.state = options.state 处理不能实现响应式

    处理Vuexstate响应式,用Vue的数据劫持来实现

    将state上的所有属性代理到_vm实例上, 然后在get时,返回代理的_vm实例

    export class Store { // Store容器 
      constructor(options){ // options为用户配置项(包括state,getters,mutations等等)
        // this.state = options.state // 不能实现响应式
        this._vm = new Vue({ // 通过Vue实例来对state进行代理
          data: { 
            state: options.state
          }
        })
        // Object.defineProperty() // Object.defineProperty方法可以在类里进行劫持
      }
      get state() {
        return this._vm.state // 返回实现代理的_vm实例
      }
    }
    

6. 处理计算属性getters

  • 获取options里的getters内容
  • 如果getters里有多个方法需遍历
  • 通过Object.defineProperty()方法对getters方法进行劫持
 // getters 计算属性,用户 
    let getters = options.getters
    this.getters = {}

    console.log(getters['getAge'](this.state));
    // 当getters里有多个方法时,对方法进行遍历
    Object.keys(getters).forEach(key => {
      // 如果this.getters没有getters方法,则进行添加
      Object.defineProperty(this.getters,key,{
        get: () => {
          console.log(getters[key]);
          console.log(this.state);
          // getters[key] 获取到到是getters里到方法,
          // 通过getters[key](this.state)传入this.state调用这个方法
          return getters[key](this.state) 
        }
      })
    })

7. 创建方法文件夹utils

  • 创建utils文件夹,并新建index.js文件

  • 新建第一个方法foreach

    export function foreach(obj, cb){
        Object.keys(obj).forEach(key => {
          cb(key, obj[key])
        })
    } 
    
  • 导入foreach方法实现getters

    // 调用foreach方法实现getters
        foreach(getters,(key, value) => {
          Object.defineProperty(this.getters,key,{
            get: () => {
              // console.log(getters[key]);
              // console.log(this.state);
              // getters[key] 获取到到是getters里到方法,
              // 通过getters[key](this.state)传入this.state调用这个方法
              return value(this.state) 
            }
          })
        })
    

8. 设置getters缓存机制

  • 通过Vue的计算属性来代理实现getters方法的缓存机制

  • 在Vue中,state响应式和getters缓存机制都是靠初始化一个Vue实例来实现,响应式通过Vue实例的data数据劫持来实现,getters缓存机制通过Vue实例的计算属性computed来实现。

    let computed = {} // 定义一个computed对象
    // 在foreach方法中添加computed方法
    computed[key] = () => {
        return value(this.state) 
    }
    // 通过Object.defineProperty()方法的get属性返回当前Vue实例_vm
    Object.defineProperty(this.getters,key,{
      get: () => {
        // console.log(getters[key]);
        // console.log(this.state);
        // getters[key] 获取到到是getters里到方法,
        // 通过getters[key](this.state)传入this.state调用这个方法
        console.log(this._vm);
        return this._vm[key] 
        // return value(this.state)
      }
    })
    
    // 在初始化Vue实例时,代理computed
    this._vm = new Vue({ // 通过Vue实例来对state进行劫持
      data: { 
        state: options.state
      },
      computed // ES6中属性和属性值相同可以直接省略  computed = computed:'computed'
    })
    

9. mutations方法实现

  • 首先明确知道 mutations和actions 是个发布订阅者模式

  • 在Store类的constructor构造方法里初始化mutations方法

    // 获取store里设置的mutations方法
        let mutations = options.mutations
        // 初始化一个mutations空对象
        this.mutations = {}
        // 遍历Store里设置的mutations方法
        foreach(mutations, (key, value) => {
          this.mutations[key] = (data) => {
            value(this.state, data)
          }
        })
    
  • 在Store类上通过commit方法调用mutations方法

    可以理解为通过commit方法专门来调用对应的mutations方法

    commit = (name, data) => { // this 永远指向store,当前的实例
       this.mutations[name](data)
    }
    

10. actions方法实现

  • actions方法与mutations方法基本一致,只有在Store类里设置actions方法时,传入方法的第一个参数是this,也就是当前Store这个类的实例。

  • 在Store类的constructor构造方法里初始化actions方法

    // 获取用户设置的acitons异步修改方法
        let actions = options.actions
        // 初始化一个空的actions对象
        this.actions = {}
        // 遍历用户设置的acitons方法并初始化执行方法
        foreach(actions, (key, value) => {
          this.actions[key] = (data) => {
            value(this, data) // 此处应传入this即Store类这个实例
          }
        })
    
  • 在Store类上通过dispatch方法调用actions方法

    // actions异步修改数据方法
      dispatch = (name, data) => {
        this.actions[name](data)
      }
    
  • dispatch方法和commit方法在使用时有差别

    // commit同步修改方法在使用时第一个参数直接传入当前Store的state
    addAge: (state, data) => { state.age += data }
    // dispatch异步修改方法在使用时第一个参数传入的是Store这个实例,dispatch在使用时会从Store实例中解构出commit同步修改方法
    awaitAddAge: ({ commit }, data) => { 
        setTimeout(() => {
          commit('addAge', data)
        }, 2000);  
    }
    

11. modules模块化实现

  • modules模块化处理需要格式化数据
  • 格式化数据 变成一个树形结构,也就是一个对象
  // 页面结构
  export default new Vuex.Store({
  state: state, // eslint-disable-next-line
  getters: getters,
  mutations: mutations,
  actions: actions,
  // 模块  {mutations: []}
  modules: {
    a: {
      state:{
        age: 200
      },
      mutations:{
        addAge: (state, data) => { state.age += data }
      }
    },
    b : {
      state:{
        age: 112
      },
      mutations:{
        addAge: (state, data) => { state.age += data }
      }
    }
  },
});
// 转换成的结构
root = {
  _raw: '用户传过来的数据',
  _children:{
    a: {
      _raw: 'a用户传过来的数据',
      _children:{

      },
      state: '数据'
    }
    b: {
      _raw: 'b用户传过来的数据',
      _children:{

      },
      state: '数据'
    }
  },
  state: '根数据'
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容