Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vuex 源码探究 #27

Open
XinChou16 opened this issue Jan 5, 2019 · 0 comments
Open

Vuex 源码探究 #27

XinChou16 opened this issue Jan 5, 2019 · 0 comments

Comments

@XinChou16
Copy link
Owner

XinChou16 commented Jan 5, 2019

全文基于vuex 3.0.1版本源码进行解析

entry

Vuex的入口在src的index文件下

当我们使用Vuex插件的时候,我们会在实例创建前这样调用Vue.use(vuex),其内部实际上是调用了install方法

// src/index.js
export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers
}

install方法是这样的,传入了Vue的引用,然后调用了applyMixin方法

// src/store.js
export 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)
}

mixin.js里定义了applyMixin方法,主要作用是在Vue的生命周期中的初始化(1.0是init,2.0是beforeCreated)钩子前插入Vuex的初始化代码。

这个方法给Vue的实例注入了$store属性,方便我们在Vue组件里通过this.$store访问到Vuex的各种数据和状态,当然也包括各个子模块下的数据。

// src/mixin.js
export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

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

初识Store构造函数

当我们使用Vuex的时候,如new Vuex.Store()的时候,会传入一个对象,对象上有state、 getters、actions等等,实例化的时候,内部到底做了什么事情,这是我们比较关心的,我们来看下内部Store类的构成。

// src/store.js
class Store {
  constructor (options = {}) {
    // Auto install if it is not done yet and `window` has `Vue`.
    // To allow users to avoid auto-installation in some cases,
    // this code should be placed here. See #731
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }

    if (process.env.NODE_ENV !== 'production') {
      assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
      assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
      assert(this instanceof Store, `store must be called with the new operator.`)
    }

    const {
      plugins = [],
      strict = false
    } = options

    // store internal state
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue()

    // bind commit and dispatch to self
    const store = this
    const { dispatch, commit } = this
    this.dispatch = function boundDispatch (type, payload) {
      return dispatch.call(store, type, payload)
    }
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
    }

    // strict mode
    this.strict = strict

    const state = this._modules.root.state

    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    installModule(this, state, [], this._modules.root)

    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    resetStoreVM(this, state)

    // apply plugins
    plugins.forEach(plugin => plugin(this))

    const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
    if (useDevtools) {
      devtoolPlugin(this)
    }
  }
  1. 开发模式下的assert
  • Vuex源码依赖于Promise,主要是dispatch进行异步操作,分发action,会返回一个Promise
  • 在不支持Promsie浏览器中,如IE,需要手动引入import 'babel-polyfill
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)

assert实现也很简单

// src/utils.js
export function assert (condition, msg) {
  if (!condition) throw new Error(`[vuex] ${msg}`)
}
  1. 初始化数据
const {
  plugins = [],
  strict = false
} = options

使用ES6的解构拿到plugins, 默认不开启严格模式,你也可以像这样,在开发模式下开启严格模式,任何手动改变state都会发出警告

const strict = process && process.env && process.env.NODE_ENV === 'development'
new Vuex.Store({
  strict
  // ...
})
  1. 内部属性的初始化
// store internal state
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()

this._committing标志提交状态,确保 state 的改变只在 mutations 的回调函数当中
this._actions 存储用户初始化实例传入的所有 actions
this._mutations 存储用户初始化实例传入的所有 mutations
this._wrappedGetters存储用户初始化实例传入的所有 getters
this._modules 用来存储所有运行时的modules
this._subscribers_ 用来存储所有对mutation变化的订阅者
this._watcherVM 是一个Vue对象实例,利用Vue实例方法$watch来检测变化

  1. 绑定 commit dispatch到实例上

把 Store 上的 commit 和 dispatch 方法绑定到当前 store 实例上,主要通过 call 方法改变 this 指向来实现

// bind commit and dispatch to self
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
  return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
  return commit.call(store, type, payload, options)
}

Vue 初始化核心

  1. installModule

这段代码是初始化的核心,通过 options 上的 modules, 分别注册和安装。

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)

// apply plugins
plugins.forEach(plugin => plugin(this))

installModule 接收5个参数,各个参数分别表示:

  • store 表示当前 Store 实例
  • rootState 表示根 state
  • path 表示当前嵌套模块的路径数组
  • module 表示当前安装的模块
  • hot 表示热更新的时候为 true
function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })
  }

  const local = module.context = makeLocalContext(store, namespace, path)

  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

我们在调用这个函数的时候传入了 installModule(this, state, [], options),对应的 options 传给了 module, options 可以拿到 state、actions 已经嵌套的 modules

  1. registerAction

registerAction 是对 store 中 action 的初始化,参数与 registerMutation 一致,不同的是 mutation 是同步修改当前模块的 state, action 是异步改变 state, 仍然需要提交 mutation 去改变 state, 只是延迟提交而已

function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(function wrappedActionHandler (payload, cb) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload, cb)
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

isPromise 检查是否为 Promise 对象

export function isPromise (val) {
  return val && typeof val.then === 'function'
}

一个例子理解 action

定义一个 action, 提交 commit, 在成功或失败,提交不同的 mutation

actions: {
  checkout ({ commit, state }, payload) {
    // 把当前购物车的商品备份起来
    const savedCartItems = [...state.cart.added]
    // 发送结帐请求,并愉快地清空购物车
    commit(types.CHECKOUT_REQUEST)
    // 购物 API 接收一个成功回调和一个失败回调
    shop.buyProducts(
      products,
      // 成功操作
      () => commit(types.CHECKOUT_SUCCESS),
      // 失败操作
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
    )
  }
}

action 的调用是通过 dispatch 来实现的

type 表示 action 的类型, payload 表示额外的参数

// src/store.js#116
dispatch (_type, _payload) {
  // check object-style dispatch
  const {
    type,
    payload
  } = unifyObjectStyle(_type, _payload)

  const action = { type, payload }
  const entry = this._actions[type]
  if (!entry) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] unknown action type: ${type}`)
    }
    return
  }

  this._actionSubscribers
    .filter(sub => sub.before)
    .forEach(sub => sub.before(action, this.state))

  const result = entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)

  result.then(() => this._actionSubscribers
    .filter(sub => sub.after)
    .forEach(sub => sub.after(action, this.state)))

  return result
}

mapActions

function normalizeMap (map) {
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}

var a = normalizeMap(['ded', 122, true])
a

Reference

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant