从未看过源码,到底该如何入手?分享一次完整的源码阅读过程(四)

简介: 接上文。

4. install 注册


Store 类的所有实现都了解完了,再来看一下入口文件里还有什么,突然发现忘记看一下非常重要的 install 方法了,根据 install 方法的导入路径找到相应的函数:


// 提供install方法
export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    if (__DEV__) {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}


当我们调用 Vue.use(vuex) 时,调用这个方法,先判断 vuex 是否已被注册,若已被注册,则不执行任何操作 ; 若没有被注册,则调用 applyMixin 方法,现在移步到 ./mixin.js 文件:


export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])
  // 2.x版本直接通过全局混入Vue.mixin的方式挂载store
  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // 兼容1.x版本
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }
  // 将vuex混入到$options中
  function vuexInit () {
    // 获取当前组件的 $options
    const options = this.$options
    // 若当前组件的$options上已存在store,则将$options.store赋值给this.$store(一般是用于根组件的)
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } 
    // 当前组件的$options上没有store,则获取父组件上的$store,即$options.parent.$store,并将其赋值给this.$store(一般用于子组件)
    else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}


applyMixin 方法先判断了 Vue 的版本号,主要做的是一个向下兼容 Vue 1.x 的版本,这里我对 Vue 1.x 的版本不太熟悉,所以就直接看 Vue 2.x 版本的处理方式吧


通过 Vue.minxin 方法做了一个全局的混入,在每个组件 beforeCreate 生命周期时会调用 vuexInit 方法,该方法处理得非常巧妙,首先获取当前组件的 $options ,判断当前组件的 $options 上是否有 sotre ,若有则将 store 赋值给当前组件,即 this.$store ,这个一般是判断根组件的,因为只有在初始化 Vue 实例的时候我们才手动传入了 store ; 若 $options 上没有 store ,则代表当前不是根组件,所以我们就去父组件上获取,并赋值给当前组件,即当前组件也可以通过 this.$store 访问到 store 实例了


这里不得不感叹,这个处理方式太棒了。


5. 辅助函数


store实例生成并且也 installVue 上了,看一下入口文件中只剩下辅助函数了,它们有 mapStatemapGettersmapMutationsmapActionscreateNamespacedHelpers ,进到相应的文件 ./helpers.js 中看一下


import { isObject } from './util.js'
export const mapState = normalizeNamespace((namespace, states) => {
  const res = {}
  if (__DEV__ && !isValidMap(states)) {
    console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(states).forEach(({ key, val }) => {
    res[key] = function mappedState () {
      let state = this.$store.state
      let getters = this.$store.getters
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapState', namespace)
        if (!module) {
          return
        }
        state = module.context.state
        getters = module.context.getters
      }
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})
export const mapMutations = normalizeNamespace((namespace, mutations) => {
  const res = {}
  if (__DEV__ && !isValidMap(mutations)) {
    console.error('[vuex] mapMutations: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(mutations).forEach(({ key, val }) => {
    res[key] = function mappedMutation (...args) {
      // Get the commit method from store
      let commit = this.$store.commit
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
        if (!module) {
          return
        }
        commit = module.context.commit
      }
      return typeof val === 'function'
        ? val.apply(this, [commit].concat(args))
        : commit.apply(this.$store, [val].concat(args))
    }
  })
  return res
})
export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  if (__DEV__ && !isValidMap(getters)) {
    console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(getters).forEach(({ key, val }) => {
    // The namespace has been mutated by normalizeNamespace
    val = namespace + val
    res[key] = function mappedGetter () {
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      if (__DEV__ && !(val in this.$store.getters)) {
        console.error(`[vuex] unknown getter: ${val}`)
        return
      }
      return this.$store.getters[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})
export const mapActions = normalizeNamespace((namespace, actions) => {
  const res = {}
  if (__DEV__ && !isValidMap(actions)) {
    console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(actions).forEach(({ key, val }) => {
    res[key] = function mappedAction (...args) {
      // get dispatch function from store
      let dispatch = this.$store.dispatch
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
        if (!module) {
          return
        }
        dispatch = module.context.dispatch
      }
      return typeof val === 'function'
        ? val.apply(this, [dispatch].concat(args))
        : dispatch.apply(this.$store, [val].concat(args))
    }
  })
  return res
})
/**
 * Rebinding namespace param for mapXXX function in special scoped, and return them by simple object
 * @param {String} namespace
 * @return {Object}
 */
export const createNamespacedHelpers = (namespace) => ({
  mapState: mapState.bind(null, namespace),
  mapGetters: mapGetters.bind(null, namespace),
  mapMutations: mapMutations.bind(null, namespace),
  mapActions: mapActions.bind(null, namespace)
})
function normalizeMap (map) {
  if (!isValidMap(map)) {
    return []
  }
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}
function isValidMap (map) {
  return Array.isArray(map) || isObject(map)
}
function normalizeNamespace (fn) {
  return (namespace, map) => {
    if (typeof namespace !== 'string') {
      map = namespace
      namespace = ''
    } 
    else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/'
    }
    return fn(namespace, map)
  }
}
function getModuleByNamespace (store, helper, namespace) {
  const module = store._modulesNamespaceMap[namespace]
  if (__DEV__ && !module) {
    console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
  }
  return module
}


整个文件里东西非常多,但我们很明确地知道,我们主要看的就是那几个辅助函数,观察发现,每个辅助函数都会先调用 normalizeNamespace 函数进行处理,那么我们就先看看这个函数做了什么:


function normalizeNamespace (fn) {
  return (namespace, map) => {
    if (typeof namespace !== 'string') {
      map = namespace
      namespace = ''
    } 
    else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/'
    }
    return fn(namespace, map)
  }
}


根据函数名的字面意思知道这应该是根据不同的调用方法,标准化命名空间的。


首先返回一个函数,接收两个参数,即 namespacemap ,这也是我们调用辅助函数时可以传入的两个参数 ;


然后判断 namespace 是否为字符串形式,若不是字符串,则表示是普通的调用方式,例如:


mapMutations(['first/second/foo', 'first/second/bar'])
mapMutations({
   foo: 'first/second/foo',
   bar: 'first/second/bar',
})


这种情况,就直接将第一个参数 namespace 赋值给映射变量 map ,而 namespace 设为空


若是字符串的话,则表示调用的是带命名空间的绑定函数的,例如:


mapState('first/second', ['foo', 'bar'])
mapState('first/second', {
  foo: 'foo',
  bar: 'bar',
})


处理好这两种不同的调用方式以后,调用一下 fn ,并将 namespacemap 作为参数


那么就先从 mapState 开始看吧


5.1 mapState


export const mapState = normalizeNamespace((namespace, states) => {
  const res = {}
  if (__DEV__ && !isValidMap(states)) {
    console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(states).forEach(({ key, val }) => {
    res[key] = function mappedState () {
      let state = this.$store.state
      let getters = this.$store.getters
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapState', namespace)
        if (!module) {
          return
        }
        state = module.context.state
        getters = module.context.getters
      }
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})


这里的 namespace 是一个字符串,states 是我们刚才处理好的映射变量 map

首先创建一个空对象 res ,这是我们最后处理好要返回的变量 ;


然后通过 isValidMap 方法判断 map 是否符合要求,即是否是数组或对象 ;


再然后调用了 normalizeMap 方法处理了变量 states ,从字面意义上来看,这是用来标准化该变量的,因为毕竟有可能是数组又有可能是对象嘛,所以要统一一下。


来看一下 normalizeMap 方法的实现:


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


首先仍然要先判断 map 是否合法,若不合法,则返回空数组,避免后续的代码报错 ;

然后判断 map 是否为数组,若是数组,则遍历 map 进行处理:


将 [1, 2, 3] 变成 [{key: 1, val: 1}, {key: 2, val: 2}, {key: 3, val: 3}]


map 不是数组,则一定为对象,那么同样也要把其处理成跟上面一样的格式:


将 {a: 1, b: 2, c: 3} 变成 [{key: a, val: 1}, {key: b, val: 2}, {key: c, val: 3}]


处理好了以后就直接返回,在得到标准化以后的 map 后要对其进行 forEach 遍历,将遍历到的每一个对象经过处理后存放在 res 中,即 res[key] = function mappedState() {...} ,来看一下这个 mappedState 里做了什么处理


首先获取一下根模块上的 stategetters


// 获取根模块的 state 、getters
let state = this.$store.state
let getters = this.$store.getters


然后判断是否存在命名空间,即 namespace 是否为空,若为空,则不做任何处理 ; 否则调用 getModuleByNamespace 方法获取到 namespace 对应的模块 module


function getModuleByNamespace (store, helper, namespace) {
  const module = store._modulesNamespaceMap[namespace]
  if (__DEV__ && !module) {
    console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
  }
  return module
}


可以看到 store._modulesNamespaceMap 终于派上了用场,在生成 Store 实例注册所有模块的时候,将带有命名空间的模块都存储在了该变量上,原来是在这里用上了


然后将刚才声明的变量 stategetters 替换成 module 对应上下文中的 stategetters


if (namespace) {
  // 获取命名空间namespace对应的模块
  const module = getModuleByNamespace(this.$store, 'mapState', namespace)
  if (!module) {
    return
  }
  // 将 state 、getters 变成该模块上下文中的 state 、getters
  state = module.context.state
  getters = module.context.getters
}


这个 context 也是非常的巧妙,在注册模块的时候,获取到该模块的上下文的同时,还将其存储了一下,即:


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


之前看到的时候不知道有啥用,但在这里看到后,觉得真的非常得赞 👍


确定好了 stategetters 的值,最后就可以返回值了


return typeof val === 'function'
  ? val.call(this, state, getters)
 : state[val]


这里还做了一层处理是因为要处理两种不同的方式,例如:


mapState({
  foo: state => state.foo,
  bar: 'bar'
})


在这里我又发现了一个官方文档里没有提及的,就是以函数形式返回的时候,还能接收第二个参数 getters ,即:foo: (state, getters) => state.foo + getters.bar

相关文章
|
4月前
|
存储 前端开发 JavaScript
PixiJS源码分析系列: 第一章 从最简单的例子入手
PixiJS源码分析系列: 第一章 从最简单的例子入手
|
7月前
|
测试技术
面试题8: 如何确定测试需求的关键场景和细节?
面试题8: 如何确定测试需求的关键场景和细节?
|
前端开发 NoSQL 数据库
项目重点知识点详解
项目重点知识点详解
|
设计模式 存储 安全
Java的第十三篇文章——JAVA多线程(后期再学一遍)
Java的第十三篇文章——JAVA多线程(后期再学一遍)
|
域名解析 网络协议 Oracle
Java的第十五篇文章——网络编程(后期再学一遍)
Java的第十五篇文章——网络编程(后期再学一遍)
|
Java API 容器
java项目设计与思路
与其和大多数Java教程一样,先讲变量,再说继承和多态,再讲数组。还不如直接来看看,我们学习java,能做些什么? 我是小白,这天,就在我慢吞吞地学习Java知识的时候,老板找到我。
100 0
|
移动开发 JSON 小程序
【小程序开篇】小程序架构和配置
【小程序开篇】小程序架构和配置
292 0
【小程序开篇】小程序架构和配置
|
Rust Kubernetes 测试技术
Krustlet 入手案例
本文将对基于 Kind 部署 Krustlet 并实践 Demo 应用
417 0
|
IDE 测试技术 API
聊聊我的源码阅读方法
本次代码阅读的项目来自 500lines 的子项目 web-server。 500 Lines or Less不仅是一个项目,也是一本同名书,有源码,也有文字介绍。这个项目由多个独立的章节组成,每个章节由领域大牛试图用 500 行或者更少(500 or less)的代码,让读者了解一个功能或需求的简单实现。
164 0
聊聊我的源码阅读方法
|
XML 缓存 JSON
看SpringCloudEureka源码前懂得这些知识事半功倍
看SpringCloudEureka源码前懂得这些知识事半功倍
看SpringCloudEureka源码前懂得这些知识事半功倍
下一篇
DataWorks