目次
応答性
总结
ホームページ ウェブフロントエンド Vue.js Vue3 応答システムを手書きする方法

Vue3 応答システムを手書きする方法

May 14, 2023 am 09:40 AM
vue3

応答性

まず第一に、応答性とは何でしょうか?

応答性とは、観測データが変化したときに一連の連携処理を行うことを意味します。 話題のソーシャル イベントと同じように、最新情報があれば、すべてのメディアがフォローアップし、関連するレポートを作成します。ここでは社会的なホットイベントが観察対象となります。では、フロントエンドフレームワークでは、観察されるターゲットは何でしょうか?明らかに、それは州です。通常、複数の状態があり、それらはオブジェクトによって編成されます。したがって、状態オブジェクトの各キーの変化を観察し、一連の処理を連動して実行できます。

次のようなデータ構造を維持する必要があります:

Vue3 応答システムを手書きする方法

状態オブジェクトの各キーには、関連する一連の効果副作用があります。関数 、つまり、変更が Set を通じて整理されるときのリンケージ実行のロジック。

各キーは一連のエフェクト機能に関連付けられており、複数のキーをマップ内に保持できます。

このマップはオブジェクトが存在するときに存在します。オブジェクトが破棄されると、オブジェクトも破棄されます。 (オブジェクトがなくなるため、各キーに関連付けられた効果を維持する必要がなくなります)

WeakMap にはまさにそのような機能があり、WeakMap のキーはオブジェクトである必要があり、値は任意のデータにすることができます. キー オブジェクトが破壊されると、値も破壊されます。

したがって、レスポンシブ マップは WeakMap を使用して保存され、キーは元のオブジェクトになります。

このデータ構造は、応答性の中核となるデータ構造です。

#たとえば、このようなステータス オブジェクト:

const obj = {
    a: 1,
    b: 2
}
ログイン後にコピー

その応答データ構造は次のようになります:

const depsMap = new Map();
const aDeps = new Set();
depsMap.set('a', aDeps);
const bDeps = new Set();
depsMap.set('b', bDeps);
const reactiveMap = new WeakMap()
reactiveMap.set(obj, depsMap);
ログイン後にコピー

作成されたデータ構造は図にあるものです:

Vue3 応答システムを手書きする方法

Vue3 応答システムを手書きする方法##次に、deps 依存関係を追加します。たとえば、関数は次のように依存します。 a の場合、それを a の deps コレクションに追加する必要があります:

effect(() => {
    console.log(obj.a);
});
ログイン後にコピー

つまり:

const depsMap = reactiveMap.get(obj);
const aDeps = depsMap.get('a');
aDeps.add(该函数);
ログイン後にコピー
この方法で deps 関数を維持することに問題はありません。しかし、私たちはユーザーに次のことを望んでいますか?deppsを手動で追加する必要がありますか?これはビジネス規範に抵触するだけでなく、見落とされやすいものでもあります。

したがって、ユーザーにdepsを手動で保守させることは絶対に許可せず、依存関係の収集は自動で行います。では、依存関係を自動的に収集するにはどうすればよいでしょうか?ステータスの値を読み取ると、ステータスとの依存関係が確立されるため、ステータスを代理するために使用できる get メソッドを考えるのは簡単です。 Object.defineProperty または Proxy を通じて:

const data = {
    a: 1,
    b: 2
}
let activeEffect
function effect(fn) {
  activeEffect = fn
  fn()
}
const reactiveMap = new WeakMap()
const obj = new Proxy(data, {
    get(targetObj, key) {
        let depsMap = reactiveMap.get(targetObj);
        
        if (!depsMap) {
          reactiveMap.set(targetObj, (depsMap = new Map()))
        }
        
        let deps = depsMap.get(key)
        
        if (!deps) {
          depsMap.set(key, (deps = new Set()))
        }
        deps.add(activeEffect)

        return targetObj[key]
   }
})
ログイン後にコピー

effect は受信コールバック関数 fn を実行します。fn で obj.a を読み取ると、get がトリガーされ、オブジェクトの応答が取得されます。数式マップ、そこから a に対応する deps セットを取得し、それに現在のエフェクト関数を追加します。

これで依存関係の収集が完了しました。

obj.a を変更するときは、すべての dep に通知する必要があるため、プロキシ セットも必要です。

set(targetObj, key, newVal) {
    targetObj[key] = newVal
    const depsMap = reactiveMap.get(targetObj)
    if (!depsMap) return
    const effects = depsMap.get(key)
    effects && effects.forEach(fn => fn())
}
ログイン後にコピー

基本的な応答性は完了です。テストしてみましょう:

Vue3 応答システムを手書きする方法 は 2 回出力されます。1 回目は 1、2 回目は 3 です。この効果は、最初に受信コールバック関数を実行し、依存関係を収集するために get をトリガーします。この時点で、出力される obj.a は 1 です。次に、obj.a に値 3 が割り当てられると、収集された依存関係を実行するために set がトリガーされます。このとき、obj. a は 3

依存関係も正しく収集されています:

Vue3 応答システムを手書きする方法結果は正しいです。基本的な対応は完了しました!もちろん、応答性はこの小さなコードだけで実現されるわけではなく、現在の実装は完璧ではなく、いくつかの問題がまだあります。たとえば、

コードに分岐スイッチがある場合、最後の実行は obj.b に依存しますが、次の実行はそれに依存しません。現時点で無効な依存関係はありますか?

このようなコード:

const obj = {
    a: 1,
    b: 2
}
effect(() => {
    console.log(obj.a ? obj.b : 'nothing');
});
obj.a = undefined;
obj.b = 3;
ログイン後にコピー
エフェクト関数が初めて実行されるとき、obj.a は 1 です。この時点では、最初のブランチに移動し、その後 obj.b に依存します。 obj.a を未定義に変更し、セットをトリガーし、すべての依存関数を実行します。この時点で、ブランチ 2 に進み、obj.b には依存しなくなります。

obj.b を 3 に変更します。現時点では b に依存する関数がないのは当然です。試してみましょう:

Vue3 応答システムを手書きする方法初めて 2 を出力する、つまり最初の分岐に到達したことを出力し、obj.b

## を出力するのが正しいです。また、2 回目に何も出力しないのも正しいです。現時点では 2 番目のブランチに到達しています。しかし、3 度目に何も出力しないのは間違いです。この時点では、obj.b には依存関数はもうありませんが、それでも出力されるからです。

印刷してdepsを見ると、obj.bのdepsがクリアされていないことがわかります

所以解决方案就是每次添加依赖前清空下上次的 deps。怎么清空某个函数关联的所有 deps 呢?记录下就好了。

我们改造下现有的 effect 函数:

let activeEffect
function effect(fn) {
  activeEffect = fn
  fn()
}
ログイン後にコピー

记录下这个 effect 函数被放到了哪些 deps 集合里。也就是:

let activeEffect
function effect(fn) {
  const effectFn = () => {
      activeEffect = effectFn
      fn()
  }
  effectFn.deps = []
  effectFn()
}
ログイン後にコピー

对之前的 fn 包一层,在函数上添加个 deps 数组来记录被添加到哪些依赖集合里。

get 收集依赖的时候,也记录一份到这里:

Vue3 応答システムを手書きする方法

这样下次再执行这个 effect 函数的时候,就可以把这个 effect 函数从上次添加到的依赖集合里删掉:

Vue3 応答システムを手書きする方法

cleanup 实现如下:

function cleanup(effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
        const deps = effectFn.deps[i]
        deps.delete(effectFn)
    }
    effectFn.deps.length = 0
}
ログイン後にコピー

effectFn.deps 数组记录了被添加到的 deps 集合,从中删掉自己。全删完之后就把上次记录的 deps 数组置空。

我们再来测试下:

Vue3 応答システムを手書きする方法

无限循环打印了,什么鬼?

问题出现在这里:

Vue3 応答システムを手書きする方法

set 的时候会执行所有的当前 key 的 deps 集合里的 effect 函数。

而我们执行 effect 函数之前会把它从之前的 deps 集合中清掉:

Vue3 応答システムを手書きする方法

执行的时候又被添加到了 deps 集合。这样 delete 又 add,delete 又 add,所以就无限循环了。

解决的方式就是创建第二个 Set,只用于遍历:

Vue3 応答システムを手書きする方法

这样就不会无限循环了。

再测试一次:

Vue3 応答システムを手書きする方法

现在当 obj.a 赋值为 undefined 之后,再次执行 effect 函数,obj.b 的 deps 集合就被清空了,所以需改 obj.b 也不会打印啥。

看下现在的响应式数据结构:

Vue3 応答システムを手書きする方法

确实,b 的 deps 集合被清空了。那现在的响应式实现是完善的了么?也不是,还有一个问题:

如果 effect 嵌套了,那依赖还能正确的收集么?

首先讲下为什么要支持 effect 嵌套,因为组件是可以嵌套的,而且组件里会写 effect,那也就是 effect 嵌套了,所以必须支持嵌套。

我们嵌套下试试:

effect(() => {
    console.log(&#39;effect1&#39;);
    effect(() => {
        console.log(&#39;effect2&#39;);
        obj.b;
    });
    obj.a;
});
obj.a = 3;
ログイン後にコピー

按理说会打印一次 effect1、一次 effect2,这是最开始的那次执行。然后 obj.a 修改为 3 后,会触发一次 effect1 的打印,执行内层 effect,又触发一次 effect2 的打印。也就是会打印 effect1、effect2、effect1、effect2。

我们测试下:

Vue3 応答システムを手書きする方法

打印了 effect1、effet2 这是对的,但第三次打印的是 effect2,这说明 obj.a 修改后并没有执行外层函数,而是执行的内层函数。为什么呢?

看下这段代码:

Vue3 応答システムを手書きする方法

我们执行 effect 的时候,会把它赋值给一个全局变量 activeEffect,然后后面收集依赖就用的这个。

当嵌套 effect 的时候,内层函数执行后会修改 activeEffect 这样收集到的依赖就不对了。

怎么办呢?嵌套的话加一个栈来记录 effect 不就行了?

也就是这样:

Vue3 応答システムを手書きする方法

执行 effect 函数前把当前 effectFn 入栈,执行完以后出栈,修改 activeEffect 为栈顶的 effectFn。

这样就保证了收集到的依赖是正确的。

这种思想的应用还是很多的,需要保存和恢复上下文的时候,都是这样加一个栈。

我们再测试一下:

Vue3 応答システムを手書きする方法

现在的打印就对了。至此,我们的响应式系统就算比较完善了。

全部代码如下:

const data = {
    a: 1,
    b: 2
}
let activeEffect
const effectStack = [];
function effect(fn) {
  const effectFn = () => {
      cleanup(effectFn)
      activeEffect = effectFn
      effectStack.push(effectFn);
      fn()
      effectStack.pop();
      activeEffect = effectStack[effectStack.length - 1];
  }
  effectFn.deps = []
  effectFn()
}
function cleanup(effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
        const deps = effectFn.deps[i]
        deps.delete(effectFn)
    }
    effectFn.deps.length = 0
}
const reactiveMap = new WeakMap()
const obj = new Proxy(data, {
    get(targetObj, key) {
        let depsMap = reactiveMap.get(targetObj)
        
        if (!depsMap) {
          reactiveMap.set(targetObj, (depsMap = new Map()))
        }
        let deps = depsMap.get(key)
        if (!deps) {
          depsMap.set(key, (deps = new Set()))
        }
        deps.add(activeEffect)
        activeEffect.deps.push(deps);
        return targetObj[key]
   },
   set(targetObj, key, newVal) {
        targetObj[key] = newVal
        const depsMap = reactiveMap.get(targetObj)
        if (!depsMap) return
        const effects = depsMap.get(key)
        // effects && effects.forEach(fn => fn())
        const effectsToRun = new Set(effects);
        effectsToRun.forEach(effectFn => effectFn());
    }
})
ログイン後にコピー

总结

响应式就是数据变化的时候做一系列联动的处理。

核心是这样一个数据结构:

Vue3 応答システムを手書きする方法

最外层是 WeakMap,key 为对象,value 为响应式的 Map。这样当对象销毁时,Map 也会销毁。Map 里保存了每个 key 的依赖集合,用 Set 组织。

我们通过 Proxy 来完成自动的依赖收集,也就是添加 effect 到对应 key 的 deps 的集合里。 set 的时候触发所有的 effect 函数执行。

这就是基本的响应式系统。

但是还不够完善,每次执行 effect 前要从上次添加到的 deps 集合中删掉它,然后重新收集依赖。这样可以避免因为分支切换产生的无效依赖。并且执行 deps 中的 effect 前要创建一个新的 Set 来执行,避免 add、delete 循环起来。此外,为了支持嵌套 effect,需要在执行 effect 之前把它推到栈里,然后执行完出栈。解决了这几个问题之后,就是一个完善的 Vue 响应式系统了。当然,现在虽然功能是完善的,但是没有实现 computed、watch 等功能,之后再实现。

最后,再来看一下这个数据结构,理解了它就理解了 vue 响应式的核心:

Vue3 応答システムを手書きする方法

以上がVue3 応答システムを手書きする方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

AI Hentai Generator

AI Hentai Generator

AIヘンタイを無料で生成します。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

vue3+vite: src に画像を動的にインポートするために require を使用するときのエラーを解決する方法 vue3+vite: src に画像を動的にインポートするために require を使用するときのエラーを解決する方法 May 21, 2023 pm 03:16 PM

vue3+vite:src は、イメージとエラー レポートと解決策を動的にインポートするために require を使用します。vue3+vite は複数のイメージを動的にインポートします。vue3。TypeScript 開発を使用している場合、イメージを導入するために require のエラー メッセージが表示されます。requireisnotdefined は使用できません。 vue2 のような imgUrl:require(' .../assets/test.png') は、typescript が require をサポートしていないため、インポートされます。そのため、import が使用されます。解決方法は次のとおりです: awaitimport を使用します

vue3 プロジェクトで tinymce を使用する方法 vue3 プロジェクトで tinymce を使用する方法 May 19, 2023 pm 08:40 PM

tinymce はフル機能のリッチ テキスト エディター プラグインですが、tinymce を vue に導入するのは他の Vue リッチ テキスト プラグインほどスムーズではありません。tinymce 自体は Vue には適しておらず、@tinymce/tinymce-vue を導入する必要があります。外国のリッチテキストプラグインであり、中国語版を通過していないため、公式 Web サイトから翻訳パッケージをダウンロードする必要があります (ファイアウォールをバイパスする必要がある場合があります)。 1. 関連する依存関係をインストールします npminstalltinymce-Snpminstall@tinymce/tinymce-vue-S2. 中国語パッケージをダウンロードします 3. スキンと中国語パッケージを導入します. プロジェクトのパブリック フォルダーに新しい tinymce フォルダーを作成し、

Vue3 がマークダウンを解析し、コードのハイライトを実装する方法 Vue3 がマークダウンを解析し、コードのハイライトを実装する方法 May 20, 2023 pm 04:16 PM

Vue はブログ フロントエンドを実装しており、マークダウン解析を実装する必要があり、コードがある場合はコードのハイライトを実装する必要があります。 Vue には、markdown-it、vue-markdown-loader、marked、vue-markdown など、マークダウン解析ライブラリが多数あります。これらのライブラリはすべて非常に似ています。ここではMarkedが使用され、コード強調表示ライブラリとしてhighlight.jsが使用されます。 1. 依存ライブラリをインストールする vue プロジェクトの下でコマンド ウィンドウを開き、次のコマンド npminstallmarked-save//marked を入力して、マークダウンを htmlnpmins に変換します。

Vue3 でページの部分的なコンテンツを更新する方法 Vue3 でページの部分的なコンテンツを更新する方法 May 26, 2023 pm 05:31 PM

ページの部分的な更新を実現するには、ローカル コンポーネント (dom) の再レンダリングを実装するだけで済みます。 Vue でこの効果を実現する最も簡単な方法は、v-if ディレクティブを使用することです。 Vue2 では、v-if 命令を使用してローカル dom を再レンダリングすることに加えて、新しい空のコンポーネントを作成することもできます。ローカル ページを更新する必要がある場合は、この空のコンポーネント ページにジャンプしてから、再びジャンプします。 beforeRouteEnter ガードを空白のコンポーネントに配置します。元のページ。以下の図に示すように、Vue3.X の更新ボタンをクリックして赤枠内の DOM を再読み込みし、対応する読み込みステータスを表示する方法を示します。 Vue3.X の scriptsetup 構文のコンポーネントのガードには o しかないので、

vue3 プロジェクトをパッケージ化してサーバーに公開した後、アクセス ページが空白で表示される問題の解決方法 vue3 プロジェクトをパッケージ化してサーバーに公開した後、アクセス ページが空白で表示される問題の解決方法 May 17, 2023 am 08:19 AM

vue3 プロジェクトがパッケージ化され、サーバーに公開されると、アクセス ページに空白の 1 が表示されます。vue.config.js ファイル内の publicPath は次のように処理されます: const{defineConfig}=require('@vue/cli-service') module.exports=defineConfig({publicPath :process.env.NODE_ENV==='production'?'./':'/&

Vue3 でアバターを選択してトリミングする方法 Vue3 でアバターを選択してトリミングする方法 May 29, 2023 am 10:22 AM

最終的な効果は、VueCropper コンポーネントのyarnaddvue-cropper@next をインストールすることです。上記のインストール値は Vue3 用です。Vue2 の場合、または他の方法を参照したい場合は、公式 npm アドレス: 公式チュートリアルにアクセスしてください。また、コンポーネント内で参照して使用するのも非常に簡単です。必要なのは、対応するコンポーネントとそのスタイル ファイルを導入することだけです。ここではグローバルに参照しませんが、import{userInfoByRequest}from'../js/api を導入するだけです。 ' コンポーネント ファイルにインポートします。import{VueCropper}from'vue-cropper&

Vue3 の再利用可能なコンポーネントの使用方法 Vue3 の再利用可能なコンポーネントの使用方法 May 20, 2023 pm 07:25 PM

はじめに vue であれ、react であれ、複数の繰り返しコードに遭遇した場合、ファイルを冗長なコードの束で埋めるのではなく、これらのコードを再利用する方法を考えます。実際、vue と React はどちらもコンポーネントを抽出することで再利用を実現できますが、小さなコードの断片に遭遇し、別のファイルを抽出したくない場合は、それに比べて、React は同じファイル内で対応するウィジェットを宣言して使用できます。または、次のような renderfunction を通じて実装します。 constDemo:FC=({msg})=>{returndemomsgis{msg}}constApp:FC=()=>{return(

DefineCustomElement を使用して Vue3 でコンポーネントを定義する方法 DefineCustomElement を使用して Vue3 でコンポーネントを定義する方法 May 28, 2023 am 11:29 AM

Vue を使用してカスタム要素を構築する WebComponents は、開発者が再利用可能なカスタム要素 (カスタム要素) を作成できるようにする一連の Web ネイティブ API の総称です。カスタム要素の主な利点は、フレームワークがなくても、任意のフレームワークで使用できることです。これらは、異なるフロントエンド テクノロジ スタックを使用している可能性のあるエンド ユーザーをターゲットにする場合、または最終アプリケーションを使用するコンポーネントの実装の詳細から切り離したい場合に最適です。 Vue と WebComponents は補完的なテクノロジであり、Vue はカスタム要素の使用と作成に対する優れたサポートを提供します。カスタム要素を既存の Vue アプリケーションに統合したり、Vue を使用してビルドしたりできます。

See all articles