Vue3 Reactivityの実装方法を教えます

Vue3 Reactivityの実装方法を教えます

序文

Vue3 の応答性は Proxy に基づいています。Vue2 で使用されていた Object.definedProperty メソッドと比較すると、Proxy を使用すると、新しく追加されたオブジェクトや配列をインターセプトするためのサポートが優れています。

Vue3 のレスポンシブ性は、抽出して使用できる独立したシステムです。では、どのように実現されるのでしょうか?

Getter と Setter については誰もが知っていますが、応答性を実現するための Getter と Setter の主な操作は何でしょうか?

ふむ、これらの質問を一緒に見ていきましょう。この記事では、完全なレスポンシブ システムを段階的に実装していきます (間違い)~。

始める

observer-util ライブラリは、Vue3 と同じアイデアを使用して書かれています。Vue3 での実装はより複雑です。より純粋なライブラリから始めましょう (Vue3 には理解できないことがいくつかあるため、これを認めるつもりはありません)。

公式サイトの例によると:

'@nx-js/observer-util' から { observable, observe } をインポートします。

const カウンター = observable({ num: 0 });
const countLogger = observe(() => console.log(counter.num));

// countLoggerを呼び出して1をログに記録します
カウンター.num++;

これら 2 つは、Vue3 のリアクティブと通常のレスポンシブに似ています。

observable 以降のオブジェクトはプロキシで追加され、依存プロパティが変更されると、オブザーバーに追加されたレスポンス関数が 1 回呼び出されます。

ちょっとした考え

ここでの大まかなアイデアは、サブスクリプションとパブリッシングのモデルです。observable によってプロキシされた後のオブジェクトは、パブリッシャー ウェアハウスを確立します。この時点で、Observe は counter.num をサブスクライブし、サブスクライブされたコンテンツが変更されるたびに 1 つずつコールバックします。
疑似コード:

// リスナーを追加 xxx.addEventListener('counter.num', () => console.log(counter.num))
// コンテンツを変更する counter.num++
//通知を送信xxx.emit('counter.num', counter.num)

応答性の核心はこれです。リスナーの追加と通知の送信は、observable と observe を通じて自動的に完了します。

コードの実装

上記の考慮事項に基づいて、Getter では、observe によって渡されたコールバックをサブスクリプション ウェアハウスに追加する必要があります。
具体的な実装では、observableは監視対象オブジェクトのハンドラを追加します。Getterハンドラには、

registerRunningReactionForOperation({ ターゲット、キー、レシーバー、タイプ: 'get' })
const connectionStore = 新しい WeakMap()
// 反応は互いに呼び出して呼び出しスタックを形成できます
定数反応スタック = []

// 現在実行中の反応を登録し、obj.key の変更時に再度キューに入れます
エクスポート関数 registerRunningReactionForOperation (操作) {
  // スタックの上から現在の反応を取得します
  const runningReaction = 反応スタック[反応スタックの長さ - 1]
  if (実行中の反応) {
    デバッグ操作(実行中の反応、操作)
    オペレーションの反応を登録します(実行中の反応、操作)
  }
}

この関数は反応 (つまり、observe によって渡されたコールバック) を取得し、registerReactionForOperation を通じて保存します。

エクスポート関数 registerReactionForOperation (反応、{ターゲット、キー、タイプ}) {
  if (type === 'iterate') {
    キー = ITERATION_KEY
  }

  const 反応ForObj = connectionStore.get(ターゲット)
  反応ForKey = 反応ForObj.get(キー) とします。
  if (!reactionsForKey) {
    反応ForKey = 新しいSet()
    反応オブジェクトを設定します(キー、反応キー)
  }
  // キーが現在の実行中に反応によって使用されているという事実を保存します
  if (!reactionsForKey.has(reaction)) {
    反応ForKey.add(反応)
    反応クリーナーをプッシュします(反応キー)
  }
}

ここでSetが生成されます。実際の業務で使われるキーに応じて、反応がSetに追加されます。全体の構造は次のようになります。

接続ストア<弱いマップ>: {
    // ターゲット例: {num: 1}
    ターゲット: <マップ>{
        数値: (反応1、反応2...)
    }
}

ここでの反応、const runningReaction = reactionStack[reactionStack.length - 1] は、グローバル変数 reactionStack を通じて取得されることに注意してください。

エクスポート関数 observe (fn, options = {}) {
  // 渡された関数がまだ反応でない場合は、それを反応でラップします
  定数反応 = fn[IS_REACTION]
    ? 関数
    : 関数反応() {
      runAsReaction(反応、fn、this、引数) を返します。
    }
  // スケジューラとデバッガを反応時に保存する
  反応.スケジューラ = オプション.スケジューラ
  反応.デバッガー = オプション.デバッガー
  // これが反応であるという事実を保存する
  反応[IS_REACTION] = true
  // 遅延反応でない場合は、反応を 1 回実行します
  if (!options.lazy) {
    反応()
  }
  反応を返す
}

関数 runAsReaction (反応、関数、コンテキスト、引数) をエクスポートします。
  // 反応が観察されていない場合は、反応関係を構築しない
  (反応が観察されない場合){
    Reflect.apply(fn, context, args) を返す
  }

  // 反応スタックにまだ存在しない場合にのみ反応を実行します
  // TODO: 明示的に再帰的な反応を許可するように改善する
  (反応スタックのインデックス(反応)が -1 の場合)
    // (オブジェクト -> キー -> 反応) 接続を解放します
    // クリーナー接続をリセットします
    releaseReaction(反応)

    試す {
      // 反応を現在実行中のものとして設定します
      // これは、get トラップで (observable.prop -> reaction) ペアを作成するために必要です
      反応スタック.push(反応)
      Reflect.apply(fn, context, args) を返す
    ついに
      // 実行を停止するときに、常に現在実行中のフラグを反応から削除します
      反応スタック.ポップ()
    }
  }
}

runAsReaction では、着信リアクション (つまり、上記の const reaction = function() { runAsReaction(reaction) }) は、独自のラップされた関数を実行してスタックにプッシュし、fn を実行します。ここで、fn は自動的に応答する関数です。この関数を実行すると、自然に get がトリガーされ、このリアクションが reactionStack に存在します。ここで、fn に非同期コードが含まれている場合、try finally の実行順序は次のようになることに注意してください。

//tryの内容を実行します。
// return がある場合、戻り内容は実行されますが、戻りません。finally を実行した後に戻り、ここでブロックされることはありません。

関数テスト() {
    試す { 
        コンソールログ(1); 
        const s = () => { console.log(2); 戻り値 4; }; 
        s() を返します。
    ついに 
        コンソール.log(3) 
    }
}

// 1 2 3 4
コンソールログ(テスト())

したがって、非同期コードが Getter の前にブロックされて実行されると、依存関係は収集されません。

真似する

目標は、Vue で派生した computed だけでなく、observable と observe も実装することです。
Vue3の考え方を借りると、取得時の操作をtrack、設定時の操作をtrigger、コールバックをeffectと呼びます。

まずはガイドマップです。

関数createObserve(obj) {
    
    ハンドラを = {
        get: 関数 (ターゲット、キー、レシーバー) {
            結果 = Reflect.get(ターゲット、キー、レシーバー)
            トラック(ターゲット、キー、受信者)            
            結果を返す
        },
        設定: 関数 (ターゲット、キー、値、レシーバー) {
            result = Reflect.set(ターゲット、キー、値、レシーバー) とします。
            トリガー(ターゲット、キー、値、レシーバー)        
            結果を返す
        }
    }

    proxyObj = new Proxy(obj, handler) とします。

    proxyObj を返す
}

関数 observable(obj) {
    createObserve(obj) を返す
}

ここでは、Vue が再帰的なカプセル化を行うのと同じように、プロキシ カプセル化のレイヤーのみを作成しました。

違いは、カプセル化が 1 層だけの場合、外側の層の = 操作のみを検出でき、Array.push などの内側の層やネストされた置換は set や get を通過できないことです。

実装トラック

トラックでは、現在トリガーされているエフェクト、つまり、observe のコンテンツまたはその他のコンテンツをリレーションシップ チェーンにプッシュし、トリガーされたときにこのエフェクトを呼び出すことができるようにします。

定数ターゲットマップ = 新しい WeakMap()
アクティブエフェクトスタックを [] にします
アクティブエフェクトを有効にする

関数 track(ターゲット、キー、レシーバー?) {
    depMap = targetMap.get(target) とします。

    場合 (!depMap) {
        targetMap.set(target, (depMap = new Map()))
    }

    dep = depMap.get(キー) とします。

    場合 (!dep) {
        depMap.set(キー、(dep = new Set()))
    }

    (!dep.has(activeEffect))の場合{
        dep.add(アクティブエフェクト)
    }
}

targetMap は、weakMap です。weakMap を使用する利点は、監視可能なオブジェクトに他の参照がない場合、正しくガベージ コレクションされることです。このチェーンは、作成した追加コンテンツであり、元のオブジェクトが存在しない場合は存在し続けてはいけません。

最終的には次のようになります。

ターゲットマップ = {
    <プロキシまたはオブジェクト> 観測可能: <マップ>{
        <観測可能なキー> キー: ( 観測、観測、観測... )
    }
}

activeEffectStack と activeEffect は、データ交換に使用される 2 つのグローバル変数です。get では、get キーによって生成された Set に現在の activeEffect を追加して保存し、set 操作でこの activeEffect を取得して再度呼び出し、応答性を実現できるようにします。

トリガーの実装

関数トリガー(ターゲット、キー、値、レシーバー?) {
    depMap = targetMap.get(target) とします。

    場合 (!depMap) {
        戻る
    }

    dep = depMap.get(キー) とします。

    場合 (!dep) {
        戻る
    }

    dep.forEach((item) => item && item())
}

ここでのトリガーは、アイデアに従って最小限のコンテンツを実装し、get で追加されたエフェクトを 1 つずつ呼び出すだけです。

観察の実装

マインドマップによると、observe では渡された関数を activeEffectStack にプッシュし、関数を 1 回呼び出して get をトリガーする必要があります。

関数 observe(fn:Function) {
    定数 wrapFn = () => {

        定数反応 = () => {
            試す {
                アクティブエフェクト = fn     
                アクティブエフェクトスタック.push(fn)
                fn() を返す
            ついに
                アクティブエフェクトスタック.pop()
                アクティブエフェクト = アクティブエフェクトスタック[アクティブエフェクトスタックの長さ - 1]
            }
        }

        反応を返す()
    }

    wrapFn()

    wrapFnを返す
}

関数は間違いを起こす可能性があり、finally のコードにより、activeEffectStack 内の対応するものが正しく削除されることが保証されます。

テスト

p = observable({num: 0}) とします。
j = observe(() => {console.log("私は観察しています:", p.num);)
e = observe(() => {console.log("i am observe2:", p.num)}) とします。

// 私は観察しています: 1
// 私は観察2です: 1
p.num++

計算の実装

Vue で非常に便利なのは計算プロパティです。計算プロパティは他のプロパティに基づいて生成される新しい値であり、依存する他の値が変更されると自動的に変更されます。
ovserve を実装した後、computed はほぼ半分実装されました。

クラス computedImpl {
    プライベート_値
    プライベート_setter
    私的効果

    コンストラクタ(オプション) {
        this._value = 未定義
        this._setter = 未定義
        const { get, set } = オプション
        this._setter = 設定

        this.effect = 観察(() => {
            this._value = get()
        })
    }

    値を取得する() {
        this._value を返す
    }

    値を設定する (val) {
        this._setter && this._setter(val)
    }
}

関数計算(fnOrOptions) {

    オプション = {
        取得: null、
        設定: null
    }

    if (fnOrOptions 関数のインスタンス) {
        オプション.get = fnOrOptions
    } それ以外 {
        const { 取得、設定 } = fnOrOptions
        options.get = 取得
        オプション.set = 設定
    }

    新しいcomputedImpl(options)を返す
}

計算には 2 つの方法があります。1 つは computed(function) で、これは get として扱われます。もう 1 つは setter を設定する方法です。setter はコールバックに似ており、他の依存プロパティとは関係ありません。

p = observable({num: 0}) とします。
j = observe(() => {console.log("私は観察しています:", p.num); return `私は観察しています: ${p.num}`})
e = observe(() => {console.log("i am observe2:", p.num)}) とします。
let w = computed(() => { return 'I am computed 1:' + p.num })
v = 計算された({
    取得: () => {
        'テスト計算ゲッター' + p.num を返す
    },

    設定: (値) => {
        p.num = `計算されたセッター${val}をテストする`
    }
})

p.num++
// 私は観察しています: 0
// 私は観察2: 0
// 私は観察しています: 1
// 私は観察2です: 1
// 1:1で計算されます
console.log(w.値)
v.値 = 3000
console.log(w.値)
// 私は観察しています: テスト計算された setter3000
// 私は observe2: テスト計算された setter3000 です
// 計算済み 1:テスト計算済み setter3000
w.値 = 1000
// w にはセッターが設定されていないので効果がありません // 計算済みです 1:test computed setter3000
console.log(w.値)

Vue3 Reactivity の実装方法についての記事はこれで終わりです。Vue3 Reactivity に関するより関連性の高いコンテンツについては、123WORDPRESS.COM で過去の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • Vue3 のリアクティブ関数 toRef 関数 ref 関数の紹介
  • VueプロジェクトでReactを書く方法の詳細
  • Vue3.0 における Ref と Reactive の違いの詳細な分析
  • Vue3 における ref と reactive の詳細な説明と拡張
  • vue3 の setUp とリアクティブ関数の使用方法の詳細な説明
  • Vue3 の組み合わせ API における setup、ref、reactive の完全な使用方法
  • React、Angular、Vueの3つの主要なフロントエンド技術の詳細説明
  • VueとReactの違いと利点
  • Vue と React の違いは何ですか?
  • VueとReactの詳細

<<:  Ubuntu 14.04 で QT5 をインストール、設定、アンインストールするための詳細な手順

>>:  Linux MySQL ルートパスワードを忘れた場合の解決方法

推薦する

Vue でカスタムパスのエイリアスを設定する方法

Vue でカスタム パス エイリアスを設定する方法日常の開発では、モジュールやコンポーネントをインポ...

詳細なハードウェア情報を取得するための Linux のいくつかのコマンドの詳細な説明

Linux システム、特にサーバー システムでは、デバイスのハードウェア情報を表示する必要がよくあり...

SVNサービスバックアップ操作手順の共有

SVN サービスのバックアップ手順1. ソースサーバーとターゲットサーバーを準備するソースサーバー:...

Docker に ElasticSearch 6.x をインストールする詳細なチュートリアル

まず、イメージをプルします(またはコンテナを作成するだけで、自然にプルされます)。 docker p...

htmlダウンロード機能の詳しい説明

新しいプロジェクトは基本的に終了しました。フロントエンドとバックエンドを分離して統合を完了したのは初...

MySQL で乱数を生成し、文字列を連結する方法の例

この記事では、MySQL が乱数を生成し、文字列を連結する方法について例を使用して説明します。ご参考...

pagodaを使用してionCube拡張機能をインストールする方法

1. まずパゴダを設置するインストール要件: Python バージョン: 2.6/2.7 (Pago...

IDEA 2020 で Tomcat サーバーを構成するための詳細な手順

IDEA 2020 で Tomcat を構成する手順は次のとおりです。最初のステップはTomcatを...

Alibaba Cloud ESC に MYSQL8.0 をインストールするチュートリアル

接続ツールを開きます。私はMobaXterm_Personal_12.1を使用します(公式サイトのダ...

マークアップ言語 - 画像の置き換え

123WORDPRESS.COM HTML チュートリアル セクションに戻るには、ここをクリックして...

Linux で CPU 使用率が高くなる原因をトラブルシューティングするプロセスの詳細な説明

目次序文始めるステップトラブルシューティング序文CPU 使用率が高くなるのは、オンラインでよくある問...

CSSでプロセスナビゲーション効果を実現する(3つの方法)

CSS によりプロセスナビゲーション効果を実現します。具体的な内容は以下のとおりです。 ::tip...

行間隔が広い場合の解決策(IE では 5 ピクセル多い)

コードをコピーコードは次のとおりです。 li {幅:300px; 高さ:23px; 行の高さ:24p...

MySQL NULLデータ変換方法(必読)

MySQL を使用してデータベースをクエリし、左結合を実行すると、関連付けられたフィールドの一部に...

HTML内の画像はbase64でエンコードされた文字列に直接置き換えられます

最近、画像はあるのに外部画像リソースが参照されていないウェブページを見つけました。気になりました。コ...