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

简介: 接上文。

3.2.4 注册模块的actions


// 注册模块的所有actions
module.forEachAction((action, key) => {
  const type = action.root ? key : namespace + key   // 判断是否需要在命名空间里注册一个全局的action
  const handler = action.handler || action          // 获取actions对应的函数
  registerAction(store, type, handler, local)   
})


遍历模块的所有 actions 方法,其中对于 typehandler 的处理主要是为了兼容两种写法:


// 第一种写法:
actions: {
  func(context, payload) {
    // 省略业务代码...
  }
}
// 第二种写法:
actions: {
  func: {
    root: true,
    handler(context, payload) {
      // 省略业务代码...
    }
  }
}


当采用第二种写法,并且 root = true 时,就会将该 actions 方法注册到全局上,即前面不加上任何的命名空间前缀


再来看看 registerAction 方法里具体实现了什么


// 注册actions方法,接收两个参数:context(包含了上下文中的dispatch方法、commit方法、getters方法、state)、传入的参数payload
function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])   // 通过store._actions 记录所有注册的actions
  entry.push(function wrappedActionHandler (payload) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload)
    // 若返回值不是一个promise对象,则包装一层promise,并将返回值作为then的参数
    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
    }
  })
}


mutations 类似,先从 store._actions 获取入口 entry ,然后将当前的 actions 进行包装处理后添加到 entry 的末尾。actions 方法接收两个参数,即 context 和我们传入的参数 payload ,其中 context 是一个对象,里面包含了 dispatchcommitgettersstaterootGettersrootState ,前4个都是在当前模块的上下文中调用的,后2个是在全局上调用的


最后对于 actions 的返回值还做了一层处理,因为 actions 规定是处理异步任务的,所以我们肯定希望其值是一个 promise 对象,这样方便后续的操作。所以这里对 actions 方法的返回值做了一个判断,如果本身就是 promise 对象,那么就直接返回 ;若不是,则包装一层 promise 对象,并将返回值 res 作为参数返回给 .then


同样的,actions 方法也是可以重名的


3.2.5 注册模块的getters


// 注册模块的所有getters
module.forEachGetter((getter, key) => {
  const namespacedType = namespace + key
  registerGetter(store, namespacedType, getter, local)
})


与上面的类似,这里就不多说了,直接跳到 registerGetters 方法


// 注册getters
function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {   // 若记录过getters了,则不再重复记录
    if (__DEV__) {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  // 在store._wrappedGetters中记录getters
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}


这里发现 getters 并不像 mutationsactions 一样去获取一个 entry ,而是直接查看 store._wrappedGetters[type] 是否有对应的 getters ,若有,则不再重复记录 ; 否则将 getters 包装一下存在 sotre._wrappedGetters 中,其中经过包装后的 getters 接收4个参数,即 stategettersrootStaterootGetters ,前2个分别表示当前上下文中的 stategetters ,后2个分别表示根模块的 stategetters


所以我们在使用 Vuex 时,调用子模块的 getters 时是这样的:


const store = Vuex.Store({
  state: {
    a: 1,
    b: 2
  },
  getters: {
    addA(state) {
      return state.a + 1
    }
  },
  modules: {
    // 子模块A
    ModuleA: {
      state: {
        c: 3
      },
      getters: {
       sum(state, getters, rootState, rootGetters) {
          console.log(state.c)   // 3
          console.log(getters.addC)  // 4
          console.log(rootState.b)  // 2
          console.log(rootGetters.addA)  // 2
        },
        addC(state) {
          return state.c + 1
        }
      }
    }
  }
})


最后我们再次得出一个结论,getters 是不能重名的,并且前一个命名的不会被后一个命名的所覆盖


3.2.6 递归注册子模块


// 递归注册子模块
module.forEachChild((child, key) => {
  installModule(store, rootState, path.concat(key), child, hot)
})


然后就是判断当前的模块里有没有嵌套的子模块了,有的话就将子模块的名称添加到 path 末尾,然后把相应的参数传入 installModule 方法,重新走一遍本文中 3.2 里所有的流程


3.3 注册vm


上面已经将模块的注册完毕了,看一下 constructor 中下一行代码是什么:


resetStoreVM(this, state)


跳到相应的方法中去看一下:


// 初始化vm
function resetStoreVM (store, state, hot) {
  const oldVm = store._vm
  store.getters = {}    // 在实例store上设置getters对象
  store._makeLocalGettersCache = Object.create(null)  // 清空本地缓存
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // 遍历getters,将每一个getter注册到store.getters,访问对应getter时会去vm上访问对应的computed
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })
  const silent = Vue.config.silent
  Vue.config.silent = true
  // 使用Vue实例来存储Vuex的state状态树,并利用computed去缓存getters返回的值
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent
  // 启用严格模式的监听警告
  if (store.strict) {
    enableStrictMode(store)
  }
  // 若存在旧的vm, 销毁旧的vm
  if (oldVm) {
    if (hot) {
      // 解除对旧的vm对state的引用
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}


这个方法里主要做的就是生成一个 Vue 的实例 _vm ,然后将 store._makeLocalGettersCache 里的 getters 以及 store.state 交给一个 _vm 托管,即将 store.state 赋值给 _vm.data.$$state ,将 store._makeLocalGettersCache 通过转化后赋值给 _vm.computed ,这样一来,state 就实现了响应式,getters 实现了类似 computed 的功能


因为生成了新的 _vm ,所以最后通过 oldVm.$destory() 将旧的 _vm 给销毁掉了


值得注意的是,其将 sotre.getters 的操作放在了这个方法里,是因为我们后续访问某个 getters 时,访问的其实是 _vm.computed 中的内容。因此,通过 Object.definePropertystore.getters 进行了处理


3.4 访问 state 、mutations 、actions


到此为止,已经实现了可以通过 store.getter.某个getters 来使用 getters ,那么如何访问 statemutationsactions 呢?


3.4.1 访问 state


通过搜索,在 Store 类中定义了一个 get 函数,用于处理 store.state 的操作:


get state () {
  return this._vm._data.$$state
}


可以很清楚地看到,当我们访问 store.state 时,就是去访问 store._vm.data.$$state ,与刚才介绍 _vm 时说的一样


3.4.2 访问 mutations


其实 mutations 的访问在一开始就触及到了,只不过当时只是提了一嘴,因为当时直接来看可能不会太明白


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 中,对 store.commitstore.dispatch 方法做了一层处理,将该方法的调用指向了 store ,先来看看 commit 方法的具体实现


commit (_type, _payload, _options) {
  // check object-style commit
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)
  const mutation = { type, payload }
  const entry = this._mutations[type]    // 查找_mutations上是否有对应的方法
  // 查找不到则不执行任何操作
  if (!entry) {
    if (__DEV__) {
      console.error(`[vuex] unknown mutation type: ${type}`)
    }
    return
  }
  // 若有相应的方法,则执行
  this._withCommit(() => {
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })
  this._subscribers
    .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
    .forEach(sub => sub(mutation, this.state))
  if (
    __DEV__ &&
    options && options.silent
  ) {
    console.warn(
      `[vuex] mutation type: ${type}. Silent option has been removed. ` +
      'Use the filter functionality in the vue-devtools'
    )
  }
}


首先通过 unifyObjectStyle 方法对传入的参数进行了处理,来看一下这个方法是干什么的


function unifyObjectStyle (type, payload, options) {
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }
  if (__DEV__) {
    assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
  }
  return { type, payload, options }
}


使用过 Vuex 的应该都知道,commit 有两种提交方式:


// 第一种提交方式
this.$store.commit('func', 1)
// 第二种提交方式
this.$store.commit({
  type: 'func',
  num: 1
})


其先对第一个参数进行判断是否为对象,是的话就当作对象提交风格处理,否则的话就直接返回


在处理完参数以后,根据 typestore._mutations 上获取到 entry ,前面分析过了,mutations 方法是以数组形式存储的,所以可能有多个方法。然后在 _withCommit 方法中遍历 entry 依次执行 mutations 方法,这是因为 Vuex 规定 state 的改变都要通过 mutations 方法,store._committing 这个属性就是用来判断当前是否处于调用 mutations 方法的,当 state 值改变时,会先去判断 store._committing 是否为 true ,若不为 true ,则表示 state 的值改变没有经过 mutations 方法,于是会打印警告⚠️ 信息


this._subscribers 这段代码我暂时还不清楚是干什么的,通过词义,目测应该是一个存放订阅的东西吧,就先放着不管了,等后续回来再看


3.4.3 访问 actions


dispatch (_type, _payload) {
  // check object-style dispatch
  const {
    type,
    payload
  } = unifyObjectStyle(_type, _payload)
  const action = { type, payload }
  const entry = this._actions[type]  // 查找_actions上是否有对应的方法
  // 查找不到则不执行任何操作
  if (!entry) {
    if (__DEV__) {
      console.error(`[vuex] unknown action type: ${type}`)
    }
    return
  }
  try {
    this._actionSubscribers
      .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
      .filter(sub => sub.before)
      .forEach(sub => sub.before(action, this.state))
  } catch (e) {
    if (__DEV__) {
      console.warn(`[vuex] error in before action subscribers: `)
      console.error(e)
    }
  }
  const result = entry.length > 1
  ? Promise.all(entry.map(handler => handler(payload)))
  : entry[0](payload)
  return new Promise((resolve, reject) => {
    result.then(res => {
      try {
        this._actionSubscribers
          .filter(sub => sub.after)
          .forEach(sub => sub.after(action, this.state))
      } catch (e) {
        if (__DEV__) {
          console.warn(`[vuex] error in after action subscribers: `)
          console.error(e)
        }
      }
      resolve(res)
    }, error => {
      try {
        this._actionSubscribers
          .filter(sub => sub.error)
          .forEach(sub => sub.error(action, this.state, error))
      } catch (e) {
        if (__DEV__) {
          console.warn(`[vuex] error in error action subscribers: `)
          console.error(e)
        }
      }
      reject(error)
    })
  })
}


前半部分与 commit 方法类似,就不多说了


代码中又出现了 this._actionSubscribers ,与 commit 中的也类似,可能这里是存放 actions 的订阅者的东西,所以这些都先不看了


其中变量 result ,先判断 entry 的长度,若大于1,则表示有多个异步方法,所以用 Promise.all 进行包裹 ; 否则直接执行 entry[0]


最后创建并返回了一个新的 promise ,内部判断了 result 的状态,成功则执行 resolve ,失败则执行 reject


到此为止,我们已经实现了 store.statestore.gettersstore.commitstore.dispatch 的调用了


3.5 插件的调用


继续看 constructor 中的代码(这段代码也是整个 Store 类的构造函数中最后的一小段代码了)


// 依次调用传入的插件
plugins.forEach(plugin => plugin(this))
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
// 使用vue的开发插件
if (useDevtools) {
  devtoolPlugin(this)
}


首先就是遍历创建 Store 类时传入的参数 Plugins ,依次调用传入的插件函数(当然一般我们都没有传入,所以 Plugins 默认是空数组)


然后就是调用 devtoolPlugin 方法啦,根据导入的路径我们去到相应的文件


// 文件路径:./plugins/devtool.js
const target = typeof window !== 'undefined'
  ? window
  : typeof global !== 'undefined'
    ? global
    : {}
const devtoolHook = target.__VUE_DEVTOOLS_GLOBAL_HOOK__
export default function devtoolPlugin (store) {
  if (!devtoolHook) return
  store._devtoolHook = devtoolHook
  devtoolHook.emit('vuex:init', store)
  devtoolHook.on('vuex:travel-to-state', targetState => {
    store.replaceState(targetState)
  })
  store.subscribe((mutation, state) => {
    devtoolHook.emit('vuex:mutation', mutation, state)
  }, { prepend: true })
  store.subscribeAction((action, state) => {
    devtoolHook.emit('vuex:action', action, state)
  }, { prepend: true })
}


看了半天,搜索了半天,都没有找到哪个文件里有 __VUE_DEVTOOLS_GLOBAL_HOOK__ ,应该是 dev-tools 插件里定义的,为了保证 Vuex 的源码阅读进度,就先舍弃阅读 dev-tools 插件的内容了


3.6 其它方法


整个 Store 实例生成的全过程差不多就是这样了,另外还会发现,其实有很多方法都没有被用到,但是却被定义出来了,这里可以稍微列举几个简单地看一下


3.6.1 更新 state


// 在store._committing = true 的状态下更新一下state
replaceState (state) {
  this._withCommit(() => {
    this._vm._data.$$state = state
  })
}


一目了然,这是提供了一种直接修改 state 的方法,并且不会打印警告信息


3.6.2 注册、卸载模块


// 注册模块
registerModule (path, rawModule, options = {}) {
  if (typeof path === 'string') path = [path]
  if (__DEV__) {
    assert(Array.isArray(path), `module path must be a string or an Array.`)
    assert(path.length > 0, 'cannot register the root module by using registerModule.')
  }
  this._modules.register(path, rawModule)
  installModule(this, this.state, path, this._modules.get(path), options.preserveState)
  // reset store to update getters...
  resetStoreVM(this, this.state)
}
// 卸载模块
unregisterModule (path) {
  if (typeof path === 'string') path = [path]
  if (__DEV__) {
    assert(Array.isArray(path), `module path must be a string or an Array.`)
  }
  this._modules.unregister(path)
  this._withCommit(() => {
    const parentState = getNestedState(this.state, path.slice(0, -1))
    Vue.delete(parentState, path[path.length - 1])
  })
  resetStore(this)
}


3.6.3 重置 store 实例


// 重置store,即注册模块、生成vm等操作
function resetStore (store, hot) {
  store._actions = Object.create(null)
  store._mutations = Object.create(null)
  store._wrappedGetters = Object.create(null)
  store._modulesNamespaceMap = Object.create(null)
  const state = store.state
  // init all modules
  installModule(store, state, [], store._modules.root, true)
  // reset vm
  resetStoreVM(store, state, hot)
}


将所有的状态都清空,然后重新执行一边 installModuleresetStoreVM ,这一般在模块结构变化以后调用,例如某个模块被卸载


相关文章
|
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