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

Posted 「零一」

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从未看过源码,到底该如何入手?分享一次完整的源码阅读过程相关的知识,希望对你有一定的参考价值。

前言

我觉得每个人可能都有过看源码的想法吧,也包括我。因为看源码不光能使自己对这个库更加熟悉,还能学习到作者强大的思想,久而久之,自己的水平和思想也会有明显的提升的。

但对于我来说,之前从来没有阅读过源码,想阅读源码却不敢迈出那一步,因为一个成熟的库有着太多的方法、逻辑,阅读起来可能会比较困难,但人总要勇于尝试的嘛,于是我就准备把 Vuex 的源码 clone 下来,没有别的原因,只是因为这个库体积比较小,算上注释,核心代码只有1000行不到,我觉得非常适合第一次阅读源码的人拿来练手

说干就干,我就先在 github 上给自己列了一个计划表,预计 15 天看完源码并完成总结,然后每天记录一下当天的收获

不过最后的结果倒是出乎我的意料,阅读源码加上整理总结只用了8天左右的时间

在阅读源码之前,我是先去看了一下 Vuex 的官方文档,算是一种回顾、查漏补缺,我也非常建议这样做,因为你看源码,你就会看到这个库里面所有的内容,那么你连这个库都没用明白呢,阅读源码的难度无形之中又增加了嘛!即先会熟练使用这个库的各个方法(尽管你并不知道为何这么使用),再在阅读源码的过程中看到相应的代码时联想到那个方法的使用,两者相互结合,对于源码的理解就变得容易许多了

这里放上 Vuex 官方文档的链接,如果有兴趣跟着我的思路阅读 Vuex 源码的小伙伴可以先把文档中提到的所有使用都熟悉一下 ➡️ Vuex官方文档

文末有 总结问答环节

🔥 源码解析

对于源码的所有注释和理解我都收录在我 githubVuex-Analysis 仓库里了,想要看更详细的注释的,可以 fork 下来参考一下 ➡️ Vuex源码解析仓库地址链接(觉得不错的可以点个 star 支持一下)

接下来本文就按照我当时阅读源码的思路,一步一步详细地讲解,希望大家耐心看完,谢谢啦~

  • 公众号:前端印象
  • 不定时有送书活动,记得关注~
  • 关注后回复对应文字领取:【面试题】、【前端必看电子书】、【数据结构与算法完整代码】、【前端技术交流群】

一、源码目录结构分析

整个 Vuex 的源码文件非常多,我们直接看最主要的文件,即 src 文件夹中的内容,结构示例如下:

├── src
    ├── module    // 与模块相关的操作
    │   ├── module-collection.js   // 用于收集并注册根模块以及嵌套模块
    │   └── module.js   // 定义Module类,存储模块内的一些信息,例如: state...
    │
    ├── plugins   // 一些插件
    │   ├── devtool.js   // 开发调试插件
    │   └── logger.js    // 
    │
    ├── helpers.js       // 辅助函数,例如:mapState、mapGetters、mapMutations...
    ├── index.cjs.js     // commonjs 打包入口
    ├── index.js         // 入口文件
    ├── index.mjs        // es6 module 打包入口
    ├── mixin.js         // 将vuex实例挂载到全局Vue的$store上
    ├── store.js         // 核心文件,定义了Store类
    └── util.js          // 提供一些工具函数,例如: deepCopy、isPromise、isObject...

二、源码阅读

1. 查看工具函数

首先我个人觉得肯定是要看一下 util.js ,这里面存放的是源码中频繁用到的工具函数,所以我觉得要最先了解一下每个函数的作用是什么

/**
 * Get the first item that pass the test
 * by second argument function
 *
 * @param Array list
 * @param Function f
 * @return *
 */

// 找到数组list中第一个符合要求的元素
export function find (list, f) 
  return list.filter(f)[0]


/**
 * 深拷贝
 * 
 * @param * obj
 * @param Array<Object> cache
 * @return *
 */
export function deepCopy (obj, cache = []) 
  // just return if obj is immutable value
  if (obj === null || typeof obj !== 'object') 
    return obj
  

  // if obj is hit, it is in circular structure
  const hit = find(cache, c => c.original === obj)
  if (hit) 
    return hit.copy
  

  const copy = Array.isArray(obj) ? [] : 
  // put the copy into cache at first
  // because we want to refer it in recursive deepCopy
  cache.push(
    original: obj,
    copy
  )

  Object.keys(obj).forEach(key => 
    copy[key] = deepCopy(obj[key], cache)
  )

  return copy


// 遍历obj对象的每个属性的值
export function forEachValue (obj, fn) 
  Object.keys(obj).forEach(key => fn(obj[key], key))


// 判断是否为对象(排除null)
export function isObject (obj) 
  return obj !== null && typeof obj === 'object'


// 判断是否为Promise对象
export function isPromise (val) 
  return val && typeof val.then === 'function'


// 断言
export function assert (condition, msg) 
  if (!condition) throw new Error(`[vuex] $msg`)


// 保留原始参数的闭包函数
export function partial (fn, arg) 
  return function () 
    return fn(arg)
  

每个函数的作用我都写上了注释,稍微阅读一下应该可以明白其作用

2. 入口文件

最主要的代码都在 src 目录下,所以以下提到的文件都是默认 src 目录下的文件

首先,肯定从入口文件 index.js 开始看,但能发现的是,还有 index.cjsindex.mjs ,这两者分别是 commonjses6 module 的打包入口,我们就不用管了

import  Store, install  from './store'
import  mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers  from './helpers'
import createLogger from './plugins/logger'

export default 
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers,
  createLogger


export 
  Store,
  install,
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers,
  createLogger

从入口文件中可以看到,主要导出了 Store 类 、install 方法以及一些辅助函数(mapState、mapMutations、mapGetters…)

那么我们主要看的就是 vuex 的核心代码,即 store.js ,可以看到 Store 类就出自于这个文件

3. Store类的实现

整个 Store 类的主要逻辑都在它的构造函数 constructor 中,因此我们就从 constructor 中分步去捋逻辑、看代码

3.1 存放类的状态

首先是定义了一些实例状态,用于存放模块、mutationsactionsgetters 缓存等东西

const 
  plugins = [],
  strict = false
 = options      // 生成Store类的入参

this._committing = false        // 表示提交的状态,当通过mutations方法改变state时,该状态为true,state值改变完后,该状态变为false; 在严格模式下会监听state值的改变,当改变时,_committing为false时,会发出警告,即表明state值的改变不是经过mutations的

this._actions = Object.create(null)  // 用于记录所有存在的actions方法名称(包括全局的和命名空间内的,且允许重复定义)      

this._actionSubscribers = []       // 存放actions方法订阅的回调函数

this._mutations = Object.create(null)  // 用于记录所有存在的的mutations方法名称(包括全局的和命名空间内的,且允许重复定义)

this._wrappedGetters = Object.create(null)  // 收集所有模块包装后的的getters(包括全局的和命名空间内的,但不允许重复定义)

this._modules = new ModuleCollection(options)  // 根据传入的options配置,注册各个模块,此时只是注册、建立好了各个模块的关系,已经定义了各个模块的state状态,但getters、mutations等方法暂未注册

this._modulesNamespaceMap = Object.create(null)   // 存储定义了命名空间的模块

this._subscribers = []    // 存放mutations方法订阅的回调

this._watcherVM = new Vue()  // 用于监听state、getters

this._makeLocalGettersCache = Object.create(null)   // getters的本地缓存

关于各个变量状态的作用都写在这了,其中只有 this._modules = new ModuleCollection(option) 执行了一些操作,其作用就是进行模块递归收集,根据 ModuleCollection 的来源,我们移步到 ./module/module-collection.js 文件

3.1.1 递归收集模块

Module-collection.js 文件中定义了 ModuleCollection 类,其作用就是通过递归遍历 options 入参,将每个模块都生成一个独立的 Moudle

这里先来熟悉一下 options 的结构,如下:

import Vuex from 'vuex'

const options = 
  state: ...,
  getters: ...,
  mutations: ...,
  actions: ...,
  modules: 
    ModuleA: 
      state: ...,
      ...
      modules: 
        ModuleA1: ...
      
    ,
    ModuleB: 
      state: ...,
      ...
      modules: 
        ModuleB1: ...
      
    
  


const store = new Vuex.Store(options)

export default store

可以看到传入的 options 整体可以看成一个根模块 root ,然后 rootmodules 中嵌套着另外两个子模块:ModuleAModuleB ,而 ModuleAModuleB 内部也分别嵌套着一个子模块,分别为 ModuleA1ModuleB1 。这样就组成了一个模块树,因此 ModuleCollection 类的工作就是将保留原来的模块关系,将每个模块封装到一个 Module 类中

export default class ModuleCollection 
  constructor (rawRootModule) 
    // 递归注册模块
    this.register([], rawRootModule, false)
  
  
  // 根据路径顺序,从根模块开始递归获取到我们准备添加新的模块的父模块
  get (path) 
    return path.reduce((module, key) => 
      return module.getChild(key)
    , this.root)
  
  
  // 递归注册模块
  register (path, rawModule, runtime = true) 
    if (__DEV__) 
      assertRawModule(path, rawModule)
    
    
    const newModule = new Module(rawModule, runtime)  // 初始化一个新的模块
    if (path.length === 0)     // 当前没有别的模块
      this.root = newModule     // 则此模块为根模块
     else     // 有多个模块     
      const parent = this.get(path.slice(0, -1))   // 获取到新模块从属的父模块,所以是path.slice(0, -1),最后一个元素就是我们要添加的子模块的名称
      parent.addChild(path[path.length - 1], newModule)    // 在父模块中添加新的子模块
    

    if (rawModule.modules)      // 如果有嵌套模块
      /**
       *  1. 遍历所有的子模块,并进行注册;
       *  2. 在path中存储除了根模块以外所有子模块的名称
       *  */ 
      forEachValue(rawModule.modules, (rawChildModule, key) => 
        this.register(path.concat(key), rawChildModule, runtime)
      )
    
  

函数作用:

  1. register(path, rawModule, runtime):注册新的模块,并根据模块的嵌套关系,将新模块添加作为对应模块的子模块
  • path:表示模块嵌套关系。当前为根模块时,没有任何嵌套关系,此时 path = [] ; 当前不是根模块时,存在嵌套关系,例如上述例子中的 ModuleA1 ,它是 ModuleA 的子模块 ,而 ModuleA 又是根模块的子模块,此时 path = ['ModuleA', 'ModuleA1']
  • rawModule:表示模块对象,此时是一个对象类型
  • runtime:表示程序运行时
  1. get(path):根据传入的 path 路径,获取到我们想要的 Module

ModuleCollection 的构造函数中调用了 register 函数,前两个参数分别为:[]rawRootModule ,此时肯定是从根模块开始注册的,所以 path 里无内容,并且 rawRootModule 指向的是根模块

然后来看一下 register 函数里的逻辑。

  1. 首先将当前要注册的模块生成一个 Module ,并将 rawModule 作为参数,用于存放 Module 的信息

  2. 然后通过 if(path.length === 0) 判断是否为根模块,是的话就将 this.root 指向 Module ; 否则就跳到第3步

  3. 判断当前模块不是根模块,就通过 get 函数找到当前模块的父模块,然后调用父模块中的 addChild 方法将当前模块添加到子模块中

  4. 最后再判断当前模块是否还有嵌套的模块,有的话就重新回到第1步进行递归操作 ; 否则不做任何处理

按照上面的逻辑,就可以将所有的模块递归收集并注册好了,其中有一个 Module 类还没有具体提到,所以这里移步到 ./module/module.js

import  forEachValue  from '../util'

// 定义了Vuex中的 Module 类,包含了state、mutations、getters、actions、modules
export default class Module 
  constructor (rawModule, runtime) 
    this.runtime = runtime
    
    this._children = Object.create(null)   // 创建一个空对象,用于存放当前模块的子模块
    
    this._rawModule = rawModule         // 当前模块的一些信息,例如:state、mutations、getters、actions、modules
    const rawState = rawModule.state    // 1. 函数类型 => 返回一个obj对象; 2. 直接获取到obj对象

    // 存储当前模块的state状态
    this.state = (typeof rawState === 'function' ? rawState() : rawState) ||    
  

  // 判断该模块是否定义了namespaced,定义了则返回true; 否则返回false
  get namespaced () 
    return !!this._rawModule.namespaced
  

  // 添加子模块,名称为key
  addChild (key, module) 
    this._children[key] = module
  

  // 移除名称为key的子模块
  removeChild (key) 
    delete this._children[key]
  

  // 获取名称为key的子模块
  getChild (key) 
    return this._children[key]
  

  // 是否存在名称为key的子模块
  hasChild (key) 
    return key in this._children
  
	
  // 将当前模块的命名空间更新到指定模块的命名空间中,并同时更新一下actions、mutations、getters的调用来源
  update (rawModule) 
    this._rawModule.namespaced = rawModule.namespaced
    if (rawModule.actions) 
      this._rawModule.actions = rawModule.actions
    
    if (rawModule.mutations) 
      this._rawModule.mutations = rawModule.mutations
    
    if (rawModule.getters) 
      this._rawModule.getters = rawModule.getters
    
  

  // 遍历当前模块的所有子模块,并执行回调操作
  forEachChild (fn) 
    forEachValue(this._children, fn)
  

  // 遍历当前模块的所有getters,并执行回调操作
  forEachGetter (fn) 
    if (this._rawModule.getters) 
      forEachValue(this._rawModule.getters, fn)
    
  

  // 遍历当前模块的所有actions,并执行回调操作
  forEachAction (fn) 
    if (this._rawModule.actions) 
      forEachValue(this._rawModule.actions, fn)
    
  

  // 遍历当前模块的所有mutations,并执行回调操作
  forEachMutation (fn) 
    if (this._rawModule.mutations) 
      forEachValue(this._rawModule.mutations, fn)
    
  

来看一下刚才模块收集时,创建的 Module 类内部做了什么事情,同样的从 constructor 中开始看

this._children 是一个对象值,用于存放该模块嵌套的其它 Module 类 ;

this._rawModule 就是用于存放该模块内部的一些信息,例如:statemutationsactionsgettersmoudles ;

this.state 对应的就是 this._rawModule 中的 state ;

这是整个构造函数中执行的操作,我们可以看到,在生成一个 Module 类的时候,其只定义了 state 属性,而 mutationsgettersactionsmodules 都是没有被定义的,即例如现在是无法通过 Module.mutations 获取到该模块所有的 mutations 方法,那么这些方法都是在何时被定义的呢?自然是等模块全部都收集完毕以后才进行的操作,因为 vuex 中的嵌套模块可能会存在命名空间 namespaced

3.2 注册模块

到此为止,各个模块的类都创建好了,那么继续回到 ./src/store.jsconstructor 构造函数中

// 将 dispatch 和 commit 方法绑定到 Store 的实例上,避免后续使用dispatch或commit时改变了this指向
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)


// 判断store是否未严格模式。true: 所有的state都必须经过mutations来改变
this.strict = strict

// 将根模块的state赋值给state变量
const state = this._modules.root.state

这段代码首先对 Store 实例上的 dispatchcommit 方法进行了一层包装,即通过 call 将这两个方法的作用对象指向当前的 Store 实例,这样就能防止后续我们操作时,出现 this.$store.dispatch.call(obj, 1) 类似的情况而报错

this.strict 是用于判断是否是严格模式。因为 vuex 中,建议所有的 state 变量的变化都必须经过 mutations 方法,因为这样才能被 devtool 所记录下来,所以在严格模式下,未经过 mutations 而直接改变了 state 的值,开发环境下会发出警告⚠️

const state = this._modules.root.state 获取的是根模块的 state ,用于后续的一些操作

一切都准备就绪了,下面就开始为每个模块注册信息了

// 从根模块开始,递归完善各个模块的信息
installModule(this, state, [], this._modules.root)

调用了 installModule 方法,并将 store 实例对象 、state 属性 、路径 、根模块对象依次作为参数进行传递

// 注册完善各个模块内的信息
function installModule (store, rootState, path, module, hot) 
  const isRoot = !path.length  // 是否为根模块
  const namespace = store._modules.getNamespace(path)  // 获取当前模块的命名空间,格式为:second/ 或 second/third/

  // 如果当前模块设置了namespaced 或 继承了父模块的namespaced,则在modulesNamespaceMap中存储一下当前模块
  if (module.namespaced) 
    if (store._modulesNamespaceMap[namespace] && __DEV__) 
      console.error(`[vuex] duplicate namespace $namespace for the namespaced module $path.join('/')`)
    
    store._modulesNamespaceMap[namespace] = module
  

  // 如果不是根模块,将当前模块的state注册到其父模块的state上
  if (!isRoot && !hot) 
    const parentState = getNestedState(rootState, path.slice(0, -1)) // 获取父模块的state
    const moduleName = path[path.length - 1]   // 当前模块的名称
    store._withCommit(() => 
      if (__DEV__) 
        if (moduleName in parentState) 
          console.warn(
            `[vuex] state field "$moduleName" was overridden by a module with the same name at "$path.join('.')"`
          )
        
      
      // 将当前模块的state注册在父模块的state上,并且是响应式的
      Vue.set(parentState, moduleName, module.state)
    )
  

  // 设置当前模块的上下文context
  const local = module.context = makeLocalContext(store, namespace, path)

  // 注册模块的所有mutations
  module.forEachMutation((mutation, key) => 
    const namespacedType = namespace + key     // 例如:first/second/mutations1
    registerMutation(store, namespacedType, mutation, local)
  )

  // 注册模块的所有actions
  module.forEachAction((action, key) => 
    /**
     * actions有两种写法:
     * 
     * actions: 
     *    AsyncAdd (context, payload) ...,   // 第一种写法
     *    AsyncDelete:                        // 第二种写法
     *      root: true,
     *      handler: (context, payload) ...
     *     
     * 
     */
    const type = action.root ? key : namespace + key   // 判断是否需要在

以上是关于从未看过源码,到底该如何入手?分享一次完整的源码阅读过程的主要内容,如果未能解决你的问题,请参考以下文章

分析开源项目源码,我们该如何入手分析?(授人以渔)

分析开源项目源码,我们该如何入手分析?(授人以渔)

WebRTC源码入手和底层功能扩展资料分享

WebRTC源码入手和底层功能扩展资料分享

曹工说Spring Boot源码(16)-- Spring从xml文件里到底得到了什么(aop:config完整解析上)

曹工说Spring Boot源码(15)-- Spring从xml文件里到底得到了什么(context:load-time-weaver 完整解析)