blog

Vuexソースコード解析

最近、ふと思い立って、Vuexのソースコードを勉強してみました。 Vuexは、Vue.jsアプリケーション専用に開発された状態管理モデルとして、誰もがよく知っているはずです。そして、私たちは次のいくつ...

Feb 29, 2020 · 13 min. read
シェア

はじめに

このヘタレが最近ふと思い立ってVuexのソースコードを勉強したので、改めてちょっと整理して共有します。Vuexは、Vue.jsアプリケーション専用に開発された状態管理モデルとして、誰もが知っているはずです。では、開発の過程でちょっとした疑問を残しておこうと思ったことはないでしょうか:

  • VuexはどのようにVueにマウントされるのですか?
  • Vuexのデータレスポンシブの実装方法
  • Vuexのstrictモードはどうですか?
  • 州がすべてのサブコンポーネントでストアを使用できる理由

これらの疑問を念頭に置いて、Vuexのソースコードを見てみましょう:

まずは公式サイトのスクリーンショット:

追記:記事の最後に、自分で実装できるVuexの簡単なバージョンがあります。

Vuex

vueがプラグインを使う方法はVue.use(plugin)だけで、Vuexの場合はVue.use(Vuex)ですご存知の通り、Vueのソースコードはプラグイン部分をマウントし、パラメータにinstallメソッドがあればinstallメソッドを実行します。Vuexプラグインのinstallメソッドが何をするのかを見てみましょう:

ストア

export function install(_Vue) { //重複インストールを避ける if (Vue && _Vue === Vue) { if (process.env.NODE_ENV !== 'production') { console.error( '[vuex] already installed. Vue.use(Vuex) should be called only once.' ) } return } Vue = _Vue //Vueインスタンスを保存する //主な操作は、VuexInitメソッドをVueのbeforeCreateフック関数に混ぜることだ。 applyMixin(Vue) }

上のコードから、installメソッドがVueインスタンスを受け取り、Vuexがすでにインストールされているかどうかを判断し、そのVueインスタンスを保存し、最後にちょっとしたmixinを実行していることがわかります。このミキシンを見てみましょう:

mixin.js

export default function (Vue) { const version = Number(Vue.version.split('.')[0]) //Vueのバージョン番号に基づいてミキシンを制御する,2.0を混ぜた結果である。 if (version >= 2) { Vue.mixin({ beforeCreate: vuexInit }) } else { const _init = Vue.prototype._init Vue.prototype._init = function (options = {}) { options.init = options.init ? [vuexInit].concat(options.init) : vuexInit _init.call(this, options) } } //init初期化関数、主にグローバル変数をマウントする$store function vuexInit() { const options = this.$options //ポイントがフォロー・ノードであれば、optionsオプションから直接ストアを取得する。 if (options.store) { this.$store = typeof options.store === 'function' ? options.store() : options.store //ルート・ノードでない場合は、オプションのparentで親コンポーネントの親を取得する。$store引用 } else if (options.parent && options.parent.$store) { //子コンポーネントは、親コンポーネントの$store,各サブコンポーネントは、ストアオブジェクトにアクセスできる。 this.$store = options.parent.$store } } }

このメソッドは、まずVueのバージョンを検出し、2.0以上であれば、Vue.mixin()メソッドを使用して、初期化関数をVueのbeforeCreateフック関数にミックスします。Vueが2.0未満の場合、初期化関数はVueプロトタイプの_initプロパティにマウントされます。vueInit関数は、基本的にStoreインスタンスをVueインスタンスの$storeプロパティにマウントします。ルートノードであるかどうかを判断し、ルートノードであれば値「options.store」を取り、コンポーネントであれば親コンポーネントのStoreであるoptions.parent.$storeを取ります。これにより、すべての子コンポーネントが同じStoreをフェッチできるようになり、すべての子コンポーネントが同じStoreをフェッチできるようになります。オプションで指定します。これは、なぜstateがすべてのサブコンポーネントでstoreを使用できるのかという問題も解決します。 ご存知のように、options.storeはStoreのインスタンスです。では、Storeコンストラクタが何をするのか見てみましょう:

Store

store.jsのコンストラクタを見てみましょう:

このコンストラクタは、まずVuexがすでにインストールされているかどうかを判断し、インストールされていない場合はVuexプラグインを再インストールします。次に、現在の環境を検出します。

//ブラウザ環境では、プラグインがインストールされていなければ自動的にインストールされる if (!Vue && typeof window !== 'undefined' && window.Vue) { //つまり、Vueの下のウィンドウを使用するために、Vueが効いていないときに install(window.Vue) } //非本番環境を判断するには:Vueを呼び出して登録し、Promiseをサポートし、newを使ってStoreを作成している必要がある。 if (process.env.NODE_ENV !== 'production') { assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`) assert( typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.` ) assert(this instanceof Store, `store must be called with the new operator.`) }

この後、ストリクトモードが有効かどうか、アクション、ミューテーション、ゲッターなどの初期化など、一連のプロパティの初期化が続きます。この中で最も重要なのは ModuleCollection で、これはモジュール・オブジェクトを構築するために使われます。

const { plugins = [], //Vuexのストリクト・モードを有効にするかどうか。このモードでは、変異関数以外での状態の変更はエラーになる。 strict = false, } = options // ストリクト・モードで、ステートがミューテーションによって変更されたかどうかを判断するために使用される。 this._committing = false //actions,アクションを保存する this._actions = Object.create(null) //actionサブスクリプション機能 this._actionSubscribers = [] //mutations this._mutations = Object.create(null) //ゲッターを格納する this._wrappedGetters = Object.create(null) //モジュール・オブジェクトをビルドする this._modules = new ModuleCollection(options) //名前空間に従ってモジュールを格納する this._modulesNamespaceMap = Object.create(null) //購読者を保存する this._subscribers = [] //Vueインスタンスを保存し、その中で主にwatchを使用する this._watcherVM = new Vue() this._makeLocalGettersCache = Object.create(null)

プロパティを初期化したら、次はコミットメソッドとディスパッチメソッドを処理します。

const { dispatch, commit } = this //以下の2つのメソッドは、いずれもdispatch/commitメソッドのthisを変更している。 this.dispatch = function boundDispatch(type, payload) { return dispatch.call(store, type, payload) } //type:メソッド名、ペイロードオプション:設定項目 this.commit = function boundCommit(type, payload, options) { return commit.call(store, type, payload, options) }

モジュール性、データのレスポンシブ処理、プラグインの登録などに焦点を当てた一連の操作を紹介します。詳しく見ていきましょう:

// ストリクトモードを有効にするかどうか this.strict = strict //ルートモジュールの状態を取得する const state = this._modules.root.state //ルート・モジュールだけがroot属性を持つ //モジュール処理、すべてのサブモジュールの再帰的処理 installModule(this, state, [], this._modules.root) //vueインスタンスを使ってストアを初期化する._vm,状態をレスポンシブにし、ゲッターをコンピューテッド・プロパティにする resetStoreVM(this, state) //プラグインを登録する plugins.forEach((plugin) => plugin(this)) //デバッグツールの登録 const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools if (useDevtools) { devtoolPlugin(this) }

ここまでがストアコンストラクタのすべての内容で、主な操作は各種プロパティの初期化、モジュール処理、データ応答処理、その他いくつかの操作です。モジュールの実装を見てみましょう。

モジュール性

モジュール性とは何か、公式サイトからの引用です:

単一のステートツリーを使用するため、アプリケーションのすべてのステートは、1つの比較的大きなオブジェクトに集中化されます。アプリケーションが非常に複雑になると、ストアオブジェクトが非常に肥大化する可能性があります。この問題を解決するために、Vuexではストアをモジュールに分割することができます。各モジュールは、独自のステート、ミューテーション、アクション、ゲッター、さらにはネストされたサブモジュールを持ち、上から下へ同じように分割されます!

ストアのコンストラクタを見ればわかるように、モジュールの初期化は ModuleCollection が行います。

モジュールコレクション.js

コンストラクタは主にルートモジュールを登録します。

//ルートモジュールを登録する register(path, rawModule, runtime = true) { if (process.env.NODE_ENV !== 'production') { assertRawModule(path, rawModule) } //モジュールのコンストラクタを呼び出す const newModule = new Module(rawModule, runtime) if (path.length === 0) { //モジュールをrootにマウントする。 this.root = newModule } else { //子モジュールを登録し、子モジュールを親モジュールの_children属性 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) }) } }

登録関数の主な点は、Moduleのインスタンス化オブジェクトを構築し、子モジュールの状態をstore.state.rawChildModule経由で取得できるように、子モジュールを再帰的に処理することです。モジュール module.js の中身を見てみましょう。

export default class Module { constructor(rawModule, runtime) { //初期値はfalse this.runtime = runtime //ストレージ・サブモジュール this._children = Object.create(null) //元のモジュールを保存して後で使う this._rawModule = rawModule const rawState = rawModule.state //元のモジュールの状態を保存する this.state = (typeof rawState === 'function' ? rawState() : rawState) || {} } addChild() {} removeChild() {} getChild() {} update() {} //... }

Module の中身は実はとてもシンプルで、主にモジュールを操作するための様々なメソッドを提供しています。ここまでで、ModuleCollection の一般的な機能を理解しました。それは、Store を作成するために渡されたオプションを受け取り、そのオプションを使って Module オブジェクトを構築し、同時にそれを再帰的に処理するというものです。最後に、モジュールツリーを構築します。

モジュールツリーが構築されたら、引き続きStoreのコンストラクタを見てみましょう。今回はinstallModuleメソッドでモジュールツリーを処理しますが、installModule関数はかなり長いので、ここではコードの一部だけを貼り付けます:

function installModule(store, rootState, path, module, hot) { //ルート・モジュールかどうか、ルート・ノードのパス。=[] const isRoot = !path.length //モジュールの名前空間を取得する const namespace = store._modules.getNamespace(path) //名前空間があれば_moduleNamespaceMap if (module.namespaced) { if ( store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production' ) { console.error( `[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join( '/' )}` ) } store._modulesNamespaceMap[namespace] = module } // set state //ルート・ノードではなく、子コンポーネントのすべての状態を親のstateプロパティに設定する。 if (!isRoot && !hot) { //親の状態を取得する const parentState = getNestedState(rootState, path.slice(0, -1)) //現在のモジュール名を取得する const moduleName = path[path.length - 1] store._withCommit(() => { if (process.env.NODE_ENV !== 'production') { if (moduleName in parentState) { console.warn( `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join( '.' )}"` ) } } //Vuexフレームワークのアドレスは以下のとおりである。.set()メソッドは、現在のモジュールの状態を親の状態にマウントし、子モジュールをレスポンシブに設定する。 Vue.set(parentState, moduleName, module.state) }) } //コンテキスト・オブジェクトに値を代入し、ローカル・ディスパッチ・メソッドとコミット・メソッド、ゲッターとステートを設定する。 const local = (module.context = makeLocalContext(store, namespace, path)) //登録変異をトラバースする module.forEachMutation() //登録アクションを繰り返し実行する module.forEachAction() //登録されたゲッターを繰り返し処理する module.forEachGetter() //再帰処理モジュール module.forEachChild() }

以上が、installModule メソッドの一般的な内容です。ご覧のとおり、installModule関数はまず、現在のノードがルートノードであるかどうか、および名前空間namespaceが設定されているかどうかを判断します。ルートノードである場合は、モジュールをstore._modulesNamespaceMapにマウントします。そうでない場合は、子コンポーネントの状態を親のstateプロパティにマウントします。そうでない場合は、子コンポーネントの状態を親のstateプロパティにマウントします。ディスパッチ、コミット、ゲッター、ステートは、makeLocalContext メソッドを介してモジュールのコンテキストプロパティにマウントされます。

そして、返されたStoreコンストラクタresetStoreVM()の後続の操作は、次のようになります

レスポンシブ

Vuexのデータはレスポンシブであり、resetStoreVM()の主な機能はデータをレスポンシブにすることです。以下は、resetStoreVM store.jsの内容です。

//古いVueインスタンス・オブジェクトを格納し、その主な役割はステートの実装と応答性の計算である。 const oldVm = store._vm //Storeのゲッターを初期化する store.getters = {} store._makeLocalGettersCache = Object.create(null) const wrappedGetters = store._wrappedGetters const computed = {} //オブジェクト.defineProperty各ゲッターにgetメソッドを設定するdata interception関数は、実はdata proxyである。 forEachValue(wrappedGetters, (fn, key) => { // use computed to leverage its lazy-caching mechanism // direct inline function use will lead to closure preserving oldVm. // using partial to return function with only arguments preserved in closure environment. computed[key] = partial(fn, store) //ゲッターにプロパティを追加する Object.defineProperty(store.getters, key, { get: () => store._vm[key], enumerable: true, // for local getters }) })

上のコードからわかるように、restoreVMはまずストアインスタンスのゲッターを初期化し、次にObject.definePropertyのデータインターセプションを使ってデータプロキシを実装し、store.gettersを通してゲッターの内容にアクセスできるようにしています。

データ・プロキシに加えて、以下のものをご覧ください:

const silent = Vue.config.silent //今のところこれをtrueに設定する目的は、Vueインスタンスを newしたときに警告を出さないようにするためである。 Vue.config.silent = true //newVueのインスタンスオブジェクトで、Vueの内部レスポンシブ実装を利用して状態を登録し、計算されるので、VuexとVueは強く結合している。 store._vm = new Vue({ data: { $$state: state, }, computed, }) Vue.config.silent = silent //ストアの変更が変異によってのみ行われるようにするために、ストリクト・モードを使用する。 if (store.strict) { enableStrictMode(store) } //古いvmが存在する場合 if (oldVm) { //古いvmの状態を解除し、古いVueオブジェクトを破棄する if (hot) { store._withCommit(() => { oldVm._data.$$state = null }) } Vue.nextTick(() => oldVm.$destroy()) }

上のコードからわかるように、この後の処理は、Vueを通じてステートと計算応答性を実装し、ストリクトモードを有効にするかどうかを決定します。最後に、古いVueインスタンスがあれば、Vue.nextTick()メソッドによって破棄されます。

resetStoreVM()メソッドを要約すると、主にゲッター、ステート、および計算データの応答性のプロキシを実装します。また、ストリクト・モードが有効になっているかどうかも判断します。ストリクト・モードの実装を見てみましょう:enableStrictMode(store)。

ストリクト・パターン

ヴュークスの公式ウェブサイトからの引用です:

ストリクト・モードでは、突然変異関数によって引き起こされたのではない状態変化が発生するたびにエラーがスローされます。これにより、すべての状態変化をデバッグ・ツールで追跡できるようになります。

では、実際にストリクト・モード・トラッキングがどのように実装されているのか見てみましょう:store.js

function enableStrictMode(store) { store._vm.$watch( function () { return this._data.$$state }, () => { if (process.env.NODE_ENV !== 'production') { //アサーション検出、ストアのチェック_committingtrueであれば、mutationメソッドでは変更されない。 assert( store._committing, `do not mutate vuex store state outside mutation handlers.` ) } }, { deep: true, sync: true } ) }

enableStrictMode()メソッドの内容は非常にシンプルで、Storeインスタンスを受け取り、Storeインスタンスに実装されたVueの実装を使用して状態データをリッスンするというものです。ここで行う主なことは、store._committingを判定し、それがfalseの場合、データを変更するにはミューテーションを使用する必要があることをユーザーに伝えるエラーを投げることです。store._committingはStoreのコンストラクタで初期化されることが知られているので、store._committingがフラグ付けされる場所を見てみましょう:

//storeVuexフレームワークのcommitメソッドは、mutation文を実行する。 _withCommit(fn) { //保存されたコミット状態 const committing = this._committing //このコミットを行うために、trueに設定せず、strictモードで、ステートを直接変更すると、Vuexは不正なステートの変更に関する警告を生成する。 this._committing = true fn() //修正後に修正した状態を復元する this._committing = committing } }

このメソッドは実際にはストアのコミット・メソッドで、とてもシンプルです。このメソッドが最終的に実行するのは、入力された変異ステートメントです。ここでは_committingをtrueに設定しています。

Vuexのストリクトモードを要約すると、ストアが初期化されるときに、データが変異によって送信されたかどうかを判断する識別子が設定されます。ストリクトモードでは、Vue.watchはデータの変更をリッスンし、データが変異によって送信されていない場合はエラーをスローします。

commit(mutation) とディスパッチ

commit

知っての通り、コミットの主な目的は変異をコミットすることです。では、コミットが何をするのか、ここでより重要なコードを見てみましょう:

ストア

//commitこの関数は、まず適応処理を行い、次に現在のアクションタイプが存在するかどうかを判断し、存在する場合は_withCommit この関数は、対応する変異を実行する commit(_type, _payload, _options) { const { type, payload, options } = unifyObjectStyle(_type, _payload, _options) const mutation = { type, payload } //型に対応する変異メソッドを取得する。 const entry = this._mutations[type] if (!entry) { if (process.env.NODE_ENV !== 'production') { console.error(`[vuex] unknown mutation type: ${type}`) } return } //mutationのすべてのメソッドを実行する this._withCommit(() => { entry.forEach(function commitIterator(handler) { handler(payload) }) }) //すべての購読者に通知する this._subscribers(...) }

ご覧のように、コミットの主な目的は_withCommit()メソッドを呼び出して変異をコミットすることで、_withCommitは前述の_withCommitメソッドです。

dispatch

ディスパッチの役割はアクションを呼び出すことで、これはコミットと同じです。ただし、アクション(したがってアクションは非同期に実行できます)はPromiseなので、ディスパッチはすべてのアクション関数を実行するためにPromise.allメソッドを呼び出す必要があります。以下はディスパッチとコミットの違いを示すコードです: store.js

try { this._actionSubscribers .slice() .filter((sub) => sub.before) .forEach((sub) => sub.before(action, this.state)) } catch (e) { if (process.env.NODE_ENV !== 'production') { console.warn(`[vuex] error in before action subscribers: `) console.error(e) } } //配列であれば、Promiseをラップして新しいPromiseを形成し、1つしかなければ0番目のPromiseを返す。 const result = entry.length > 1 ? Promise.all(entry.map((handler) => handler(payload))) : entry[0](payload) //最後の Promiseを実行する。 return result.then((res) => { try { this._actionSubscribers .filter((sub) => sub.after) .forEach((sub) => sub.after(action, this.state)) } catch (e) { if (process.env.NODE_ENV !== 'production') { console.warn(`[vuex] error in after action subscribers: `) console.error(e) } } return res })

ツール機能

通常のmapState、mapAction、mapGettersなどは、すべてhelper.jsファイルに実装されています。これらのメソッドの実装は似ています。要するに、すべて渡された名前空間を受け取り、store._modulesNamespaceMapを呼び出して対応するステート、アクション、その他のデータをマッピングして取得します。

やり方はとても簡単なので、ここでは簡単な紹介にとどめ、コードの貼り付けは省略します。

自分でVuexを作る

Vuexのコードはまだ比較的シンプルで、コード量も少ないです。Vuexの公式サイトと合わせてドキュメントとして閲覧することができます。Vuexのソースコードを解析することで、簡単なVuexフレームワークを実装し、Vuexフレームワークの理解を深めることができます。簡単なVuexフレームワークのアドレスはこちらです:

Read next

フロントエンドVIの再学習 - HTTPリクエスト

ウェブサービスとエンドユーザー間のインターフェース。 データ表現、セキュリティ、圧縮。 セッションの確立、管理、終了。 データ転送用のプロトコル・ポート番号、フロー制御、エラー・チェックの定義。 論理アドレス指定、異なるネットワーク間のパス選択 論理接続の確立、ハードウェアアドレス指定、エラーチェック。 ビットをバイトに結合し、...

Feb 29, 2020 · 4 min read