VUEX 3.x源码分析——1. 理解state

这是对vuex3.x版本的源码分析。
本次分析会按以下方法进行:

    按官网的使用文档顺序,围绕着某一功能点进行分析。这样不仅能学习优秀的项目源码,更能加深对项目的某个功能是如何实现的理解。这个对自己的技能提升,甚至面试时的回答都非常有帮助。

    在围绕某个功能展开讲解时,所有不相干的内容都会暂时去掉,等后续涉及到对应的功能时再加上。这样最大的好处就是能循序渐进地学习,同时也不会被不相干的内容影响。省略的内容都会在代码中以...表示。

    每段代码的开头都会说明它所在的文件目录,方便定位和查阅。如果一个函数内容有多个函数引用,这些都会放在同一个代码块中进行分析,不同路径的内容会在其头部加上所在的文件目录。

本章只讲解vuex中的state,这也是vuex官网中“核心概念”的第一个。

State的初始化

当我们创建了一个store时,state发生了什么。
假设我们按官网的内容,初始化了一个最基础的store:

import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state: { count: 0 } // 这里去掉了 mutations 的内容,只关注state })

  • 这里实例上是创建了一个Store类,并将count内容作为参数传入

// ./index.js import { Store } from './store' // Vuex.Store 就是这里的Store,它是从 ./store 导入的 export default { Store, ... } // ./store.js import ModuleCollection from './module/module-collection' export class Store{ constructor(options = {}) { ... this._modules = New ModuleCollection(options) const state = this._modules.root.state // 可以看出是来自于ModuleCollection类的,那接着看下一段 ModuleCollection 的代码 } } // ./module/module-collection.js import Module from './module' export default class ModuleCollection { constructor(rawRootModule) { this.register([], rawRootModule, false) } ... register(path, rawModule, runtime=true) { const newModule = new Module(rawModule, runtime) if (path.length === 0) { this.root = newModule } else {...} ... } } // ./module/module.js export default class Module { constructor(rawModule, runtime) { this._rawModule = rawModule // 最后在这里处理我们传入的state,Module.state const rawState = rawModule.state // 这里能让我们定义的state除了可以是对象,还可以是函数 this.state = (typeof rawState === 'function' ? rawState() : rawState) || {} } }

  • 至此,可以看出store在初始化时,将定义的state内容赋值到了module.state上,

  • 同时将module绑定到 ModuleCollection.root,

  • 并将实例化的ModuleCollection赋值给Store._modules

  • 最后,为了让vue能监测到state的变化并更新响应的组件,调用了vue中的reactive函数,创建了一个响应式对象 store._state = {data: state},代码如下:

// ./store.js import { resetStoreState } from './store-util' export class Store { constructor(options = {}) { ... this._modules = new ModuleCollection(options) ... const state = this._modules.root.state ... // 在这里生成响应式对象 this._state resetStoreState(this, state) } } // ./store-util.js import { reactive } from 'vue' export function resetStoreState(store, state, hot) { ... store._state = reactive({data: state}) ... }

  • 在这里还没有结束,因为如果按上面的内容,我们要使用定义的count属性的话,代码应该是
    store._state.data.count

  • 但实际上是 store.state.count。vuex还做了一些简单的处理,就是定义了一个getter方法

  • 另外还对state设置了setter方法,来阻止用户直接修改state的值

  • 下面是store.js文件中目前我们所接触的所有代码

// ./store.js import ModuleCollection from './module/module-collection' import { resetStoreState } from './store-util' export class Store { constructor(options = {}) { ... this._modules = new ModuleCollection(options) const state = this._modules.root.state resetStoreState(this, state) } get state() { return this._state.data } set state(v) { if (__DEV__) { assert(false, `use store.replaceState() to explicit replace store state.`) } } }

  • 之前使用vuex时,经常有人说直接修改state的属性会告警且不能修改,原因就在这里。

  • 但我们通过源码分析可以发现,vuex其实并没有阻止使用者修改state,只是阻止了开发环境的修改,如果部署到生成环境就可以直接修改且不会告警。至于官方建议使用mutation方式来提交修改,说更明确地追踪到状态的变化,等后续源码分析完后就能深刻地理解了。

mapState 辅助函数

  • 先看下官方的示例:

// 在单独构建的版本中辅助函数为 Vuex.mapState import { mapState } from 'vuex' export default { // ... computed: mapState({ // 箭头函数可使代码更简练 count: state => state.count, // 传字符串参数 'count' 等同于 `state => state.count` countAlias: 'count', // 为了能够使用 `this` 获取局部状态,必须使用常规函数 countPlusLocalState (state) { return state.count + this.localCount } }) } // 当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。 computed: mapState([ // 映射 this.count 为 store.state.count 'count' ])

  • 让我们看下vuex的源码,是如何实现辅助生成计算属性这个功能的,同时还保证了多种定义计算属性的方法,同时既可以传入对象,又可以传入数组。

  • 首先 mapState 函数定义在 ./helpers.js 中

  • normalizeNamespace是一个高阶函数,它接受一个函数作为参数,并返回一个函数。在它的返回函数中,接受namespace和map作为参数,返回normalizeNamespace接受的函数的执行结果。

  • 在normalizeNamespace的代码中,如果namespace不是字符串类型的话,就将两个参数的值互换,这样保证我们传入的第一个参数(实例中的对象或数组)会被赋值给第二个参数。但是我感觉这里一股别扭感,按道理map在前,namespace在后更合理一些,也不用交换两个参数的值了。可能是为了保持某种使用习惯的统一吧。

  • 所以 mapState 就是 normalizeNamespace 返回的函数。当按上面官方的例子执行类似 mapState(...)的时候,就是将...作为参数传入normalizeNamespace接受的函数,并执行这个函数,返回结果。

  • vuex还有其他地方都有使用这种类似的高阶函数,大致就是接受的参数是一个函数A,同时返回的也是一个函数B,在返回的函数B里执行接受的函数A的操作。

  • 之后 normalizeMap 函数将我们传入的state做了格式化处理,这里的代码解释了为什么mapState传入的参数既可以是对象,也可以是数组。

  • 格式化参数后states后,遍历新的数组,得到res对象。这个对象的key是我们传参时的对象的key或者数组的每个元素,value是一个函数,或者我们定义的函数。

// ./helper.js export const mapState = normalizeNamespace((namespace, states) => { const res = {} normalizeMap(states).forEach(({ key, val }) => { res[key] = function mappedState() { let state = this.$store.state let getters = this.$store.getters ... // 这里保证我们在使用mapState时,既可以直接使用state的某个属性, // 也可以函数式地定义来获得某个计算属性 // 同时在使用函数式定义时, // 还能获得当前组件的相关内容(this),以及store中的state和getters return typeof val === 'function' ? val.call(this, state, getters) : state[val] } }) return res }) // normalizeNamespace()括号包裹住的整个部分就是normalizeNamespace它接受的参数,也是一个函数,mapState就是它返回的函数 function normalizeNamespace(fn) { // 下面就是 mapState 会执行的函数内容 return (namespace, map) => { if (typeof namespace !== 'string') { map = namespace namespace = '' } else {...} return fn(namespace, map) } } 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]})) }

  • 所以,mapState是一个高阶函数的返回函数

  • 执行mapState后,会返回一个对象res,如果我们传入mapState的参数是对象,那res的key就是传参对象的key;如果传入的参数是数组,那这个res的key就是数组中的每个元素。

  • res的value都是一个待执行的mappedState函数

  • 这个res对象作为当前组件的计算属性。

  • 当计算属性所依赖的响应式数据发生变化时,对应的mappedState函数才会执行,然后根据最新的state和getter来计算并返回相应的值。

  • 这种设计模式使得状态的映射可以延迟到组件的计算属性被访问时执行。这样做的好处是可以确保状态映射总是基于最新的 state 和 getters,并且可以利用 Vue 的响应式系统来自动更新。

  • 同时通过分析源码,还发现了除了可以利用state以外,还能访问到getters的内容,这个是官网没写的,例如,我们可以这样修改官方的例子:

import { mapState } from 'vuex' export default { // ... computed: mapState({ // 为了能够使用 `this` 获取局部状态,必须使用常规函数 countPlusLocalState (state) { return state.count + this.localCount } // 函数还可以接受getters,例如: countPlusLocalState_2(state, getters) { return state.count + this.localCount + getters.doubleCount } }) }

  • 最后就是官网的“对象展开运算符”

  • 只要理解了mapState是返回一个对象,以及展开运算符的意义,就知道必然能这样使用了

computed: { localComputed () { /* ... */ }, // 使用对象展开运算符将此对象混入到外部对象中 ...mapState({ // ... }) }

以上就是官网上state的相关源码,下一遍会分析getters的实现源码。

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
zest
作者分享
最近业务需要,计划招多名短期兼职,时长半个月左右,做数据标注任务。 要求理解标注规则,(修改一下)按数量付钱,多劳多得,但是必须在某个规定时间内完成,需要有充足的时间。最好有标注经验。 可远程,有需要的结束后可开实习证明。 感兴趣的加我微信详谈。
7
有没有学习 Python 的鱼友,有用 Python 开发鱼皮项目的经验。 boss 上看了一堆都没匹配上,在这里随缘发一发。 应届或者毕业一年以内,计算机基础扎实,刷过题,有项目经验。 base 深圳南山区,薪资 15k-20k。 除了后端,还能接触到前端和大模型的方向,对职业发展非常有帮助。 感兴趣并且符合要求的加我微信 15773283322
5
新年伊始,招聘一名全栈(偏前端)工程师
5
站在面试官的角度,分析如何更好地应对面试
15
我又来招人了~ 这次是招一名实习生,做 AI 大模型 agent 应用开发。 基于上次招人的情况,我这里做了一些规定和要求,希望有意向的小伙伴能认真看完下面的内容并考虑清楚后再来投递简历。 岗位名称:大模型应用开发工程师 地点:深圳南山科技园 时间:至少实习3个月,周末双休 年限要求:应届实习 岗位要求: 1. 计算机相关专业,最好是大四或研二; 2. 熟悉 Python 语言,具有扎实的数据结构和算法功底; 3. 对基于AI大模型的应用开发感兴趣; 4. 时间比较充裕,能到公司坐班; 5. 良好的沟通能力、学习能力、和团队协作精神; 工作内容: 1. 基于大模型的 agent 应用开发; 2. 基于场景对 Prompt 进行设计和测评,提升大模型效果和稳定性; 3. 根据公司业务需求,与开发团队、产品运营团队紧密配置,持续改进应用; 感兴趣的小伙伴可以添加我微信 15773283322 --------------------------------------------------------------- 随便说一下,上次发布的招聘,也成功地在编程导航上找到了满意的候选人,目前已经入职半个月多了。 上次招人收到了很多人的简历,但是就简历质量而言,真的是一言难尽。 这周末我找时间输出一篇站在面试官的角度看待招聘过程的感受,写一些对焦虑难找工作的人的一些建议。 这里提一句就是真的要好好准备简历啊!
9
下载 APP