# 一文读懂vuex4源码,原来provide/inject就是妙用了原型链?

# 1. 前言

你好,我是若川 (opens new window),微信搜索「若川视野」 (opens new window)关注我,专注前端技术分享,一个愿景是帮助5年内前端开阔视野走向前列的公众号。欢迎加我微信ruochuan12,长期交流学习。

这是学习源码整体架构系列 之 vuex4 源码(第十篇)。学习源码整体架构系列文章(有哪些必看的JS库 (opens new window)):jQuery (opens new window)underscore (opens new window)lodash (opens new window)sentry (opens new window)vuex (opens new window)axios (opens new window)koa (opens new window)redux (opens new window)vue-devtools 直接打开文件功能揭秘 (opens new window)

10篇源码系列文章小成就达成,从19年7月开始写,19年写了6篇,20年写了2篇,今年写了2篇。算是一个完结吧。短时间内应该暂时不更新这个系列了。主要是投入的时间和精力比较多,看的人很少,得到的反馈也比较少。之后先写其他文章吧。欢迎持续关注我(若川 (opens new window))。

本文仓库地址 (opens new window)git clone https://github.com/lxchuan12/vuex4-analysis.git,本文最佳阅读方式,克隆仓库自己动手调试,容易吸收消化。

要是有人说到怎么读源码,正在读文章的你能推荐我的源码系列文章,那真是无以为报啊

我的文章,尽量写得让想看源码又不知道怎么看的读者能看懂。我都是推荐使用搭建环境断点调试源码学习哪里不会点哪里边调试边看,而不是硬看。正所谓:授人与鱼不如授人予渔

阅读本文后你将学到:

    1. git subtree 管理子仓库
    1. 如何学习Vuex 4源码、理解Vuex原理
    1. Vuex 4Vuex 3 的异同
    1. Vuex 4 composition API 如何使用
    1. Vue.provide / Vue.inject API 使用和原理
    1. 如何写一个 Vue3 插件
  • 等等

如果对于谷歌浏览器调试还不是很熟悉的读者,可以看这篇文章chrome devtools source面板 (opens new window),写的很详细。顺带提一下,我打开的设置,source面板中支持展开搜索代码块(默认不支持),一图胜千言code-folding。谷歌浏览器是我们前端常用的工具,所以建议大家深入学习,毕竟工欲善其事,必先利其器

之前写过Vuex 3的源码文章学习 vuex 源码整体架构,打造属于自己的状态管理库 (opens new window)若川的博客Vuex源码 (opens new window),仓库有很详细的注释和看源码方法,所以本文不会过多赘述与Vuex 3源码相同的地方。

# 1.1 本文阅读最佳方式

把我的vuex4源码仓库 git clone https://github.com/lxchuan12/vuex4-analysis.git克隆下来,顺便star一下我的vuex4源码学习仓库 (opens new window)^_^。跟着文章节奏调试和示例代码调试,用chrome动手调试印象更加深刻。文章长段代码不用细看,可以调试时再细看。看这类源码文章百遍,可能不如自己多调试几遍,大胆猜测,小心求证。也欢迎加我微信交流ruochuan12

# 2. Vuex 原理简述

结论先行Vuex原理可以拆解为三个关键点。 第一点、其实就是每个组件实例里都注入了Store实例。 第二点、Store实例中的各种方法都是为Store中的属性服务的。 第三点、Store中的属性变更触发视图更新。

本文主要讲解第一点。第二点在我的上一篇文章学习 vuex 源码整体架构,打造属于自己的状态管理库 (opens new window)详细讲了,本文就不赘述了。第三点两篇文章都没有详细讲述。

以下是一段简短的代码说明Vuex原理的。

// 简版
class Store{
  constructor(){
    this._state = 'Store 实例';
  }
  dispatch(val){
    this.__state = val;
  }
  commit(){}
  // 省略
}


const store = new Store();
var rootInstance = {
  parent: null,
  provides: {
    store: store,
  },
};
var parentInstance = {
  parent: rootInstance,
  provides: {
    store: store,
  }
};
var childInstance1 = {
  parent: parentInstance,
  provides: {
    store: store,
  }
};
var childInstance2 = {
  parent: parentInstance,
  provides: {
    store: store,
  }
};

store.dispatch('我被修改了');
// store Store {_state: "我被修改了"}

// rootInstance、parentInstance、childInstance1、childInstance2 这些对象中的provides.store都改了。
// 因为共享着同一个store对象。

provide,inject示例图

看了上面的官方文档中的图,大概知道是用provide父级组件中提供Store实例,用inject来获取到Store实例。

那么接下来,带着问题:

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

2、Vuex4作为Vue的插件如何实现和Vue结合的。

3、provideinject的如何实现的,每个组件如何获取到组件实例中的Store的。

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

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

# 3. Vuex 4 重大改变

在看源码之前,先来看下Vuex 4发布的release和官方文档迁移提到的重大改变,Vuex 4 release (opens new window)

从 3.x 迁移到 4.0 (opens new window)

Vuex 4的重点是兼容性。Vuex 4支持使用Vue 3开发,并且直接提供了和Vuex 3完全相同的API,因此用户可以在Vue 3项目中复用现有的Vuex代码。

相比Vuex 3版本。主要有如下重大改变(其他的在上方链接中):

# 3.1 安装过程

Vuex 3Vue.use(Vuex)

Vuex 4则是app.use(store)

import { createStore } from 'vuex'

export const store = createStore({
  state() {
    return {
      count: 1
    }
  }
})
import { createApp } from 'vue'
import { store } from './store'
import App from './App.vue'

const app = createApp(App)

app.use(store)

app.mount('#app')

# 3.2 核心模块导出了 createLogger 函数

import { createLogger } from 'vuex'

接下来我们从源码的角度来看这些重大改变

# 4. 从源码角度看 Vuex 4 重大变化

# 4.1 chrome 调试 Vuex 4 源码准备工作

git subtree add --prefix=vuex https://github.com/vuejs/vuex.git 4.0

这种方式保留了vuex4仓库的git记录信息。更多git subtree使用方式可以查看这篇文章用 Git Subtree 在多个 Git 项目间双向同步子项目,附简明使用手册 (opens new window)

作为读者朋友的你,只需克隆我的Vuex 4源码仓库 (opens new window) https://github.com/lxchuan12/vuex4-analysis.git 即可,也欢迎star一下。

vuex/examples/webpack.config.js,加个devtool: 'source-map',这样就能开启sourcemap调试源码了。

我们使用项目中的购物车的例子调试,贯穿全文。

git clone https://github.com/lxchuan12/vuex4-analysis.git
cd vuex
npm i
npm run dev
# 打开 http://localhost:8080/
# 选择 composition  购物车的例子 shopping-cart
# 打开 http://localhost:8080/composition/shopping-cart/
# 按 F12 打开调试工具,source面板 => page => webpack:// => .

据说一图胜千言,这时简单截个调试的图。

vuex debugger

找到 createStore函数打上断点。

// webpack:///./examples/composition/shopping-cart/store/index.js
import { createStore, createLogger } from 'vuex'
import cart from './modules/cart'
import products from './modules/products'

const debug = process.env.NODE_ENV !== 'production'

export default createStore({
  modules: {
    cart,
    products
  },
  strict: debug,
  plugins: debug ? [createLogger()] : []
})

找到app.js入口,在app.use(store)app.mount('#app')等打上断点。

// webpack:///./examples/composition/shopping-cart/app.js
import { createApp } from 'vue'
import App from './components/App.vue'
import store from './store'
import { currency } from './currency'

const app = createApp(App)

app.use(store)

app.mount('#app')

接下来,我们从createApp({})app.use(Store)两个方面发散开来讲解。

# 4.2 Vuex.createStore 函数

相比 Vuex 3 中,new Vuex.Store,其实是一样的。只不过为了和Vue 3 统一,Vuex 4 额外多了一个 createStore 函数。

export function createStore (options) {
  return new Store(options)
}
class Store{
  constructor (options = {}){
    // 省略若干代码...
    this._modules = new ModuleCollection(options)
    const state = this._modules.root.state
    resetStoreState(this, state)
    // 省略若干代码...
  }
}
function resetStoreState (store, state, hot) {
  // 省略若干代码...
  store._state = reactive({
    data: state
  })
  // 省略若干代码...
}

监测数据

Vuex 3不同的是,监听数据不再是用new Vue(),而是Vue 3提供的reactive方法。

Vue.reactive 函数方法,本文就不展开讲解了。因为展开来讲,又可以写篇新的文章了。只需要知道主要功能是监测数据改变,变更视图即可。

这也就算解答了开头提出的第一个问题。

跟着断点我们继续看app.use()方法,Vue提供的插件机制。

# 4.3 app.use() 方法

use做的事情说起来也算简单,把传递过来的插件添加插件集合中,到防止重复。

执行插件,如果是对象,install是函数,则把参数app和其他参数传递给install函数执行。如果是函数直接执行。

// webpack:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js
function createAppAPI(render, hydrate) {
    return function createApp(rootComponent, rootProps = null) {
      // 代码有删减
      const installedPlugins = new Set();
      const app = (context.app = {
        use(plugin, ...options) {
          // 已经有插件,并且 不是生产环境,报警告。
            if (installedPlugins.has(plugin)) {
                (process.env.NODE_ENV !== 'production') && warn(`Plugin has already been applied to target app.`);
            }
            // 插件的install 是函数,则添加插件,并执行 install 函数
            else if (plugin && isFunction(plugin.install)) {
                installedPlugins.add(plugin);
                // 断点
                plugin.install(app, ...options);
            }
            // 插件本身 是函数,则添加插件,并执行 插件本身函数
            else if (isFunction(plugin)) {
                installedPlugins.add(plugin);
                plugin(app, ...options);
            }
            // 如果都不是报警告
            else if ((process.env.NODE_ENV !== 'production')) {
                warn(`A plugin must either be a function or an object with an "install" ` +
                    `function.`);
            }
            // 支持链式调用
            return app;
        },
        provide(){ 
          // 省略... 后文再讲
        }
      });
    }
}

上面代码中,断点这行plugin.install(app, ...options);

跟着断点走到下一步,install函数。

# 4.4 install 函数

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

Vuex4中的install函数相对比Vuex3中简单了许多。 第一句是给Composition API提供的。注入到根实例对象中。 第二句则是为option API提供的。

接着断点这两句,按F11来看app.provide实现。

# 4.4.1 app.provide

简单来说就是给contextprovides属性中加了store = Store实例对象

provide(key, value) {
    // 如果已经有值了警告
    if ((process.env.NODE_ENV !== 'production') && key in context.provides) {
        warn(`App already provides property with key "${String(key)}". ` +
            `It will be overwritten with the new value.`);
    }
    // TypeScript doesn't allow symbols as index type
    // https://github.com/Microsoft/TypeScript/issues/24587
    context.provides[key] = value;
    return app;
}

接着从上方代码中搜索context,可以发现这一句代码:

const context = createAppContext();

接着我们来看函数 createAppContextcontext 为上下文

// webpack:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js
function createAppContext() {
    return {
        app: null,
        config: {
            isNativeTag: NO,
            performance: false,
            globalProperties: {},
            optionMergeStrategies: {},
            isCustomElement: NO,
            errorHandler: undefined,
            warnHandler: undefined
        },
        mixins: [],
        components: {},
        directives: {},
        provides: Object.create(null)
    };
}

Vue3 文档应用配置(app.config) (opens new window)

# 4.4.2 app.config.globalProperties

app.config.globalProperties 官方文档 (opens new window)

用法:

app.config.globalProperties.$store = {}

app.component('child-component', {
  mounted() {
    console.log(this.$store) // '{}'
  }
})

也就能解释为什么每个组件都可以使用 this.$store.xxx 访问 vuex中的方法和属性了。

也就是说在appContext.provides中注入了一个Store实例对象。这时也就是相当于根组件实例和config全局配置globalProperties中有了Store实例对象

至此我们就看完,createStore(store)app.use(store)两个API

app.provide 其实是用于composition API使用的。

但这只是文档中这样说的,为什么就每个组件实例都能访问的呢,我们继续深入探究下原理。

接下来,我们看下源码具体实现,为什么每个组件实例中都能获取到的。

这之前先来看下组合式API中,我们如何使用Vuex4,这是线索。

# 4.5 composition API 中如何使用Vuex 4

接着我们找到如下文件,useStore是我们断点的对象。

// webpack:///./examples/composition/shopping-cart/components/ShoppingCart.vue
import { computed } from 'vue'
import { useStore } from 'vuex'
import { currency } from '../currency'

export default {
  setup () {
    const store = useStore()

    // 我加的这行代码
    window.ShoppingCartStore = store;
    // 省略了若干代码
  }
}

接着断点按F11,单步调试,会发现最终是使用了Vue.inject方法。

# 4.5.1 Vuex.useStore 源码实现

// vuex/src/injectKey.js
import { inject } from 'vue'

export const storeKey = 'store'

export function useStore (key = null) {
  return inject(key !== null ? key : storeKey)
}

# 4.5.2 Vue.inject 源码实现

接着看inject函数,看着代码很多,其实原理很简单,就是要找到我们用provide提供的值。

如果没有父级,也就是根实例,就取实例对象中的vnode.appContext.provides。 否则就取父级中的instance.parent.provides的值。

Vuex4源码里则是:Store实例对象。

// webpack:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js
function inject(key, defaultValue, treatDefaultAsFactory = false) {
    // fallback to `currentRenderingInstance` so that this can be called in
    // a functional component
    // 如果是被一个函数式组件调用则取 currentRenderingInstance
    const instance = currentInstance || currentRenderingInstance;
    if (instance) {
        // #2400
        // to support `app.use` plugins,
        // fallback to appContext's `provides` if the intance is at root
        const provides = instance.parent == null
            ? instance.vnode.appContext && instance.vnode.appContext.provides
            : instance.parent.provides;
        if (provides && key in provides) {
            // TS doesn't allow symbol as index type
            return provides[key];
        }
        // 如果参数大于1个 第二个则是默认值 ,第三个参数是 true,并且第二个值是函数则执行函数。
        else if (arguments.length > 1) {
            return treatDefaultAsFactory && isFunction(defaultValue)
                ? defaultValue()
                : defaultValue;
        }
        // 警告没找到
        else if ((process.env.NODE_ENV !== 'production')) {
            warn(`injection "${String(key)}" not found.`);
        }
    }
    // 如果没有当前实例则说明则报警告。
    // 也就是是说inject必须在setup中调用或者在函数式组件中使用
    else if ((process.env.NODE_ENV !== 'production')) {
        warn(`inject() can only be used inside setup() or functional components.`);
    }
}

接着我们继续来看inject的相对应的provide

# 4.5.3 Vue.provide 源码实现

provide函数作用其实也算简单,1、也就是给当前组件实例上的provides对象属性,添加键值对key/value

2、还有一个作用是当当前组件和父级组件的provides相同时,在当前组件实例中的provides对象和父级,则建立链接,也就是原型[[prototype]],(__proto__)。

// webpack:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js
function provide(key, value) {
    if (!currentInstance) {
        if ((process.env.NODE_ENV !== 'production')) {
            warn(`provide() can only be used inside setup().`);
        }
    }
    else {
        let provides = currentInstance.provides;
        // by default an instance inherits its parent's provides object
        // but when it needs to provide values of its own, it creates its
        // own provides object using parent provides object as prototype.
        // this way in `inject` we can simply look up injections from direct
        // parent and let the prototype chain do the work.
        const parentProvides = currentInstance.parent && currentInstance.parent.provides;
        if (parentProvides === provides) {
            provides = currentInstance.provides = Object.create(parentProvides);
        }
        // TS doesn't allow symbol as index type
        provides[key] = value;
    }
}

provide函数中的这段,可能不是那么好理解。

if (parentProvides === provides) {
    provides = currentInstance.provides = Object.create(parentProvides);
}

我们来举个例子消化一下。

var currentInstance = { provides: { store: { __state: 'Store实例' }  } };
var provides = currentInstance.provides;
// 这句是我手动加的,在后文中则是创建实例时就是写的同一个对象,当然就会相等了。
var parentProvides = provides;
if(parentProvides === provides){
    provides =  currentInstance.provides = Object.create(parentProvides);
}

经过一次执行这个后,currentInstance 就变成了这样。

{
  provides: {
    // 可以容纳其他属性,比如用户自己写的
    __proto__ : { store: { __state: 'Store实例' }  }
  }
}

执行第二次时,currentInstance 则是:

{
  provides: {
    // 可以容纳其他属性,比如用户自己写的
    __proto__: {
        // 可以容纳其他属性,比如用户自己写的
        __proto__ : { store: { __state: 'Store实例' }  }
    }
  }
}

以此类推,多执行provide几次,原型链就越长。

上文injectprovide函数中都有个变量currentInstance当前实例,那么当前实例对象是怎么来的呢。

为什么每个组件就能访问到,依赖注入的思想。 有一个讨巧的方法,就是在文件runtime-core.esm-bundler.js中搜索provides,则能搜索到createComponentInstance函数

接下来我们createComponentInstance函数如何创建组件实例。

# 4.6 createComponentInstance 创建组件实例

可以禁用其他断点,单独断点这里, 比如:const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext; 来看具体实现。

// webpack:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js
const emptyAppContext = createAppContext();
let uid$1 = 0;
function createComponentInstance(vnode, parent, suspense) {
    const type = vnode.type;
    const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
    const instance = {
        uid: uid$1++,
        vnode,
        type,
        parent,
        appContext,
        root: null,
        next: null,
        subTree: null,
        // ...
        provides: parent ? parent.provides : Object.create(appContext.provides),
        // ...
    }
    instance.root = parent ? parent.root : instance;
    // ...
    return instance;
}

断点时会发现,根组件实例时vnode已经生成,至于是什么时候生成的,我整理了下简化版。

// 把上文中的 appContext 赋值给了 `appContext`
mount(rootContainer, isHydrate) {
    if (!isMounted) {
        const vnode = createVNode(rootComponent, rootProps);
        // store app context on the root VNode.
        // this will be set on the root instance on initial mount.
        vnode.appContext = context;
    }
},

其中 Object.create 其实就是建立原型关系。这时放一张图,一图胜千言。

直观的图,出自黄轶老师拉勾专栏,本想自己画一张图,但觉得这张挺好的。

# 4.6.1 组件实例生成了,那怎么把它们结合呢

这时,也有一个讨巧的方法,在runtime-core.esm-bundler.js文件中,搜索 provide(可以搜到如下代码:

这段代码其实看起来很复杂的样子,实际上主要就是把用户在组件中写的provides对象或者函数返回值遍历, 生成类似这样的实例对象:

// 当前组件实例
{
  parent: '父级的实例',
  provides: {
    // 可以容纳其他属性,比如用户自己写的
    __proto__: {
        // 可以容纳其他属性,比如用户自己写的
        __proto__ : { store: { __state: 'Store实例' }  }
    }
  }
}
// webpack:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js
function applyOptions(instance, options, deferredData = [], deferredWatch = [], deferredProvide = [], asMixin = false) {
  // ...
  if (provideOptions) {
      deferredProvide.push(provideOptions);
  }
  if (!asMixin && deferredProvide.length) {
      deferredProvide.forEach(provideOptions => {
          // 组件中写 provides 可以是对象或者是函数
          const provides = isFunction(provideOptions)
              ? provideOptions.call(publicThis)
              : provideOptions;
          Reflect.ownKeys(provides).forEach(key => {
              provide(key, provides[key]);
          });
      });
  }
  // ...
}

这样一来就从上到下app.provide提供的对象,被注入到每一个组件实例中了。同时组件本身提供的provides也被注入到实例中了。

接着我们跟着项目来验证下,上文中的表述。翻看Vue3文档可以发现有一个API可以获取当前组件实例。

# 4.7 getCurrentInstance 获取当前实例对象

getCurrentInstance 支持访问内部组件实例,用于高阶用法或库的开发。

import { getCurrentInstance } from 'vue'

const MyComponent = {
  setup() {
    const internalInstance = getCurrentInstance()

    internalInstance.appContext.config.globalProperties // 访问 globalProperties
  }
}

知道这个API后,我们可以在购物车例子的代码中添加一些代码。便于我们理解。

// vuex/examples/composition/shopping-cart/components/App.vue
import { getCurrentInstance, provide } from 'vue'
import { useStore } from 'vuex';
setup () {
  const store = useStore()
  provide('ruochuan12', '微信搜索「若川视野」关注我,专注前端技术分享。')

  window.AppStore = store;
  window.AppCurrentInstance = getCurrentInstance();
},
// vuex/examples/composition/shopping-cart/components/ProductList.vue
setup(){
  const store = useStore()

  // 若川加入的调试代码--start
  window.ProductListStore = store;
  window.ProductListCurrentInstance = getCurrentInstance();
  provide('weixin-2', 'ruochuan12');
  provide('weixin-3', 'ruochuan12');
  provide('weixin-4', 'ruochuan12');
  const mp = inject('ruochuan12');
  console.log(mp, '介绍-ProductList'); // 微信搜索「若川视野」关注我,专注前端技术分享。
  // 若川加入的调试代码---end
}
// vuex/examples/composition/shopping-cart/components/ShoppingCart.vue
setup () {
    const store = useStore()

    // 若川加入的调试代码--start
    window.ShoppingCartStore = store;
    window.ShoppingCartCurrentInstance = getCurrentInstance();
    provide('weixin', 'ruochuan12');
    provide('weixin1', 'ruochuan12');
    provide('weixin2', 'ruochuan12');
    const mp = inject('ruochuan12');
    console.log(mp, '介绍-ShoppingList'); // 微信搜索「若川视野」关注我,专注前端技术分享。
    // 若川加入的调试代码--start
}

在控制台输出这些值

AppCurrentInstance
AppCurrentInstance.provides
ShoppingCartCurrentInstance.parent === AppCurrentInstance // true
ShoppingCartCurrentInstance.provides
ShoppingCartStore === AppStore // true
ProductListStore === AppStore // true
AppStore // store实例对象

控制台输出的结果

看控制台截图输出的例子,其实跟上文写的类似。这时如果写了顺手自己注入了一个provide('store': '空字符串'),那么顺着原型链,肯定是先找到用户写的store,这时Vuex无法正常使用,就报错了。

当然vuex4提供了注入的key可以不是store的写法,这时就不和用户的冲突了。

export class Store{
    // 省略若干代码...
    install (app, injectKey) {
        // 为 composition API 中使用
        //  可以传入 injectKey  如果没传取默认的 storeKey 也就是 store
        app.provide(injectKey || storeKey, this)
        // 为 option API 中使用
        app.config.globalProperties.$store = this
    }
    // 省略若干代码...
}
export function useStore (key = null) {
  return inject(key !== null ? key : storeKey)
}

# 5. 解答下开头提出的5个问题

统一解答下开头提出的5个问题:

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

答:使用Vue 中的 reactive 方法监测数据变化的。

class Store{
  constructor (options = {}){
    // 省略若干代码...
    this._modules = new ModuleCollection(options)
    const state = this._modules.root.state
    resetStoreState(this, state)
    // 省略若干代码...
  }
}
function resetStoreState (store, state, hot) {
  // 省略若干代码...
  store._state = reactive({
    data: state
  })
  // 省略若干代码...
}

2、Vuex4作为Vue的插件如何实现和Vue结合的。

答:app.use(store) 时会执行Store中的install方法,一句是为 composition API 中使用,提供Store实例对象到根实例中。一句则是注入到根实例的全局属性中,为 option API 中使用。它们都会在组件生成时,注入到每个组件实例中。

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

3、provideinject的如何实现的,每个组件如何获取到组件实例中的Store的。

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

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

// 有删减
function provide(){
    let provides = currentInstance.provides;
    const parentProvides = currentInstance.parent && currentInstance.parent.provides;
    if (parentProvides === provides) {
        provides = currentInstance.provides = Object.create(parentProvides);
    }
    provides[key] = value;
}
// 有删减
function inject(){
    const provides = instance.parent == null
        ? instance.vnode.appContext && instance.vnode.appContext.provides
        : instance.parent.provides;
    if (provides && key in provides) {
        return provides[key];
    }
}

也就是类似这样的实例:

// 当前组件实例
{
  parent: '父级的实例',
  provides: {
    // 可以容纳其他属性,比如用户自己写的
    __proto__: {
        // 可以容纳其他属性,比如用户自己写的
        __proto__ : { store: { __state: 'Store实例' }  }
    }
  }
}

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

答:渲染生成组件实例时,调用createComponentInstance,注入到组件实例的provides中。

function createComponentInstance(vnode, parent, suspense) {
    const type = vnode.type;
    const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
    const instance = {
        parent,
        appContext,
        // ...
        provides: parent ? parent.provides : Object.create(appContext.provides),
        // ...
    }
    // ...
    return instance;
}
  1. 你怎么知道那么多的

答:因为社区有人写了Vue4源码文章 (opens new window)

# 6. 总结

本文主要讲述了Vuex4Store实例注入到各个组件中的原理,展开讲述了Vuex4相对与Vuex3安装方式的改变Vuex.createStoreapp.use(store) ,深入源码分析Vue.injectVue.provide实现原理。

Vuex4 除了安装方式和监测数据变化方式使用了Vue.reactive,其他基本和Vuex3.x版本没什么区别。

最后回顾下文章开头的图,可以说就是原型链的妙用。

provide,inject示例图

是不是觉得豁然开朗。

Vuex其实也是Vue的一个插件,知晓了Vuex原理,对于自己给Vue写插件也是会游刃有余。

如果读者朋友发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出,也欢迎加我微信 ruochuan12 交流。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对我的一种支持,万分感谢。如果能关注我的前端公众号:「若川视野」 (opens new window),就更好啦。

# 关于

你好,我是若川 (opens new window),微信搜索「若川视野」 (opens new window)关注我,专注前端技术分享,一个愿景是帮助5年内前端开阔视野走向前列的公众号。欢迎加我微信ruochuan12,长期交流学习。 主要有以下系列文章:学习源码整体架构系列 (opens new window)年度总结 (opens new window)JS基础系列 (opens new window)

# 参考链接

官网文档 Provide / Inject (opens new window)
github 仓库 provide/inject 源码 (opens new window)
github 仓库 provide/inject 测试 (opens new window)
Vuex 4 官方中文文档 (opens new window)

最后更新时间: 5/19/2021, 1:13:16 AM