• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

编写vuex(万字核心原理)

武飞扬头像
溜达溜达哈哈哈
帮助2

一、简易版

1. 文件结构

vuex

  • index.js --- 功能汇总 合并导出

  • install.js --- 插件注册

  • store.js --- 核心功能

  • module --- 模块化核心

    • module-collection.js --- 模块收集
    • module.js --- module构造函数 用于创建module

store

  • index.js --- 1 Vue.use(Vuex) 2 new Vuex.store创建store实例

main.js

  • 引用store,注册到Vue实例上

util.js

  • 遍历方法,遍历对象中的属性,将属性和key传递给回调函数
export const mergeForEach = (obj, fn) => {
  if (typeof obj === 'object' && obj != null && typeof fn === 'function') {
    Object.keys(obj).forEach((key) => {
      fn(obj[key], key)
    })
  }
}

2. Vuex/index.js

import install from './install';
import Store from './store'

export default{
    install,
    Store,
    //...
}

3. install.js

export let Vue; // 同步导出一份
function install (_Vue) {
  Vue = _Vue;
  Vue.mixin({ // 借用Vue的mixin方法以及生命周期钩子,合并选项和初始化数据
    beforeCreate () {
      // 获取根组件上的store 将他共享给每个组件 每个组件中都应该有$store
      let options = this.$options;
      if (options.store) {
        this.$store = options.store
      } else {
        // 先保证他是一个子组件,并且父亲上有$store
        if (this.$parent && this.$parent.$store) {
          this.$store = this.$parent.$store
        }
      }
    }
  })
}
export default install

4. store.js

import { Vue } from './install'; // install时接收的vue,顺便导出一份,确保调用的是同一个

class Store {
  constructor(options) {
    // new Store 初始化时传入的数据选项
    const { state, mutations, actions, getters } = options
    this.getters = {} // 初始化getters
    this.mutations = {} // 初始化mutations
    this.actions = {} // 初始化actions
    const computed = {} // 初始化计算属性,用于关联getter

    // 合并getter数据
    mergeForEach(getters, (fn, key) => {
      // 将getters上的属性同步一份给computed,接收的state就是当前实例的state
      computed[key] = () => {
        return fn(this.state)
      }
      // 从getter上面取值的时候,劫持到computed上
      Object.defineProperty(this.getters, key, {
        get: () => this._vm[key] // 这里的this和vue实例是同一个
      })
    })

    // 合并mutations数据,订阅事件
    mergeForEach(mutations, (fn, key) => {
      this.mutations[key] = (payload) => {
        return fn.call(this, this.state, payload) // 确保this指向
      }
    })

    // 合并actions数据
    mergeForEach(actions, (fn, key) => {
      this.actions[key] = (payload) => {
        // call的第二个this是当前store实例,上面有commit方法 state等
        return fn.call(this, this, this.state)
      }
    })

    // 通过vue实例作为响应式桥梁,store数据变化后,能触发watcher更新视图
    this._vm = new Vue({
      data: {
        // $开头的属性会被vue内部合并在_vm上,表示内部属性
        $$state: state // state结束vue的响应式数据,访问state时指向$$state
      },
      computed // 让getter借助vue的计算属性watcher
    })
  }

  // 当访问store时
  get state () {
    return this._vm._data.$$state // 从vm上面取值
  }

  // commit方法 发布事件,(用箭头函数不用clll其this指向)
  commit = (type, payload) => {
    this.mutations[type](payload)
  }

  // 触发actions方法
  dispatch = (type, payload) => {
    this.actions[type](payload)
  }
}

export default Store

5. store/index.js

import Vue from 'vue'
import Vuex from '@/vuex' // 用自己的vuex
Vue.use(Vuex) // 注册插件 调用install方法

export default new Vuex.Store({ // vuex持久化插件?
  state: {
    name: 'zhufeng',
    age: 12
  },
  mutations: { // method  commit 同步更改状态
    changeAge (state, payload) {
      state.age  = payload
    }
  },
  actions: { // 异步操作 调用api接口 dispatch, 多次commit mutation  
    changeAge ({ commit }, payload) {
      setTimeout(() => {
        commit('changeAge', payload);
      }, 1000);
    }
  },
  getters: { // 计算属性
    myAge (state) {
      return state.age   10
    }
  },
})

6. main.js

import Vue from 'vue'
import App from './App.vue'
import store from './store'
let vm = new Vue({
  name: 'root',
  store, // 此store的目的是让所有组件都能访问到store对象
  render: h => h(App)
}).$mount('#app')

二、完整版

1. ModuleCollection & Module

ModuleCollection --- 将modules转换为tree

Module --- module构造类

Store
class Store {
  constructor(options) {
    // 对用户的模块进行整合  当前格式化完毕的数据 放到了this._modules里
    this._modules = new ModuleCollection(options); // 对用户的参数进行格式化操作
    // ……
  }
}
ModuleCollection
import { mergeForEach } from '../util'
import Module from './module';
class ModuleCollection {
  constructor(options) {
    this.root = null; // 对数据进行格式化操作 初始化根节点
    this.register([], options); // [] 为了记录父子关系 用队列方式
  }
  register (path, rawModule) {
    let newModule = new Module(rawModule) // 创建module实例,包含_raw  _children state等
    if (path.length == 0) {
      this.root = newModule // 判断为根节点
    } else {
      // 根据当前注册的key ,将他注册到对应的模块的儿子处
      let parent = path.slice(0, -1).reduce((memo, current) => {
        return memo.getChild(current)
      }, this.root);
      parent.addChild(path[path.length - 1], newModule);
    }
    // 注册完毕当前模块,在进行注册根模块 
    if (rawModule.modules) {
      mergeForEach(rawModule.modules, (module, key) => {
        this.register(path.concat(key), module);
      })
    }
  }
}

export default ModuleCollection
Module
import { mergeForEach } from '../util'
class Module {
  constructor(rawModule) {
    this._raw = rawModule; // 初始化raw
    this._children = {}; // 初始化children
    this.state = rawModule.state // 初始化state
  }
  // 根据name获取children
  getChild (childName) {
    return this._children[childName]
  }
  // 添加children
  addChild (childName, module) {
    this._children[childName] = module
  }
  // 遍历getters
  forEachGetter (cb) {
    this._raw.getters && mergeForEach(this._raw.getters, cb)
  }
  // 遍历Mutations
  forEachMutation (cb) {
    this._raw.mutations && mergeForEach(this._raw.mutations, cb)
  }
  // 遍历actions
  forEachAction (cb) {
    this._raw.actions && mergeForEach(this._raw.actions, cb)
  }
  // 遍历childrens
  forEachChildren (cb) {
    this._children && mergeForEach(this._children, cb)
  }
  // 用于标识他自己是否写了namesapced
  get namespaced () {
    return !!this._raw.namespaced // module.namespaced直接走这里
  }
}

export default Module

2. installModule

根据root中的module初始化options,installModule

Store中
constructor(options) {
    // ……
    // 此时_module中有root树,包含_raw _child state, _raw中包含了所有属性
    this.wrapperGetters = {} // 关联module的_raw.getter
    this.mutations = {}; // 初始化mutations
    this.actions = {}; // 初始化actions
    this._subscribes = []; // 订阅事件
    this._committing = false; // 判断是否在mutation中更改state,默认false
    this.strict = options.strict; // 开启严格模式,此时如果直接更改state需要抛出错误
    let state = options.state; // 初始化状态
    installModule(this, state, [], this._modules.root);
    resetVM(this, state); // 每次需要重置借用的Vue数据劫持和getter 确保新增的属性有效
    // ……
}
getNewState
// 获取最新的state 可能replaceState将数据变了,但store还是绑定的之前的数据
function getNewState (store, path) {
  // 通过pact获取最新state 有可能数据变了 但是 'a/b/name' 无法取到
  return path.reduce((memo, current) => {
    return memo[current];
  }, store.state)
}
store._withCommittting
// 将每个mutation都用该函数包裹,只要通过mutation修改state就一定会触发该函数
// 然后将_committing改为true,再执行mutation,执行完重置,这样就能监听到是否通过mutation修改state
_withCommittting (fn) {
    this._committing = true; // 如果true
    fn(); // 函数是同步的 获取_commiting 就是true,如果是异步的那么就会变成false 就会打印日志
    this._committing = false;
 }
installModule
function installModule (store, rootState, path, module) {
  // 没有namespace的时候 getters都放在根上,actions,mutations 会被合并数组
  let ns = store._modules.getNamespace(path); // _modules == moduleCollection类的实例
  // module.state => 放到rootState对应的儿子里
  if (path.length > 0) {
    //  儿子模块  需要找到对应父模块,将状态声明上去
    let parent = path.slice(0, -1).reduce((memo, current) => {
      return memo[current];
    }, rootState);
    // _withCommittting函数用于配合判断是否在mutation中更改state
    store._withCommittting(() => {
      // 对象新增属性不能导致重新更新视图,需要借用Vue的数据劫持
      Vue.set(parent, path[path.length - 1], module.state);
    })
  }
  // 初始化getters
  module.forEachGetter((fn, key) => {
    key = ns   key // namespace为true时,命名空间成了 a/b/name,需用相应的格式访问
    store.wrapperGetters[key] = function () {
      // 调用getter,改变this指向,向其传递最新的数据
      return fn.call(store, getNewState(store, path));
    }
  });
  // 初始化mutations
  module.forEachMutation((fn, key) => {
    key = ns   key // namespace为true时,命名空间成了 a/b/name,需用相应的格式访问
    store.mutations[key] = store.mutations[key] || []; // 如果不是数组则需要转为数组
    store.mutations[key].push((payload) => {
      store._withCommittting(() => {
        fn.call(store, getNewState(store, path), payload); // 调用mutations
      })
      // 先调用mutation 在执行subscirbe
      store._subscribes.forEach(fn => fn({ type: key, payload }, store.state));
    })
  });
  // 初始化actions
  module.forEachAction((fn, key) => {
    key = ns   key // namespace为true时,命名空间成了 a/b/name,需用相应的格式访问
    store.actions[key] = store.actions[key] || [];
    store.actions[key].push((payload) => {
      return fn.call(store, store, payload) // 调用actions
    })
  });
  // 初始化children
  module.forEachChildren((child, key) => {
    installModule(store, rootState, path.concat(key), child);
  });
}
reaetVM
function resetVM (store, state) {
  let oldVm = store._vm; // 保存一份旧的实例 新的绑定成功后需要把旧的销毁
  store.getters = {}; // store的getters,关联vm上的computed
  const computed = {}; // vm的计算属性,这里需要借用,以实现响应式数据
  // installModule时将getters都绑定在了warpperGetter上,这里需要同步一份给computed
  forEach(store.wrapperGetters, (getter, key) => {
    computed[key] = getter; // 同步一份给computed
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key] // 访问store.getters时,代理到vm的computed
    })
  });
  // 借用Vue实例,实现响应式
  store._vm = new Vue({
    data: {
      $$state: state // $开头的属性会被Vue识别为内部属性,只能通过vm访问
    },
    computed // 同步了getters的计算属性,访问getters时 其实就在访问computed
  });
  if (store.strict) { // 说明是严格模式我要监控状态
    // 通过Vue的$watch监听$$state变化,$$state就是store的state
    store._vm.$watch(() => store._vm._data.$$state, () => {
      console.assert(store._committing, 'no mutate in mutation handler outside')
      // 我希望状态变化后直接就能监控到,这里需要同步执行才能获取
      // 而watcher都是异步的,所以sync需设置为true,同时层级问题需要deep递归
    }, { deep: true, sync: true });
  }
  if (oldVm) { // 重新创建实例后,需要将老的实例卸载掉
    Vue.nextTick(() => oldVm.$destroy())
  }
}

3. namespaced

namespaced 能解决子模块和父模块的命名冲突文件 ,相当于增加了一个命名空间

如果没有namespaced 默认getters都会被定义到父模块上

mutations 会被合并在一起, 最终一起调用,有了命名空间就没有这个问题了

需要注意的是,子模块的名字不能和父模块中的状态重名,否则会覆盖

使用时 --- 入口
new Vuex.Store({
  namespaced: true, // 开启命名空间
  state: {},
  mutations: {}
})
ModuleCollection类中
class ModuleCollection {
  constructor(options) {
    // ……
    this.register([], options); // [] 为了记录父子关系 用队列方式
  }
  register (path, rawModule) {
    // 传入的rawModule就是用户的options
    let newModule = new Module(rawModule) // 创建module实例,包含_raw  _children state等
    // ……
  }
}
Module类中
class Module {
  constructor(rawModule) {
    this._raw = rawModule; // 传入的options
    // ……
  }
  // ……
  // 用于标识他自己是否写了namesapced
  get namespaced () {
    return !!this._raw.namespaced // module.namespaced直接走这里
  }
}
getNamespace
// ModuleCollection的原型方法
getNamespace (path) { // 返回一个字符串 a/b/c   ''
    let root = this.root
    let ns = path.reduce((ns, key) => { // this.root.c.namespace
      let module = root.getChild(key); // module上的getChild方法,返回匹配的儿子
      root = module; // 重置root 用于下一次迭代继续查找儿子
      // namespaced则开启了命名空间,需用 obj['a/b/name'] 方式访问
      return module.namespaced ? ns   key   '/' : ns
    }, '');
    return ns;
}
使用命名空间前缀key
function installModule (store, rootState, path, module) {
  let ns = store._modules.getNamespace(path); // _modules == moduleCollection类的实例
  // ……
  module.forEachGetter((fn, key) => {
    key = ns   key // namespace为true时,命名空间成了 a/b/name,需用相应的格式访问
    store.wrapperGetters[key] = function () {
      // 调用getter,改变this指向,向其传递最新的数据
      return fn.call(store, getNewState(store, path));
    }
  });
  // ……
}

4. logger

plugin插件 --- 通过plugins注入一个函数,该函数中会接收store实例,基于store实例进行扩展

依赖方法 store.subscribe,每次通过mutations修改state时都会调用该方法

使用时 --- 入口
new Vuex.Store({
  plugins: [
    logger() // 使用日志插件
  ],
  state: {},
  mutations: {}
})
store.subscribe
subscribe (fn) {
    this._subscribes.push(fn); // 收集订阅事件
}
初始化mutations时
module.forEachMutation((fn, key) => {
    key = ns   key // namespace为true时,命名空间成了 a/b/name,需用相应的格式访问
    store.mutations[key] = store.mutations[key] || []; // 如果不是数组则需要转为数组
    store.mutations[key].push((payload) => {
      store._withCommittting(() => {
        fn.call(store, getNewState(store, path), payload); // 调用mutations
      })
      // 先调用mutation 在执行subscirbe --- 调用完mutation后 在这里调用subscribe
      store._subscribes.forEach(fn => fn({ type: key, payload }, store.state));
    })
});
logger
function logger () {
  return function (store) {
    // 克隆一份,否则打印的是同一个,可以用lodash的deepClone
    let prevState = JSON.stringify(store.state);
    // 所有的更新操作都基于mutation (状态变化都是通过mutation的)
    store.subscribe((mutation, state) => { 
      // 如果直接手动的更改状态 此scbscribe是不会执行 commit()
      console.log('prevState:'   prevState); // 旧的
      console.log('mutation:'   JSON.stringify(mutation)); // 执行的哪个
      console.log('currentState:'   JSON.stringify(state)); // 新的
      prevState = JSON.stringify(state); // 重置旧的
    })
  }
}

5. persistent 依赖方法 replaceState

插件,持久化,核心是使用浏览器缓存 store.reolaceState

使用时 --- 入口
new Vuex.Store({
  plugins: [
    persists() // 使用持久化插件
  ],
  state: {},
  mutations: {}
})
store.replaceState
replaceState (newState) { // 需要替换的状态
    this._withCommittting(() => {
      this._vm._data.$$state = newState; // 替换最新的状态, 赋予对象类型会被重新劫持
    })
     // 虽然替换了状态,但是mutation getter中的state在初始化的时候 已经被绑定死了老的状态
     // 所以需要用到getNewState方法,从vm上获取最新的数据,对store进行绑定
}
presisted --- 插件核心
function persists () {
  return function (store) { // vuex-persists
    let localState = JSON.parse(localStorage.getItem('VUEX:STATE'))
    if (localState) {
      store.replaceState(localState); // 替换state
    }
    // 和mutation挂钩 状态变化时 需要更新浏览器缓存,触发mutation就一定会触发subscribe
    store.subscribe((mutation, rootState) => {
      // 状态发生变化就存localStorage中,这里可以用队列 nextTick做防抖处理
      localStorage.setItem('VUEX:STATE', JSON.stringify(rootState));
    });
  }
}

6. registerModule

store原型方法,动态插入一个新的store-module

使用时 --- 入口
store.registerModule('a/newModule', {
  state: {},
  mutations: {}
})
store.registerModule核心方法
registerModule = (path, module) => {
    if (typeof path == 'string') path = [path]; // 确保最终都转换成数组 register(['a','c'])
    this._modules.register(path, module); // 模块的注册, 将用户给的数据放到树中
    // 注册完毕后 将用户的module 重新安装
    installModule(this, this.state, path, module.newModule);
    // vuex内部重新注册的话 会重新生成实例, 虽然重新安装了 ,只解决了状态的问题,但是computed就丢失了
    resetVM(this, this.state); // 销毁重来
}

7. strict

开启严格模式,执行一些限制规则

例如只能通过mutations修改state,否则抛出错误

使用时 --- 入口
new Vuex.Store({
  strict: true,
  state: {}
})
Store构造函数中初始化状态
class Store{
    constructor(options) {
        this.strict = options.strict; // 开启严格模式,此时如果直接更改state需要抛出错误
        // ……
    }
}
vm.$watch

resetVM方法中,借用vm.$watch监听store关联的vm实例数据$$store

数据变化时,如果_committing状态为false则说明其没有通过mutations修改state,此时抛出错误

关于_committing可回顾前面的代码


function resetVM (store, state) {
  if (store.strict) { // 说明是严格模式我要监控状态
    // 通过Vue的$watch监听$$state变化,$$state就是store的state
    store._vm.$watch(() => store._vm._data.$$state, () => {
      // console.assert可查阅MDN,当第一个参数为false时打印第二个参数内容
      console.assert(store._committing, 'no mutate in mutation handler outside')
      // 我希望状态变化后直接就能监控到,这里需要同步执行才能获取
      // 而watcher都是异步的,所以sync需设置为true,同时层级问题需要deep递归
    }, { deep: true, sync: true });
  }
}

8. 辅助函数

mapMutations mapActions mapGetters mapState

可通过...mapActions的方式结构出来使用

mapState
export function mapState (stateList) {
  let obj = {};
  for (let i = 0; i < stateList.length; i  ) {
    let stateName = stateList[i];
    obj[stateName] = function () {
      return this.$store.state[stateName];
    };
  }
  return obj;
}
mapMutations
export function mapMutations (mutationList) {
  let obj = {};
  for (let i = 0; i < mutationList.length; i  ) {
    obj[mutationList[i]] = function (payload) {
      this.$store.commit(mutationList[i], payload);
    };
  }
  return obj;
}
mapActions
export function mapActions (actionList) {
  let obj = {};
  for (let i = 0; i < actionList.length; i  ) {
    obj[actionList[i]] = function (payload) {
      this.$store.dispatch(actionList[i], payload);
    };
  }
  return obj;
}
mapGetters
export function mapGetters (gettersList) {
  let obj = {};
  for (let i = 0; i < gettersList.length; i  ) {
    let getterName = gettersList[i];
    obj[getterName] = function () {
      return this.$store.getters[getterName];
    };
  }
  return obj;
}

三、总结

  • 核心:install Store ModuleCollection Module installModule resetVm
  • 辅助方法:getNewState mergeForEach
  • 插件方法:logger presisted
  • 辅助函数:mapState mapGetters mapMutations mapActions

install

核心原理:

借用Vue的mixin混入生命周期钩子beforeCreate

在该钩子中将传入的store挂载到this.store上,如果是子组件就取父组件的store上,如果是子组件就取父组件的store

Store

核心原理:

初始化状态,解析用户传入的modulds,调用installModule方法对每一个module进行安装

借用Vue的数据劫持和computed,实现state和getters,通过resetVm方法将数据关联起来

提供几个核心方法:

reolaceState 整体替换state,直接改写借用的vm._data.$$state

commit 遍历执行mutations

dispatch 遍历执行actions

registerModule 注入一个新的module,核心是利用_module.register合并module,然后installModule安装,最后再用resetVM重置getters和劫持的数据

_withCommitting 用于切片mutations,调用mutations时对状态进行监听

subscribe 订阅执行发布的事件,用于扩展插件使用

ModuleCollection

核心原理:

通过register方法递归合并一个module树,module树由Module构造类产生

额外提供一个可返回命名空间字符串key的方法 getNamespace

Module

核心原理:

将用户传入的options转变为children state _raw格式的数据

额外提供一些遍历options上的getters、actions等的方法

installModule

核心原理:

递归解析store._module,进行安装,借助module提供的遍历方法,向store中赋值

resetVm

核心原理:

借助Vue的响应式数据实现state,computed实现getters

借助$watch监听state变化,执行严格模式下的错误收集

初始化完成后,借助vue生命周期钩子销毁实例

另外还有 createNamespacedHelpers方法 用于寻找命名空间中的map

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgbaijc
系列文章
更多 icon
同类精品
更多 icon
继续加载