Vuex 源码学习

发布于 2023-07-18 19:39:18 字数 13127 浏览 62 评论 0

1. 我的疑问

1.1 $store 是怎么保证每个组件都可以进行访问的
1.2. 为什么需要 Mutation,而不是直接使用 Action
1.3. mapState 等方法是怎么映射到组件的
1.4. 怎么和 Vue 的响应式结合起来的
1.5. 插件的实现【兴趣不大】
1.6. 模块的实现【兴趣不大】

带着我的疑问,现在就开搞!

2. 基本介绍

流程图

Vuex 的核心就是一个 Store , 管理着应用的全局状态。组件从 store 中读取数据,当 store 中数据变化时,组件也会一起更新。
Vuex 中的数据需要通过 Mutation 进行显示更改,当然根据实际业务直接更改对象类型的 state 其实也可以。显示更改更方便我们定位问题和在 devtools 中进行展示

项目地址: https://github.com/vuejs/vuex
构建工具: Rollup
入口文件:src/index.js

由于平时使用的 3.x 版本的 Vuex ,所以分支为 v3.6.2

3. 初始化

回想下平时我们是怎么使用 Vuex, 我们使用 Vue.use 安装了 Vuex 插件,然后通过
然后再来看 Vuex 导出的相关包

Vue.use(Vuex);
const store = new Vuex.Store({
	state,
	getters,
	mutations,
	actions,
})

  
export default store;

首先我们查看入口文件文件

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

Store 就是我们初始化的类, install 就是 Vue.use 安装的逻辑,还有平时使用到的一些辅助函数。

3.1 Vuex 初始化

看下 install 的逻辑

export function install () {
	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 中混入一些逻辑,2.0 中混入了 beforeCreate 函数的逻辑

Vue.mixin({ beforeCreate: vuexInit })

vuexInit 中的逻辑就是混入 $store , 保证应用下的组件都可以访问到 $store

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

通过这里的逻辑就能解答我的[第一个疑问]了

3.2 Store 初始化

Store 初始化主要分为:

  1. Module 注册,处理嵌套逻辑,形成 tree
  2. Module 安装,初始化 state,getters 等
  3. 初始化 Store.vm

3.2.1 Module 注册

初始化 Store 时我们会传入一个对象,里面包含 state , mutations , getters 等属性。现在我们就来看看 Store 这个类.

this._modules = new ModuleCollection(options)

整个 options 可以视为 Store 的 root module, 在 ModuleCollection 中进行解析,如果有嵌套的 module ,该方法也会进行处理
看一下模块注册的逻辑

register (path, rawModule, runtime = true) {
	
	const newModule = new Module(rawModule, runtime)

	// 构建 tree
	if (path.length === 0) {
		this.root = newModule
	} else {
		// slice(0, -1) 不包括最后一项
		const parent = this.get(path.slice(0, -1))
		parent.addChild(path[path.length - 1], newModule)
	}

	// 处理嵌套的模块
	if (rawModule.modules) {
		forEachValue(rawModule.modules, (rawChildModule, key) => {
			this.register(path.concat(key), rawChildModule, runtime)
		})
	}
}

其中 pathstore tree 的路径, rawModule 为配置项, runtime 为是否运行时创建的模块。

通过 Module 类中对模块进行初始化,判断当前 path 的长度,若为 0 则称为 root module , 否则就找到 父模块调用 addChild 方法形成父子逻辑。

这样子模块可以通过路径找到父模块,父模块可以通过 _children key 找到子模块

3.2.2 Module 安装

installModule(this, state, [], this._modules.root)

这个方法的核心逻辑为

// 获取命名空间,模块标识 namespace 为 true, 可以嵌套
const namespace = store._modules.getNamespace(path)


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)
	})
}
// 挂载 state
const local = module.context = makeLocalContext(store, namespace, path)

  
// 处理 mutations,action,getter
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)
})

makeLocalContext 方法中实现上下文的注册,上下文包括了 state , getters , dispatch , commit 等。这几个属性除了 state 都处理了 namespace 的情况。这里讲一下 state 的获取逻辑就好了

Object.defineProperties(local, {
	getters: {
		get: noNamespace
		? () => store.getters
		: () => makeLocalGetters(store, namespace)
	},
	state: {
		get: () => getNestedState(store.state, path)
	}

})

function getNestedState (state, path) {
	return path.reduce((state, key) => state[key], state)
}

state 开始,层层查找子模块 state ,最终找到目标模块的 state .

registerMutation , registerAction 等方法完成对 mutation , action 的注册

3.2.3 初始化 Store.vm

resetStoreVM(this, state)

resetStoreVM 的具体逻辑是这样的

  

function resetStoreVM (store, state, hot) {

   const oldVm = store._vm
   // bind store public getters
   store.getters = {}

   // reset local getters cache

   store._makeLocalGettersCache = Object.create(null)

   const wrappedGetters = store._wrappedGetters

   const 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
   store._vm = new Vue({
   	data: {
   		$$state: state
   	},
   	computed
   })
   Vue.config.silent = silent


   if (oldVm) {
   	if (hot) {
   		store._withCommit(() => {

   			oldVm._data.$$state = null

   		})
   
   	}
   	Vue.nextTick(() => oldVm.$destroy())
   }
}

这里的流程就是从 _wrappedGetters 中取出对应 gettter 函数,挂在到 store._vm 中,同时存入 computed 中,通过 Vuecompute ,和 state 产生依赖关系

store._vm = new Vue({
   	data: {
   		$$state: state
   	},
   	computed
})

然后 storestategetter 是这么设置的

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

因此获取 state 时,实际上访问的是 vm 的 $$state

总体的逻辑来说就是这样的

根据 `key` 访问 `store.getters` 的某一个 `getter` 的时候,实际上就是访问了 `store._vm[key]`,也就是 `computed[key]`,在执行 `computed[key]` 对应的函数的时候,会执行 `rawGetter(local.state,...)` 方法,那么就会访问到 `store.state`,进而访问到 `store._vm._data.$$state`,这样就建立了一个依赖关系。当 `store.state` 发生变化的时候,下一次再访问 `store.getters` 的时候会重新计算

在上面的代码中还有 严格模式的校验

function enableStrictMode (store) {

	store._vm.$watch(function () { return this._data.$$state }, () => {

	if (__DEV__) {

	assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)

	}

	}, { deep: true, sync: true })
}

如果处于严格模式,就会手动添加一个 同步的 watch , 监听 $$state 的变化是否是 mutation 改变的,

_withCommit (fn) {
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}

_withCommit 方法保证了通过 Vuex 自身提供的方法改变数据才能使 _committingtrue

读到这里,关于 [1.4]和[1.6]的问题,已经有了答案,响应式是通过 Vue 实现的,模块是 Store 的基本单位,兼容层级的嵌套和命名空间。

4. API

在 store 的初始化过程中完成了数据的存储,接下来就是通过 API 来进行使用了。

4.1 Mutation & commit

在初始化时,我们进行了 mutation 方法的初始化,可以通过 mutation 进行 state 的更改。

function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}

这里我们也能明白在 mutation 中改变的数据是当前模块的数据,访问的 thisstore . 然后将每个 mutation 存入到 store._mutations[type]

然后我们再看看 commit, store 提供 commit 来提交 commit

commit (_type, _payload, _options) {

	// 规范传入的参数
	const {
		type,
		payload,
		options
	} = unifyObjectStyle(_type, _payload, _options)

  

	const mutation = { type, payload }
	// 获取存放的方法
	const entry = this._mutations[type]
	// 执行 mutation 改变 state
	this._withCommit(() => {
		entry.forEach(function commitIterator (handler) {
			handler(payload)
		})
	})
	// 提供给订阅者使用,一般是插件
	this._subscribers
	.slice() 
	.forEach(sub => sub(mutation, this.state))
}

从这里可以看出,mutation 必须是同步的,不然下面通知 订阅者 的逻辑会出错

4.2 Action & dispatch

Action 和 Mutation 的区别就是 Action 是异步的,所以它们大部分逻辑还是差不多的,只是在异步处理和通知上有区别

...
// 执行前 hook
try {
	this._actionSubscribers
	.slice()
	.filter(sub => sub.before)
	.forEach(sub => sub.before(action, this.state))
} catch (e) {
}

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


return new Promise((resolve, reject) => {
	// 完成 hook 和 error hook
	result.then(res => {
		this._actionSubscribers
		.filter(sub => sub.after)
		.forEach(sub => sub.after(action, this.state))
		
		resolve(res)
	}, error => {
		this._actionSubscribers
		.filter(sub => sub.error)
		.forEach(sub => sub.error(action, this.state, error))
		
		reject(error)
	})
})

4.2 mapState

src/helper.js

export const mapState = normalizeNamespace((namespace, states) => {

	const res = {}

	normalizeMap(states).forEach(({ key, val }) => {
		// 组成 getter 函数
		res[key] = function mappedState () {
			let state = this.$store.state
			let getters = this.$store.getters
			// 有命名空间就使用模块的数据,不然就使用根的
			// 这里的 this 是指向组件的
			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
})

这里的主要逻辑是 得到一些 getter 函数,其中也包括对一些命名空间的处理。这里兼容了 val 是函数还是字符串的情况。

接下来我们看看 normalizeNamesapce 函数干了啥,

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

其实对于 namespace/state 做处理,拿到斜杠前的命名空间 然后对函数进行包裹,兼容传命名空间和不穿命名空间的两种情况。

4.3 mapMutations

其实 mapMutations 的逻辑和 mapState 差不多,这里讲一下核心的逻辑就好了

res[key] = function mappedMutation (...args) {
	let commit = this.$store.commit
	// 没 namespace 就用 store 的顶级 commit,不然就使用模块上下文自己的 commit
	if (namespace) {
		const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
		if (!module) return
		commit = module.context.commit
	}
	// 然后执行 commit 函数,也支持 val 是函数的情况,将 commit 进行传入
	return typeof val === 'function'
	? val.apply(this, [commit].concat(args))

	: commit.apply(this.$store, [val].concat(args))

}

其他的 mapGetters , mapActions 的逻辑都与其类似。

5. 插件

插件平时用的还是很少的,也就学习教程时知道一个 logger 插件,还有 Vuex 持久化 的插件。
现在就顺便研究下插件机制, 显示插件激活

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

代码其实就是将 store 传递给插件,然后插件调用 store.subscribe 订阅 mutation 事件,或者订阅 action 事件。

看了这么多源码,想一下 持久化插件会怎么做呢?我觉得可以在每次数据改变后 将当前 state 存到 localStorage 中,当 store 在激活插件时,从 localStorage 中拿到旧的数据,然后改变 $$state

看了下源码果然逻辑差不多 Vuex-persistedstate

6. 总结

看完源码后我的 6 个疑问解决了 5 个,收获还是很大的。对于自己平时的开发,如果状态比较多的话,子模块和命名空间的确很好用。毕竟 Vuex 花了很大精力去处理这块逻辑。

还有插件的设计,也让我学会了如何去制作 Vuex 插件和 设计插件系统,当然还有最后的疑问,为啥需要 Mutation,而不是直接使用 Action 呢?像 pinia 中就放弃了 Mutation.

我其实也有了答案,正如作者说的,Mutation 其实是为了更好的追踪状态的改变,方便 devtools 去捕捉,同时数据更改后能够及时同步插件,保证插件获取的数据是正确的。

当然直接使用 Action 也是可以,因为 Action 也能够通知订阅者。且用户直接更改数据也不会影响响应式的功能。所以这也是为什么 Mutation 被放弃的原因。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

白芷

暂无简介

文章
评论
27 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文