背景
vue3はまだ正式にリリースされておらず、vue2の実装のアイデアの一部は参考として非常に価値があるため、この理論的な説明はまだvue2です。
応答性の高いシステムの実装
Vue.jsはMVVMフレームワークです。データモデルは通常のJavaScriptオブジェクトですが、これらのオブジェクトが操作されると、対応するビューに影響を与える可能性があります。そのコア実装は「レスポンシブシステム」です。
vue 2.0では、Object.defineProperty
「レスポンシブシステム」の実装に基づいてい ます。Vue3はProxy/Reflect
実現に基づいており 、vue3の詳細な分析には書く時間があり、この記事はvue2の実現についてです。
主に属性が含まれます:
-
列挙可能、プロパティを列挙できるかどうか、デフォルトはfalse。
-
構成可能。プロパティーを変更または削除できるかどうかにかかわらず、デフォルトはfalseです。
-
get、属性を取得するメソッド。
-
set、プロパティの設定方法。
応答性の基本原則は、Vueコンストラクターでオプションのデータを処理することです。つまり、vueインスタンスが初期化されると、データ、プロパティ、その他のオブジェクトの各プロパティは、Object.definePropertyによって一度定義されます。データが設定されたら、いくつかの操作を実行して、対応するビューを変更します。
class Vue {
/* Vue构造类 */
constructor(options) {
this._data = options.data;
observer(this._data);
}
}
function observer (value) {
if (!value || (typeof value !== 'object')) {
return;
}
Object.keys(value).forEach((key) => {
defineReactive(value, key, value[key]);
});
}
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true, /* 属性可枚举 */
configurable: true, /* 属性可被修改或删除 */
get: function reactiveGetter () {
return val;
},
set: function reactiveSetter (newVal) {
if (newVal === val) return;
cb(newVal);
}
});
}
実際のアプリケーションでは、さまざまなシステムが非常に複雑です。これでグローバルオブジェクトがあると仮定します。これを複数のVueオブジェクトで使用して表示できます。あるいは、dataに書き込まれたデータがビューに適用されておらず、この時点でビューを更新する必要がない場合もあります。これは収集プロセスによって異なります。
依存コレクション
いわゆる依存収集とは、データが使用される場所を収集することであり、このデータが変更されると、各場所に対応する操作を行うよう通知します。VUEの「サブスクライバー」の基本モードは次のとおりです。
exportdefaultclass Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
//依赖收集,有需要才添加订阅
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
サブスクライバーについて、Watcherの実装を見てみましょう。ソースコードウォッチャーはより論理的であり、簡略化されたモデルは次のとおりです。
class Watcher{
constructor(vm,expOrFn,cb,options){
//传进来的对象 例如Vue
this.vm = vm
//在Vue中cb是更新视图的核心,调用diff并更新视图的过程
this.cb = cb
//收集Deps,用于移除监听
this.newDeps = []
this.getter = expOrFn
//设置Dep.target的值,依赖收集时的watcher对象
this.value =this.get()
}
get(){
//设置Dep.target值,用以依赖收集
pushTarget(this)
const vm = this.vm
let value = this.getter.call(vm, vm)
return value
}
//添加依赖
addDep (dep) {
// 这里简单处理,在Vue中做了重复筛选,即依赖只收集一次,不重复收集依赖
this.newDeps.push(dep)
dep.addSub(this)
}
//更新
update () {
this.run()
}
//更新视图
run(){
//这里只做简单的console.log 处理,在Vue中会调用diff过程从而更新视图
console.log(`这里会去执行Vue的diff相关方法,进而更新数据`)
}
}
defineReactive詳細ロジック
exportfunction defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
所以响应式原理就是,我们通过递归遍历,把vue实例中data里面定义的数据,用defineReactive(Object.defineProperty)重新定义。每个数据内新建一个Dep实例,闭包中包含了这个 Dep 类的实例,用来收集 Watcher 对象。在对象被「读」的时候,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。之后如果当该对象被「写」的时候,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图。
ウォッチャーの誕生
Vueでは、Watcherを生成する4つの状況があります。
-
Vueインスタンスオブジェクトのウォッチャー、ルートデータを観察し、変更が発生したときにコンポーネントを再レンダリングしますupdateComponent =()=> {vm._update(vm._render()、hydrating)} vm._watcher = new Watcher(vm、updateComponent、noop)
-
ユーザーがvueオブジェクトのwatch属性を使用して作成したウォッチャー
-
ユーザーがVueオブジェクトに作成した計算属性は、基本的にウォッチャーです
-
ユーザーがvmを使用して作成したWatcher $ Watch
ワッサーは増減し、レンダリング時に追加される場合があります。したがって、ウォッチャーをスケジュールするスケジュールが必要です。主なコードは次のとおりです。
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
スケジュールの役割:
-
重複排除、各Watcherには一意のIDがあります。まず、IDがすでにキューにある場合は、スキップして実行を繰り返す必要はありません。IDがキューにない場合は、キューが実行されているかどうかによって異なります。実行されていない場合、キューは次のタイムスライスで実行されるため、キューは常に非同期で実行されます。
-
並べ替えは、解析とレンダリングの順序で実行されます。つまり、最初にWatcherが実行されます。WatcherのIDは自動的に増加し、最初に作成されたIDは後で作成されたIDよりも小さくなります。したがって、次のルールがあります。
2.1。コンポーネントはネストすることができ、分析では最初に親コンポーネントを解析し、次に子コンポーネントを解析する必要があります。したがって、親コンポーネントのidは子コンポーネントよりも小さいです。
2.2。ユーザーが作成したウォッチャーは、レンダリング時に作成されるウォッチャーの前に解析されます。したがって、ユーザーが作成したWatcherのIDは、レンダリング時に作成されたIDよりも小さくなります。
-
ウォッチャーの削除。コンポーネントのウォッチャーがキューにあり、その親コンポーネントが削除されている場合、このウォッチャーもこの時点で削除する必要があります。
-
キューの実行中に、各ウォッチャーの実行回数を含むオブジェクトサーキュラーが保存されます。いずれかのウォッチャーがMAX_UPDATE_COUNTで定義された回数を超えて実行される場合、無限ループと見なされ、実行されなくなります。デフォルトは100回です。
总之,调用的作用就是管理 Watcher。
補足
Object.definePropertyを使用してVUE2の配列オブジェクトを再定義する方法?データ内のアイテムを直接変更しても、ビューが応答して変化しないのはなぜですか(arr [3] = 4)。
答えは、アレイの応答性は完全ではなく、VUEは限られたメソッドのみを書き直したことです。書き換えロジックは次のとおりです。
const arrayProto = Array.prototype
exportconst arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case'push':
case'unshift':
inserted = args
break
case'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})