问题:
vuex作为一个全局数据管理,是怎么把自己挂在实例上
使得组件实例无论嵌套多少层,都能通过this.$store访问到数据中心的数据的?

  1. <template>
  2. <div>
  3. count {{$store.state.count}}
  4. </div>
  5. </template>

每个组件(也就是Vue实例)在beforeCreate的生命周期中都混入(Vue.mixin)同一个Store实例 作为属性 $store, 也就是为啥可以通过 this.$store.dispatch 等调用方法的原因。
==》 意思是每一个组件,生成实例的时候,都传入了 实例后的store对象?

Vue.use(Vuex)
站在vue系统角度,是怎么做的统筹的?

(业务使用角度来说,vuex注入到组件里,并不需要我们自己去绑定什么
image.png

vuex上的data 变量是响应式的,vuex上的数据变化了之后,消费vuex的组件们的本地state,也会得到更新

Vuex原理可以拆解为三个关键点:
1、每个组件实例里都注入了Store实例
2、Store实例中的各种方法都是为Store中的属性服务的
3、Store中的属性变更触发视图更新

vue读取vuex状态

由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态:

  1. // 创建一个 Counter 组件
  2. const Counter = {
  3. template: `<div>{{ count }}</div>`,
  4. computed: {
  5. count () {
  6. return store.state.count
  7. }
  8. }
  9. }

每当 store.state.count 变化的时候, 都会重新求取计算属性,并且触发更新相关联的 DOM。
即手动导入 store,再将store手动映射到组件实例的data上
又由于想要props上的数据变更后,触发本地的state也变更
所以用本地组件用了 computed属性去接住 props上的store数据

但,vuex进行了更精妙的设计:
Vuex 通过 Vue 的插件系统将 store 实例从根组件中“注入”到所有的子组件里
且子组件能通过 this.$store 访问到

  1. const Counter = {
  2. template: `<div>{{ count }}</div>`,
  3. computed: {
  4. count () {
  5. return this.$store.state.count
  6. }
  7. }
  8. }

猜测是怎么注入的?
根组件从何理解?
业务组件 -> 父组件 -> root组件
这样的一个访问链条,所以业务组件可以通过 this.$xxx 访问到 在根部组件注册的 实例

那么对应到 我的应用上
是不是就是把store 注册到 base类上? 因为base类是所有组件的根基

store实例注入到所有的子组件里

下面我们来详细 扒开了揉碎了,理解下这句话
Vuex 通过 Vue 的插件系统将 store 实例从根组件中“注入”到所有的子组件里

applymixin实现方案

  1. import { createStore } from 'vuex'
  2. export const store = createStore({
  3. state() {
  4. return {
  5. count: 1
  6. }
  7. }
  8. })
  9. import { createApp } from 'vue'
  10. import { store } from './store'
  11. import App from './App.vue'
  12. const app = createApp(App)
  13. app.use(store) // Vuex 3是Vue.use(Vuex)
  14. app.mount('#app')

简版:

  1. // 简版
  2. class Store{
  3. constructor(){
  4. this._state = 'Store 实例';
  5. }
  6. dispatch(val){
  7. this.__state = val;
  8. }
  9. commit(){}
  10. // 省略
  11. }

image.png

  1. # 使用者,各个组件们
  2. const store = new Store();
  3. var rootInstance = {
  4. parent: null,
  5. provides: { # vueprovide方法,从上到下注入data
  6. store: store,
  7. },
  8. };
  9. var parentInstance = {
  10. parent: rootInstance,
  11. provides: {
  12. store: store,
  13. }
  14. };
  15. var childInstance1 = {
  16. parent: parentInstance,
  17. provides: {
  18. store: store,
  19. }
  20. };
  21. var childInstance2 = {
  22. parent: parentInstance,
  23. provides: {
  24. store: store,
  25. }
  26. };
  27. # 一旦store变更数据,下面的都能得到更新
  28. store.dispatch('我被修改了'); // state = '我被修改了'
  29. # rootInstanceparentInstancechildInstance1childInstance2
  30. # 这些对象中的provides.store都改了
  31. # 因为共享着同一个store对象。
  1. Vue.use(Vuex); // vue的插件机制,安装vuex插件
  2. 0. vue插件机制里,会调用传入的vuex插件,运行 vuex.install()
  3. 1. vuex.install applyMixin(vue),即vue插件将 vue实例传给了vuex
  4. 2. applyMixin: Vue.mixin({beforeCreatevuexinit}) // 指定vue在beforecreate周期前 执行vuexinit
  5. 3. vuexinit核心代码

image.png

  1. # vuexinit方法的 核心代码:
  2. Vue.mixin({
  3. beforeCreate() {
  4. if (this.$options && this.$options.store) {
  5. //找到根组件 main 上面挂一个$store
  6. this.$store = this.$options.store
  7. // console.log(this.$store);
  8. } else {
  9. //非根组件指向其父组件的$store
  10. this.$store = this.$parent && this.$parent.$store
  11. }
  12. }
  13. })

store注入 vue的实例组件的方式,是通过vue的 mixin机制,借助vue组件的生命周期 钩子 beforeCreate 完成的。即 每个vue组件实例化过程中,会在 beforeCreate 钩子前调用 vuexInit 方法。

梳理下思路:
1、mixin方法:每个组件都会执行这个方法,一旦一段代码被放到vue mixin里执行了
2、让每个组件,执行mixin方法的时候,this.$store = this.parent.store || this.options.store
这样的方式实现的 从上层注入 到最最底层的组件都能享受到 store

好了再再触达下核心:
1、父子组件保持关联
vue里 root组件、parent组件、child组件
他们之间有保持联系的引用关系,child可以通过 this.parent 找到父组件
2、从根上注入vuex,如何实现的 子组件自动能通过this.store 得到跟上注入的组件?
从上到下的链路里,都手动挂了一下 parent.store实例在当前组件上
相比原型链的形式,这样的访问更高效。缩短了原型链查找时间

再来回答下我的项目现状

如果是为了全局都能访问到$store 的话,为什么不把 store 绑定到 Vue.prototype 上?使用 mixin 有什么优势吗? ==》 可能是因为,挂到Vue的原型上,每次访问$stroe,都得访问到原型链比较顶端的位置,性能就会受损

provide实现方案

Vuex4的实现
image.png
2、Vuex4作为Vue的插件如何实现和Vue结合的。
3、provide、inject如何实现的,每个组件如何获取到组件实例中的Store的。
4、为什么每个组件对象里都有Store实例对象了(渲染组件对象过程)。、

5、为什么在组件中写的provide提供的数据,能被子级组件获取到。

1、为什么修改了实例store里的属性,变更后会触发视图更新。

  1. export class Store{
  2. // 省略若干代码...
  3. install (app, injectKey) {
  4. // 为 composition API 中使用
  5. // 可以传入 injectKey 如果没传取默认的 storeKey 也就是 store
  6. app.provide(injectKey || storeKey, this)
  7. // 为 option API 中使用
  8. app.config.globalProperties.$store = this
  9. }
  10. // 省略若干代码...
  11. }

帮手1:app.provide
简单来说就是给context的provides属性中加了store = Store实例对象

  1. provide(key, value) {
  2. // context: const context = createAppContext();
  3. // store: app.provide(injectKey || storeKey, this)
  4. context.provides[key] = value;
  5. return app;
  6. }

createAppContext

  1. function createAppContext() {
  2. return {
  3. app: null,
  4. config: {
  5. isNativeTag: NO,
  6. performance: false,
  7. globalProperties: {},
  8. optionMergeStrategies: {},
  9. isCustomElement: NO,
  10. errorHandler: undefined,
  11. warnHandler: undefined
  12. },
  13. mixins: [],
  14. components: {},
  15. directives: {},
  16. provides: Object.create(null)
  17. };
  18. }

帮手2:app.config.globalProperties

  1. app.config.globalProperties.$store = {}
  2. app.component('child-component', {
  3. mounted() {
  4. console.log(this.$store) // '{}'
  5. }
  6. })

所以为什么每个组件都可以使用 this.$store.xxx 访问 vuex中的方法和属性:
1、在appContext.provides中注入了一个Store实例对象
// vuex插件 vuexinit时通过 app.provide将自己注入到ctx.provides里了

即此时根组件实例、config全局配置globalProperties中有了Store实例对象

2、然后就是 provide 和inject 怎么相互配合 在各个实例上 结合原型链的知识,相互建立链接的故事了
(源码解析过于晦涩 看不下去了。。

这里:4.5.3 Vue.provide 源码实现

看下大佬自己回答的问题:

  • provide、inject的如何实现的,每个组件如何获取到组件实例中的Store的。
  • 为什么在组件中写的provide提供的数据,能被子级组件获取到。

provide函数建立原型链区分出组件实例用户自己写的属性和系统注入的属性。
inject函数则是通过原型链找父级实例中的provides对象中的属性。

  1. function provide(){
  2. let provides = currentInstance.provides;
  3. const parentProvides = currentInstance.parent && currentInstance.parent.provides;
  4. if (parentProvides === provides) {
  5. provides = currentInstance.provides = Object.create(parentProvides);
  6. }
  7. provides[key] = value;
  8. }
  9. function inject(){
  10. const provides = instance.parent == null
  11. ? instance.vnode.appContext && instance.vnode.appContext.provides
  12. : instance.parent.provides;
  13. if (provides && key in provides) {
  14. return provides[key];
  15. }
  16. }
  1. // 当前组件实例
  2. {
  3. parent: '父级的实例',
  4. provides: {
  5. // 可以容纳其他属性,比如用户自己写的
  6. __proto__: {
  7. // 可以容纳其他属性,比如用户自己写的
  8. __proto__ : { store: { __state: 'Store实例' } }
  9. }
  10. }
  11. }

综上总结下吧
这一版的vuex实现,是借助了provide方法 对比 mixin方法呢
mixin:每个组件都会运行这样的方法,然后根据组件实例的访问联系,找到父上的store

而provide:
是provide函数执行本身,将自己挂在了上下文上,
而store又挂在了provide上
所以,store的层层挂在,现在是依靠了provide的层层挂在,顺便自己就得到了层层挂载

  • 为什么每个组件对象里都有Store实例对象了(渲染组件对象过程)。

渲染生成组件实例时,调用createComponentInstance,注入到组件实例的provides中。
前面说了store是挂靠了provide实现的层层挂载,但是不是系统静态时就挂在了
静态挂在的只是provide
vuex,是在渲染生成组件实例的运行时,注入到provide里的

  1. # 每个组件的生成时,都会调用这样的方法,给组件注入:自己的上下文、parentprovides
  2. function createComponentInstance(vnode, parent, suspense) {
  3. const type = vnode.type;
  4. const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
  5. const instance = {
  6. parent,
  7. appContext,
  8. // ...
  9. provides: parent ? parent.provides : Object.create(appContext.provides),
  10. // ...
  11. }
  12. // 使得组件有了自己的上下文、parent、provides
  13. // ...
  14. return instance;
  15. }

store属性变更触发下游更新

触发下游的订阅者
对于vue组件来说,就是触发了 view更新

由此可以扩展很多知识点:

扩:为什么需要状态管理

为什么需要?
image.png

  • 状态state,驱动应用的数据源;
  • 视图view,以声明方式将状态映射到视图;
  • 操作action,响应在视图上的用户输入导致的状态变化。

什么时候需要抽象个全局的状态管理?
当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:

  • 多个视图依赖于同一状态。
    • scrollindexes:gridrange、headerrange、textrange、selectorindex
  • 来自不同视图的行为需要变更同一状态。
    • 鼠标wheel、鼠标滚动 ===> scrollindexes


多个视图依赖于同一状态。
==> 传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力
来自不同视图的行为需要变更同一状态。
我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝
即把父组件调和到 子实例里
有点文学情怀

vuex思路由来
我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理
在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!

image.png

如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式就足够您所需了

store VS 类Flux架构

store:
当访问数据对象时,一个组件实例只是简单的代理访问
如果你有一处需要被多个实例间共享的状态,你可以使用一个 reactive 方法让对象作为响应式对象
image.png

全局响应的是一个对象,有什么弊端?
为什么store是一个类?
===> 应用的任何部分都可以随时更改任何数据,而不会留下变更过的记录。
所以,类store抽象了action,规范了只能通过action来修改store上的数据

  1. const appB = createApp({
  2. data() {
  3. return sourceOfTruth
  4. },
  5. mounted() {
  6. # 直接修改对象属性,我们认为是 过于自由了
  7. sourceOfTruth.message = 'Goodbye'
  8. // both apps will render 'Goodbye' message now
  9. }
  10. }).mount('#app-b')

所有 store 中 state 的变更,都放置在 store 自身的 action 中去管理
类Flux架构

类flux:
所有 store 中 state 的变更,都放置在 store 自身的 action 中去管理。这种集中式状态管理能够被更容易地理解哪种类型的变更将会发生,以及它们是如何被触发。当错误出现时,现在也会有一个 log 记录 bug 之前发生了什么。

  1. const store = {
  2. debug: true,
  3. state: reactive({
  4. message: 'Hello!'
  5. }),
  6. setMessageAction(newValue) {
  7. if (this.debug) {
  8. console.log('setMessageAction triggered with', newValue)
  9. }
  10. this.state.message = newValue
  11. },
  12. clearMessageAction() {
  13. if (this.debug) {
  14. console.log('clearMessageAction triggered')
  15. }
  16. this.state.message = ''
  17. }
  18. }
  19. const appA = createApp({
  20. data() {
  21. return {
  22. privateState: {},
  23. sharedState: store.state
  24. }
  25. },
  26. mounted() {
  27. # 组件通过action去触发 对象的修改
  28. store.setMessageAction('Goodbye!')
  29. }
  30. }).mount('#app-a')

总结下:
随着我们进一步扩展约定,即组件不允许直接变更属于 store 实例的 state,而应执行 action 来分发 (dispatch) 事件通知 store 去改变,最终达成了 Flux 架构

这样约定的好处是:
1、能够记录所有 store 中发生的 state 变更
2、能做到记录变更、保存状态快照、历史回滚/时光旅行的先进的调试工具。

vuex与redux?Redux 无法感知视图层
Vuex 区别在于它是一个专门为 Vue 应用所设计。这使得它能够更好地和 Vue 进行整合


参考

用一句话说清楚vuex原理