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 ルートパスワードを忘れた場合の解決方法

推薦する

JavaScript の矢印関数と通常の関数の違いの詳細な説明

この記事では、JavaScriptにおけるアロー関数と通常の関数の違いについて解説します。具体的な内...

Ubuntu 18.04 Linux システムに JDK と Mysql をインストールする方法

プラットフォームの展開1. JDKをインストールするステップ1. OracleJDKをダウンロードす...

docker-compose.yml ファイル内の一般的なテンプレート コマンドの詳細な説明

注意: docker-compose.yml ファイルを書き込むときは、すべてのコロン (:) とダ...

ネイティブ js はフォームの定期的な検証を実装します (検証後にのみ送信)

以下の機能が実装されています。 1. ユーザー名: onfouc は msg ルールを表示します。o...

CentOS 7 で MySQL 5.7 をインストールして設定する

この記事では、以下の環境をテストします。 CentOS 7 64 ビット 最小 MySQL 5.7 ...

JavaScript における継承の 3 つの方法

継承する1. 継承とは何か継承: まず、継承とは関係、つまりクラス間の関係です。JS にはクラスはあ...

Tomcat9 Windows サービスのインストールに関する詳細なチュートリアル

1. 準備1.1 service.bat を含む tomcat 圧縮パッケージをダウンロードします。...

Nginxを使ってサーバー内で複数コンテナの共存を実現する方法

背景Tencent Linux クラウド ホストがあり、その上に Docker (ServiceDo...

Vueはシンプルなマーキー効果を実装します

この記事では、Vueの具体的なコードを共有して、シンプルなマーキー効果を実現しています。具体的な内容...

MySQL で準備、実行、割り当て解除ステートメントを使用するチュートリアル

序文MySQLでは、準備、実行、割り当て解除を正式にはPREPARE STATEMENTと呼びます。...

レスポンシブ Web デザインが価値のない 5 つの理由

この記事は Tom Ewer の Managewp ブログからのもので、現在人気のレスポンシブ デザ...

Springboot および Vue プロジェクトの Docker デプロイメントの実装手順

目次A. SpringbootプロジェクトのDockerデプロイメント1. Springbootプロ...

LambdaProbe を使用して Tomcat を監視する方法

導入: Lambda Probe (旧称 Tomcat Probe) は、Apache Tomcat...

WINDOWS での MYSQL のインストールに関する詳細なチュートリアル

1. インストールパッケージをダウンロードする- お使いのコンピュータシステムに応じて適切なバージョ...

MySQL デッドロック シナリオ例の分析

序文最近、MySQL で RR レベルでデッドロック問題に遭遇しました。興味深いと思ったので、調べて...