ホームページ > 記事 > ウェブフロントエンド > 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);
作成されたデータ構造は図にあるものです:
##次に、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())
}
は 2 回出力されます。1 回目は 1、2 回目は 3 です。この効果は、最初に受信コールバック関数を実行し、依存関係を収集するために get をトリガーします。この時点で、出力される obj.a は 1 です。次に、obj.a に値 3 が割り当てられると、収集された依存関係を実行するために set がトリガーされます。このとき、obj. a は 3
依存関係も正しく収集されています:
結果は正しいです。基本的な対応は完了しました!もちろん、応答性はこの小さなコードだけで実現されるわけではなく、現在の実装は完璧ではなく、いくつかの問題がまだあります。たとえば、
コードに分岐スイッチがある場合、最後の実行は 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 には依存しなくなります。
初めて 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 收集依赖的时候,也记录一份到这里:
这样下次再执行这个 effect 函数的时候,就可以把这个 effect 函数从上次添加到的依赖集合里删掉:
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 数组置空。
我们再来测试下:
无限循环打印了,什么鬼?
问题出现在这里:
set 的时候会执行所有的当前 key 的 deps 集合里的 effect 函数。
而我们执行 effect 函数之前会把它从之前的 deps 集合中清掉:
执行的时候又被添加到了 deps 集合。这样 delete 又 add,delete 又 add,所以就无限循环了。
解决的方式就是创建第二个 Set,只用于遍历:
这样就不会无限循环了。
再测试一次:
现在当 obj.a 赋值为 undefined 之后,再次执行 effect 函数,obj.b 的 deps 集合就被清空了,所以需改 obj.b 也不会打印啥。
看下现在的响应式数据结构:
确实,b 的 deps 集合被清空了。那现在的响应式实现是完善的了么?也不是,还有一个问题:
如果 effect 嵌套了,那依赖还能正确的收集么?
首先讲下为什么要支持 effect 嵌套,因为组件是可以嵌套的,而且组件里会写 effect,那也就是 effect 嵌套了,所以必须支持嵌套。
我们嵌套下试试:
effect(() => { console.log('effect1'); effect(() => { console.log('effect2'); obj.b; }); obj.a; }); obj.a = 3;
按理说会打印一次 effect1、一次 effect2,这是最开始的那次执行。然后 obj.a 修改为 3 后,会触发一次 effect1 的打印,执行内层 effect,又触发一次 effect2 的打印。也就是会打印 effect1、effect2、effect1、effect2。
我们测试下:
打印了 effect1、effet2 这是对的,但第三次打印的是 effect2,这说明 obj.a 修改后并没有执行外层函数,而是执行的内层函数。为什么呢?
看下这段代码:
我们执行 effect 的时候,会把它赋值给一个全局变量 activeEffect,然后后面收集依赖就用的这个。
当嵌套 effect 的时候,内层函数执行后会修改 activeEffect 这样收集到的依赖就不对了。
怎么办呢?嵌套的话加一个栈来记录 effect 不就行了?
也就是这样:
执行 effect 函数前把当前 effectFn 入栈,执行完以后出栈,修改 activeEffect 为栈顶的 effectFn。
这样就保证了收集到的依赖是正确的。
这种思想的应用还是很多的,需要保存和恢复上下文的时候,都是这样加一个栈。
我们再测试一下:
现在的打印就对了。至此,我们的响应式系统就算比较完善了。
全部代码如下:
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()); } })
响应式就是数据变化的时候做一系列联动的处理。
核心是这样一个数据结构:
最外层是 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 応答システムを手書きする方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。