vue3原則 - ミニvue3
Vue3のベータ版が公開されてしばらく経ちますが、Vue3は8月に公開されます。
以下のコードはVueの実装ロジックを紹介するもので、実用的なユースケースをすべてカバーするものではありません。
マウントドム
まず、dom 要素をマウントすることから始めましょう。
この関数は、jsタグのラベル、属性、子ノードで表現されたdom構造を引数にとります。 jsで構築されたvdomのテンプレートが次のようなものだとします。
const vdom = h('div', {
class: 'red'
}, [
h('span', null, 'hello')
])
そして、この構造をパースして、実際のdom構造にマウントします。
mount メソッドがすでに実装されていれば、 mount メソッドを呼び出すだけでマウントできます。
// マウントする仮想ドムと親ノードを渡す
mount(vdom, document.getElementById('app'))
mount メソッドがすべきことは
- vdomの解析
- 小道具の解析
- 解析子子ノード
// 要素をマウントする
function mount(vNode, container) {
const elm = document.createElement(vNode.tag)
// props
if (vNode.props) {
for (const key in vNode.props) {
const attr = vNode.props[key]
if (key.startsWith('on')) {
// イベントリスナー
const type = key.substr(2).toLocaleLowerCase()
elm.addEventListener(type, attr)
} else {
elm.setAttribute(key, attr)
}
}
}
// children
if (vNode.children) {
if (typeof vNode.children === 'string') {
elm.textContent = vNode.children
} else {
// 子要素を再帰的に解析する
vNode.children.forEach(child => {
mount(child, elm)
})
}
}
container.appendChild(elm)
}
これで、とてもシンプルなドムのマウント処理が完了しました。
ドムの更新
domがマウントされた後、domを更新するためにいくつかの操作が発生することがあります。例えば、ボタンをクリックして色を変更したり、番号を変更したりするような操作です。
このようなパッチ関数が実装されていれば、古い要素にパッチを当てることができます。
const vdom2 = h('div', {
class: 'red'
}, [
h('span', null, 'hello')
])
patch(vdom, vdom2)
ここでは、新旧のvdomを比較する必要があるので、マウント変換は、実際のdom構造を格納し、後で操作するのは簡単です。
const elm = vNode.elm = document.createElement(vNode.tag)
これは、vdom.elmを介して実際のドム構造にアクセスできるようになります。
次に、パッチが何をする必要があるかを考えてみましょう。
つのノードを比較して同じノードであるかどうかを確認し、もしそうであれば、属性と子の比較に進みます。
もしそうでなければ、ノードの置換が必要になりますが、これも扱いが複雑なのでここでは説明しません。
- 新旧プロップの比較
ここで重要なことは、新旧のノードのプロップが存在しない、両方が存在する、新しいものが存在する、古いものが存在する、といった多くの分岐状況が存在することに注意することです。その場合、それぞれのプロップは変更されたように見えるかもしれませんし、変更されていないように見えるかもしれません。
繰り返しになりますが、ここでは属性の場合のみを取り上げ、両方にpropsがある場合のみを説明します。vue内部でこの状況に対処するのは複雑なので、ここではパッチのアイデアだけを紹介します。
const oldProps = n1.props || {}
const newProps = n2.props || {}
// このプロパティが存在するか、または追加された場合
for (const key in newProps) {
const oldValue = oldProps[key]
const newValue = newProps[key]
// 新しい属性または2つの属性の値が等しくない場合は、ノードの内容を変更する必要がある。
if (oldValue !== newValue) {
elm.setAttribute(key, newValue)
}
}
// 属性を削除した
for (const key in oldProps) {
if (!(key in newProps)) {
// プロパティを削除した
elm.removeAttribute(key)
}
}
- 新旧の子を比較
同様に、子の比較も同じ問題にぶつかり、考慮すべき多くの分岐ケースがあります。
最も複雑なのは、両方の子要素が配列である場合で、vue では diff アルゴリズムへの負担を減らすためにキー値を表示する必要があります。 ここではキー値がないと仮定し、単純に 2 つの子要素の各ノードを比較します。
// 子供を比較する
const oldChildren = n1.children
const newChildren = n2.children
if (typeof newChildren === 'string') {
if (typeof oldChildren === 'string') {
// 両方のノードが文字ノードであり、異なる時間内に、コンテンツを変更する
} else {
// 新しいノードは文字である 古いノードは配列である 直接置換
}
} else if (Array.isArray(newChildren)) {
if (typeof oldChildren === 'string') {
// 古いノードは単なる文字である 新しいノードは配列である 新しいノードをマウントする
} else if (Array.isArray(oldChildren)) {
// 両方のノードが配列の場合、vueではキーパターンを使って同じ要素かどうかを判断する。
// キーがないと仮定すると、同じインデックスを持つ2つの配列を比較する
const commonLength = Math.min(newChildren.length, oldChildren.length)
for (let i = 0; i < commonLength; i++) {
// everychildのパブリック部分と比較する
}
// 次に、差の部分を比較する
if (newChildren.length > oldChildren.length) {
// より多くの新しい子ノード、マウント新しい子ノード
}
if (newChildren.length < oldChildren.length) {
// 少ない新しい子ノード 削除された子ノード
}
}
}
レスポンシブ
次のようなプログラムがあるとします。
let a = 10 let b = a * 10うまくいけば、aが変更されるとbも一緒に変更されます。
これは、bの変更がaの変更の副次的な効果であると言えます。 エクセルのスプレッドシートで、B列 = A列 * 10という数式を定義し、Aの値が変更されるとBも一緒に変更されることを想像してみてください。
実際、これは a が変更されたときに b = a * 10 を出力する onAchange 関数を持っていることと同じです。
let a = 10
let b = a * 10
では、このonAchangeはどのように実装するのでしょうか? reactのsetStateを思い浮かべてください。
onAchange(() => {
b = a * 10
})
/**
* () => {
b = a * 10
}
この関数は、変更が実行するものの副作用である。
/
setStateはフレームワークのユーザーに公開することができ、setStateの表示呼び出しはフレームワークに私のアクションの副作用をトリガーするように伝えます。
しかし、vueでは、値を更新するのはstate.a = newValueです。
vue3が提供する新しいAPIを使う簡単な例から始めましょう。
let _state, _update // を定義する_state 状態を保存する_update変更を実行する副作用を保存する
const onStateChange = update => {
_update = update // 副作用を保存する
}
const setState = newState => {
_state = newState
_update() // トリガーの副作用
}
これら2つのAPIはComposition APIの一部であり、完全に分離しており、options APIと共存できます。
この2つのAPIがどのように実装されているかを見てみましょう。
- 最初のステップは、watchEffectと依存関係のトラッキングを有効にすることです。
ここで何をしたいかを考えてみましょう
import {
reactive watchEffect
} from 'vue'
// 状態の値をラップするためにリアクティブを呼び出すと、状態に応答する方法で値を返す
// 依存関係のコレクションを含む
const state = reactive({
count: 0
})
// この関数が使用したすべてのもの、彼の実行中に使用されるすべての応答属性を追跡する
// 状態を変更する.countこの関数は
watchEffect(() => {
console.log(state.count)
}) // 0
state.count++ // 1
1. watchEffectを呼び出してエフェクトを渡した後、エフェクトは依存関係によって副作用として収集され、呼び出されるのを待っている必要がある。
2. このエフェクトが依存するパラメータが変更された場合、エフェクトを再度実行する必要がある。
- ステップ2 リアクティブな部分の実装
これまでのdepクラスの実装では、notifyをトリガーするためにdepが値を保存し、値の変更がトリガーされたときに副作用関数を呼び出すようになっていました。実際のvueでは、refは上記のnew Dep(value)と同様で、各refが値を保存します。
let activeEffect
// 依存関係
class Dep {
constructor(value) {
this.subscribers = new Set()
this._value = value
}
get value() { // ゲッターで依存関係の収集を自動化する
this.depend()
return this._value
}
set value(newVlue) { // セッターで副作用を自動化する
this._value = newVlue
this.notify()
}
// 依存関係を収集する
depend() {
if (activeEffect) this.subscribers.add(activeEffect)
}
// トリガーの依存関係
notify() {
this.subscribers.forEach(effect => effect())
}
}
function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}
const dep = new Dep('hello')
watchEffect(() => {
console.log(dep.value)
})
dep.value = 'world!'
実際のvueでは、refは上記のnew Dep(value)に似ていて、各refが値を保存します。一方、reactiveはオブジェクト全体のプロキシというより、オブジェクトの各プロパティがdepに対応し、値はdepではなくオブジェクトに対応します。
では、reactiveは具体的に何をするのでしょうか?
import {ref} from 'vue'
let count = ref(0)
count.value++ // countを使用するときに直接countを使用することができ、それを変更するときにのみcountを呼び出す必要がある。.value
1. オブジェクト全体のプロキシは、オブジェクトのプロパティにアクセスするときに全体のプロパティに依存性の追跡を追加する
2. 依存関係の追跡は、プロパティの値が変更されたときにトリガされ、副作用を引き起こす
さて、vue2では、これは 、によって行われ、彼はオブジェクトのプロキシの仕事を行い、かなりうまく実行します。しかし、どうしても欠点があります:
Object.defineProperty1. オブジェクトの各プロパティにバインドするために、各プロパティを繰り返し処理する必要がある。
2. オブジェクト自体にない、このオブジェクトのプロパティの変更を扱うことができない。
3. 配列をプロキシする場合、配列のプロトタイプをハックして元のメソッドを変更する必要がある。`array[index]` この方法で配列を変更しても、レスポンシブが発生しない理由は、次のとおりである。
vue3では、この機能の中核はプロキシであり、プロキシの特性は、詳細に入ることはありません、利害関係者は、APIをチェックすることができ、プロキシはまた、.NETの痛みのポイントに良い解決策です。
まず第一に、Dep依存クラスがまだ必要であり、その後、オブジェクトの属性にアクセスするとき、プロキシは、アクションをインターセプトし、依存トラックにすべての属性をバインドし、属性の依存トラックをバインドし、格納するものがある必要があり、ここではマップを選択し、各オブジェクトの各属性の依存トラックをバインドする必要性もあるので、オブジェクトの属性を見つける必要があります。どのオブジェクトがどの属性に対応するかは、このMapに依存します。
オブジェクト => オブジェクトのプロパティのマップ。
Object.defineProperty => オブジェクトのプロパティのマップ ( オブジェクトのプロパティ=> 依存関係に対応する属性 ) なぜプロキシはこのような問題を解決できるのでしょうか? おわかりのように、ループも再帰もなくなります。配列を特別に扱う必要もありません。
例えば、array.pushは、実際には最初にarray.lengthにアクセスし、length + 1の処理をトリガーします。
mini-vue3
これで、h関数、mount関数mount、dep依存クラス、ractive responsive、watchEffect副作用リスナーができました。
これらをまとめて、アプリをマウントする関数である $mount 関数を記述したシンプルな vue アプリケーションができました。
const targetMap = new WeakMap() // オブジェクト全体のすべてのオブジェクトと依存マッピングを収集する
// => オブジェクトのプロパティのマップ ( オブジェクトのプロパティ=> 依存関係に対応する属性 )
function getDep(target, key) {
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map() // オブジェクトの各プロパティを対応する依存関係にマッピングする
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Dep() //
depsMap.set(key, dep)
}
return dep
}
const reactiveHandler = {
get(target, key, receiver) {
const dep = getDep(target, key)
dep.depend() // 依存関係のコレクション
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const dep = getDep(target, key)
const result = Reflect.set(target, key, value, receiver)
dep.notify() // トリガーの副作用
return result
}
}
function reactive(obj) {
// プロキシオブジェクト
return new Proxy(obj, reactiveHandler)
}
画面上の div をクリックすると、魔法のように数字が加算されていることに気づくでしょう!つまり、ミニvue3アプリを実装したことになります!
Object.defineProperty実際、コンポジションAPIはロジックの再利用、コンポーネントの再利用を簡単に完結させることができます。
コンポジションAPIはリアクトのフックのように見えますが、実は同じではなく、リアクトのフックはイミュータブルなデータで、変数はクロージャの中に存在します!





