1. 一文讀懂 Vuex4 源碼,原來 provide / inject 就是妙用了這個特性呀!

        共 34698字,需瀏覽 70分鐘

         ·

        2021-07-11 15:11

        1. 前言

        要是有人說到怎么讀源碼,正在讀文章的你能推薦我的源碼系列文章,那真是無以為報啊。

        我的文章,盡量寫得讓想看源碼又不知道怎么看的讀者能看懂。我都是推薦使用搭建環(huán)境斷點調試源碼學習哪里不會點哪里,邊調試邊看,而不是硬看。正所謂:授人與魚不如授人予漁

        閱讀本文后你將學到:

          git subtree 管理子倉庫
          如何學習Vuex 4源碼、理解Vuex原理
          Vuex 4Vuex 3 的異同
          Vuex 4 composition API 如何使用
          Vue.provide / Vue.inject API 使用和原理
          如何寫一個 Vue3 插件
          等等

        如果對于谷歌瀏覽器調試還不是很熟悉的讀者,可以看這篇文章chrome devtools source面板,寫的很詳細。順帶提一下,我打開的設置,source面板中支持展開搜索代碼塊(默認不支持),一圖勝千言。

        谷歌瀏覽器是我們前端常用的工具,所以建議大家深入學習,畢竟工欲善其事,必先利其器。

        之前寫過Vuex 3的源碼文章學習 vuex 源碼整體架構,打造屬于自己的狀態(tài)管理庫[2],倉庫有很詳細的注釋和看源碼方法,所以本文不會過多贅述與Vuex 3源碼相同的地方。

        1.1 本文閱讀最佳方式

        把我的vuex4源碼倉庫 git clone https://github.com/lxchuan12/vuex4-analysis.git克隆下來,順便star一下我的vuex4源碼學習倉庫[3]^_^。跟著文章節(jié)奏調試和示例代碼調試,用chrome動手調試印象更加深刻。文章長段代碼不用細看,可以調試時再細看??催@類源碼文章百遍,可能不如自己多調試幾遍,大膽猜測,小心求證。也歡迎加我微信交流ruochuan12。

        2. Vuex 原理簡述

        結論先行Vuex原理可以拆解為三個關鍵點。第一點、其實就是每個組件實例里都注入了Store實例。第二點、Store實例中的各種方法都是為Store中的屬性服務的。第三點、Store中的屬性變更觸發(fā)視圖更新。

        本文主要講解第一點。第二點在我的上一篇文章學習 vuex 源碼整體架構,打造屬于自己的狀態(tài)管理庫詳細講了,本文就不贅述了。第三點兩篇文章都沒有詳細講述。

        以下是一段簡短的代碼說明Vuex原理的。

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


        const store = new Store();
        var rootInstance = {
          parentnull,
          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里的屬性,變更后會觸發(fā)視圖更新。
        2、Vuex4作為Vue的插件如何實現(xiàn)和Vue結合的。
        3、provide、inject的如何實現(xiàn)的,每個組件如何獲取到組件實例中的Store的。
        4、為什么每個組件對象里都有Store實例對象了(渲染組件對象過程)。
        5、為什么在組件中寫的provide提供的數(shù)據,能被子級組件獲取到。

        3. Vuex 4 重大改變

        在看源碼之前,先來看下Vuex 4發(fā)布的release和官方文檔遷移提到的重大改變,Vuex 4 release[4]

        從 3.x 遷移到 4.0[5]

        Vuex 4的重點是兼容性。Vuex 4支持使用Vue 3開發(fā),并且直接提供了和Vuex 3完全相同的API,因此用戶可以在Vue 3項目中復用現(xiàn)有的Vuex代碼。

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

        3.1 安裝過程

        Vuex 3Vue.use(Vuex)

        Vuex 4則是app.use(store)

        import { createStore } from 'vuex'

        export const store = createStore({
          state() {
            return {
              count1
            }
          }
        })
        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 函數(shù)

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

        作為讀者朋友的你,只需克隆我的`Vuex 4`源碼倉庫[7] 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函數(shù)打上斷點。

        // 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)兩個方面發(fā)散開來講解。

        4.2 Vuex.createStore 函數(shù)

        相比 Vuex 3 中,new Vuex.Store,其實是一樣的。只不過為了和Vue 3 統(tǒng)一,Vuex 4 額外多了一個 createStore 函數(shù)。

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

        監(jiān)測數(shù)據

        Vuex 3不同的是,監(jiān)聽數(shù)據不再是用new Vue(),而是Vue 3提供的reactive方法。

        Vue.reactive 函數(shù)方法,本文就不展開講解了。因為展開來講,又可以寫篇新的文章了。只需要知道主要功能是監(jiān)測數(shù)據改變,變更視圖即可。

        這也就算解答了開頭提出的第一個問題。

        跟著斷點我們繼續(xù)看app.use()方法,Vue提供的插件機制。

        4.3 app.use() 方法

        use做的事情說起來也算簡單,把傳遞過來的插件添加插件集合中,到防止重復。

        執(zhí)行插件,如果是對象,install是函數(shù),則把參數(shù)app和其他參數(shù)傳遞給install函數(shù)執(zhí)行。如果是函數(shù)直接執(zhí)行。

        // 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) {
                  // 已經有插件,并且 不是生產環(huán)境,報警告。
                    if (installedPlugins.has(plugin)) {
                        (process.env.NODE_ENV !== 'production') && warn(`Plugin has already been applied to target app.`);
                    }
                    // 插件的install 是函數(shù),則添加插件,并執(zhí)行 install 函數(shù)
                    else if (plugin && isFunction(plugin.install)) {
                        installedPlugins.add(plugin);
                        // 斷點
                        plugin.install(app, ...options);
                    }
                    // 插件本身 是函數(shù),則添加插件,并執(zhí)行 插件本身函數(shù)
                    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函數(shù)。

        4.4 install 函數(shù)

        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函數(shù)相對比Vuex3中簡單了許多。第一句是給Composition API提供的。注入到根實例對象中。第二句則是為option API提供的。

        接著斷點這兩句,按F11來看app.provide實現(xiàn)。

        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,可以發(fā)現(xiàn)這一句代碼:

        const context = createAppContext();

        接著我們來看函數(shù) createAppContext。context 為上下文

        // webpack:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js
        function createAppContext({
            return {
                appnull,
                config: {
                    isNativeTag: NO,
                    performancefalse,
                    globalProperties: {},
                    optionMergeStrategies: {},
                    isCustomElement: NO,
                    errorHandlerundefined,
                    warnHandlerundefined
                },
                mixins: [],
                components: {},
                directives: {},
                providesObject.create(null)
            };
        }

        Vue3 文檔應用配置(app.config)[8]

        4.4.2 app.config.globalProperties

        app.config.globalProperties 官方文檔[9]

        用法:

        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使用的。

        但這只是文檔中這樣說的,為什么就每個組件實例都能訪問的呢,我們繼續(xù)深入探究下原理。

        接下來,我們看下源碼具體實現(xiàn),為什么每個組件實例中都能獲取到的。

        這之前先來看下組合式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,單步調試,會發(fā)現(xiàn)最終是使用了Vue.inject方法。

        4.5.1 Vuex.useStore 源碼實現(xiàn)

        // 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 源碼實現(xiàn)

        接著看inject函數(shù),看著代碼很多,其實原理很簡單,就是要找到我們用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
            // 如果是被一個函數(shù)式組件調用則取 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];
                }
                // 如果參數(shù)大于1個 第二個則是默認值 ,第三個參數(shù)是 true,并且第二個值是函數(shù)則執(zhí)行函數(shù)。
                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中調用或者在函數(shù)式組件中使用
            else if ((process.env.NODE_ENV !== 'production')) {
                warn(`inject() can only be used inside setup() or functional components.`);
            }
        }

        接著我們繼續(xù)來看inject的相對應的provide。

        4.5.3  Vue.provide 源碼實現(xiàn)

        provide函數(shù)作用其實也算簡單,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函數(shù)中的這段,可能不是那么好理解。

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

        我們來舉個例子消化一下。

        var currentInstance = { provides: { store: { __state'Store實例' }  } };
        var provides = currentInstance.provides;
        // 這句是我手動加的,在后文中則是創(chuàng)建實例時就是寫的同一個對象,當然就會相等了。
        var parentProvides = provides;
        if(parentProvides === provides){
            provides =  currentInstance.provides = Object.create(parentProvides);
        }

        經過一次執(zhí)行這個后,currentInstance 就變成了這樣。

        {
          provides: {
            // 可以容納其他屬性,比如用戶自己寫的
            __proto__ : { store: { __state'Store實例' }  }
          }
        }

        執(zhí)行第二次時,currentInstance 則是:

        {
          provides: {
            // 可以容納其他屬性,比如用戶自己寫的
            __proto__: {
                // 可以容納其他屬性,比如用戶自己寫的
                __proto__ : { store: { __state'Store實例' }  }
            }
          }
        }

        以此類推,多執(zhí)行provide幾次,原型鏈就越長。

        上文inject、provide函數(shù)中都有個變量currentInstance當前實例,那么當前實例對象是怎么來的呢。

        為什么每個組件就能訪問到,依賴注入的思想。有一個討巧的方法,就是在文件runtime-core.esm-bundler.js中搜索provides,則能搜索到createComponentInstance函數(shù)

        接下來我們createComponentInstance函數(shù)如何創(chuàng)建組件實例。

        4.6 createComponentInstance 創(chuàng)建組件實例

        可以禁用其他斷點,單獨斷點這里, 比如:const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;來看具體實現(xiàn)。

        // 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,
                rootnull,
                nextnull,
                subTreenull,
                // ...
                provides: parent ? parent.provides : Object.create(appContext.provides),
                // ...
            }
            instance.root = parent ? parent.root : instance;
            // ...
            return instance;
        }

        斷點時會發(fā)現(xiàn),根組件實例時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對象或者函數(shù)返回值遍歷, 生成類似這樣的實例對象:

        // 當前組件實例
        {
          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 可以是對象或者是函數(shù)
                  const provides = isFunction(provideOptions)
                      ? provideOptions.call(publicThis)
                      : provideOptions;
                  Reflect.ownKeys(provides).forEach(key => {
                      provide(key, provides[key]);
                  });
              });
          }
          // ...
        }

        這樣一來就從上到下app.provide提供的對象,被注入到每一個組件實例中了。同時組件本身提供的provides也被注入到實例中了。

        接著我們跟著項目來驗證下,上文中的表述。翻看Vue3文檔可以發(fā)現(xiàn)有一個API可以獲取當前組件實例。

        4.7 getCurrentInstance 獲取當前實例對象

        getCurrentInstance 支持訪問內部組件實例,用于高階用法或庫的開發(fā)。

        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個問題

        統(tǒng)一解答下開頭提出的5個問題:

        1、為什么修改了實例store里的屬性,變更后會觸發(fā)視圖更新。

        答:使用Vue 中的 reactive 方法監(jiān)測數(shù)據變化的。

        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的插件如何實現(xiàn)和Vue結合的。

        答:app.use(store) 時會執(zhí)行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、provide、inject的如何實現(xiàn)的,每個組件如何獲取到組件實例中的Store的。

        5、為什么在組件中寫的provide提供的數(shù)據,能被子級組件獲取到。

        答:provide函數(shù)建立原型鏈區(qū)分出組件實例用戶自己寫的屬性和系統(tǒng)注入的屬性。inject函數(shù)則是通過原型鏈找父級實例中的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. 你怎么知道那么多的

        答:因為社區(qū)有人寫了`Vue4`源碼文章[10]。

        6. 總結

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

        Vuex4 除了安裝方式和監(jiān)測數(shù)據變化方式使用了Vue.reactive,其他基本和Vuex3.x版本沒什么區(qū)別。

        最后回顧下文章開頭的圖,可以說就是原型鏈的妙用。

        provide,inject示例圖

        是不是覺得豁然開朗。

        Vuex其實也是Vue的一個插件,知曉了Vuex原理,對于自己給Vue寫插件也是會游刃有余。

        參考資料

        [1]

        本文倉庫地址: https://github.com/lxchuan12/vuex4-analysis.git

        [2]

        更多參考鏈接,可以點擊閱讀原文查看



        瀏覽 116
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 亚洲国产又黄又爽女人高潮的 | 青青操青青摸 | 91精品成人www | 男人舔女人下体视频 | 狠狠操狠狠色 |