看了 vuex4 源碼後,vuex4 和 provide/inject 原來就是妙用了原型鏈?

1. 前言

你好,我是若川,微信搜索「若川視野」關注我,專一前端技術分享,一個願景是幫助5年內前端開闊視野走向前列的公衆號。歡迎加我微信ruochuan12,長期交流學習。html

這是學習源碼總體架構系列 之 vuex4 源碼(第十篇)。學習源碼總體架構系列文章(有哪些必看的JS庫):jQueryunderscorelodashsentryvuexaxioskoareduxvue-devtools 直接打開文件功能揭祕前端

10篇源碼系列文章小成就達成,從19年7月開始寫,19年寫了6篇,20年寫了2篇,今年寫了2篇。算是一個完結吧。短期內應該暫時不更新這個系列了。主要是投入的時間和精力比較多,看的人不多,獲得的反饋也比較少。以後先寫其餘文章吧。歡迎持續關注我(若川)。vue

本文倉庫地址git clone https://github.com/lxchuan12/vuex4-analysis.git,本文最佳閱讀方式,克隆倉庫本身動手調試,容易吸取消化。node

要是有人說到怎麼讀源碼,正在讀文章的你能推薦個人源碼系列文章,那真是無覺得報啊react

個人文章,儘可能寫得讓想看源碼又不知道怎麼看的讀者能看懂。我都是推薦使用搭建環境斷點調試源碼學習哪裏不會點哪裏邊調試邊看,而不是硬看。正所謂:授人與魚不如授人予漁webpack

閱讀本文後你將學到:ios

    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面板,寫的很詳細。順帶提一下,我打開的設置,source面板中支持展開搜索代碼塊(默認不支持),一圖勝千言。
code-foldinggit

谷歌瀏覽器是咱們前端經常使用的工具,因此建議你們深刻學習,畢竟工欲善其事,必先利其器github

以前寫過Vuex 3的源碼文章學習 vuex 源碼總體架構,打造屬於本身的狀態管理庫若川的博客Vuex源碼,倉庫有很詳細的註釋和看源碼方法,因此本文不會過多贅述與Vuex 3源碼相同的地方。web

1.1 本文閱讀最佳方式

把個人vuex4源碼倉庫 git clone https://github.com/lxchuan12/vuex4-analysis.git克隆下來,順便star一下個人vuex4源碼學習倉庫^_^。跟着文章節奏調試和示例代碼調試,用chrome動手調試印象更加深入。文章長段代碼不用細看,能夠調試時再細看。看這類源碼文章百遍,可能不如本身多調試幾遍,大膽猜想,當心求證。也歡迎加我微信交流ruochuan12

2. Vuex 原理簡述

結論先行Vuex原理能夠拆解爲三個關鍵點。
第一點、其實就是每一個組件實例裏都注入了Store實例。
第二點、Store實例中的各類方法都是爲Store中的屬性服務的。
第三點、Store中的屬性變動觸發視圖更新。

本文主要講解第一點。第二點在個人上一篇文章學習 vuex 源碼總體架構,打造屬於本身的狀態管理庫詳細講了,本文就不贅述了。第三點兩篇文章都沒有詳細講述。

如下是一段簡短的代碼說明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、childInstance一、childInstance2 這些對象中的provides.store都改了。
// 由於共享着同一個store對象。

provide,inject示例圖

看了上面的官方文檔中的圖,大概知道是用provide父級組件中提供Store實例,用inject來獲取到Store實例。

那麼接下來,帶着問題:

一、爲何修改了實例store裏的屬性,變動後會觸發視圖更新。

二、Vuex4做爲Vue的插件如何實現和Vue結合的。

三、provideinject的如何實現的,每一個組件如何獲取到組件實例中的Store的。

四、爲何每一個組件對象裏都有Store實例對象了(渲染組件對象過程)。

五、爲何在組件中寫的provide提供的數據,能被子級組件獲取到。

TODO:
那麼每一個組件如何獲取組件實例中的Store實例,composition API中本質上則是使用inject函數。

全局的Store 實例對象。經過Vue.reactive()監測數據。

3. Vuex 4 重大改變

在看源碼以前,先來看下Vuex 4發佈的release和官方文檔遷移提到的重大改變,Vuex 4 release

從 3.x 遷移到 4.0

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 項目間雙向同步子項目,附簡明使用手冊

做爲讀者朋友的你,只需克隆個人Vuex 4源碼倉庫 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();

接着咱們來看函數 createAppContext
context 爲上下文

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

4.4.2 app.config.globalProperties

app.config.globalProperties 官方文檔

用法:

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函數做用其實也算簡單,一、也就是給當前組件實例上的provides對象屬性,添加鍵值對key/value

二、還有一個做用是噹噹前組件和父級組件的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個問題:

一、爲何修改了實例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
  })
  // 省略若干代碼...
}

二、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
    }
    // 省略若干代碼...
}

三、provideinject的如何實現的,每一個組件如何獲取到組件實例中的Store的。

五、爲何在組件中寫的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實例' }  }
    }
  }
}

四、爲何每一個組件對象裏都有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源碼文章

6. 總結

本文主要講述了Vuex4Store實例注入到各個組件中的原理,展開講述了Vuex4相對與Vuex3安裝方式的改變Vuex.createStoreapp.use(store) ,深刻源碼分析Vue.injectVue.provide實現原理。

Vuex4 除了安裝方式和監測數據變化方式使用了Vue.reactive,其餘基本和Vuex3.x版本沒什麼區別。

最後回顧下文章開頭的圖,能夠說就是原型鏈的妙用。

provide,inject示例圖

是否是以爲豁然開朗。

Vuex其實也是Vue的一個插件,知曉了Vuex原理,對於本身給Vue寫插件也是會遊刃有餘。

若是讀者朋友發現有不妥或可改善之處,再或者哪裏沒寫明白的地方,歡迎評論指出,也歡迎加我微信 ruochuan12 交流。另外以爲寫得不錯,對您有些許幫助,能夠點贊、評論、轉發分享,也是對個人一種支持,萬分感謝。若是能關注個人前端公衆號: 「若川視野」,就更好啦。

關於

你好,我是 若川,微信搜索 「若川視野」關注我,專一前端技術分享,一個願景是幫助5年內前端開闊視野走向前列的公衆號。歡迎加我微信 ruochuan12,長期交流學習。
主要有如下系列文章: 學習源碼總體架構系列年度總結JS基礎系列

參考連接

官網文檔 Provide / Inject

github 倉庫 provide/inject 源碼

github 倉庫 provide/inject 測試

Vuex 4 官方中文文檔

相關文章
相關標籤/搜索