vueソースコードのデータ応答式原理

10655 ワード

vueの概要
漸進的なフレームワーク:フレームを階層化することです.最も核心的なのは、ビューレイヤをレンダリングし、外側にコンポーネントメカニズムを追加し、その上でルーティングメカニズムを追加し、状態管理を追加し、最外層の構築ツールを追加することです.階層化とは、最もコアなビューレイヤレンダリングでいくつかのニーズを開発したり、vueファミリーバケツで大規模なアプリケーションを開発したりすることができます.異なるレベルを選択するには、より独自のニーズを持つことができます.
データ傍受(Object)Object.definePropertyES6を用いたProxy
    function defineReactive(data, key ,val) {
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function() {
                return val
            },
            set: function(newVal) {
                if(val === newVal) {
                    return;
                }
                val = newVal
            }
        })
    }

ここでの関数defineReactiveは、Object.definePropertyをカプセル化するために使用される.関数の名前から,応答式データを定義する役割を果たすことがわかる.すなわち,この関数で変化追跡を行い,カプセル化後はdata,keyvalを伝達するだけでよい.
パッケージ化後、datakeyからデータを読み出すたびに、get関数がトリガーされる.datakeyにデータを設定するたびに、set関数がトリガーされます.
依存を収集する方法Object.definePropertyをカプセル化するだけでは、実際には何の役にも立たず、本当に役に立つのは収集依存です.
考えてみれば、データを観察する目的は、データの属性が変化したときに、そのデータを使用した場所を通知することです.
    

このテンプレートにはデータnameが使用されているので、変更が発生した場合は、使用した場所に通知を送信します.
注意:Vue.js 2.0では、テンプレートがデータを使用するのはコンポーネントがデータを使用するのと同じであるため、データが変化すると、通知がコンポーネントに送信され、コンポーネント内部が仮想DOMで再レンダリングされる.
上記の問題では,まず依存を収集し,すなわちデータnameを用いた場所を収集し,その後属性が変化した場合に,以前に収集した依存ループを一度トリガすればよい.
まとめると、実は一言で、getterで依存を収集し、setterで依存をトリガする.
どこに集めて
考えてみると、まず考えられるのは、現在のkeyの依存を格納するための配列です.依存が関数であると仮定し、keyに保存し、window.target関数を少し改造することができます.
    function defineReactive(data, key, val) {
        let dep = [];
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function () {
                dep.push(window.target) //   
                return val
            },
            set(newVal) {
                if(val === newVal) {
                    return;
                }
                //   
                for (let i = 0; i < dep.length; i++) {
                    dep[i](newVal, val)
                }
                val = newVal
            }
        })
    }

ここでは,収集された依存性を格納するための配列defineReactiveを追加した.
そして、depがトリガされると、収集された依存をトリガするためにsetがループされる.
しかし、このように書くと、収集に依存するコードをdepクラスにカプセル化し、依存を管理するのに役立ちます.このクラスを使用すると、依存を収集したり、削除したり、依存に通知を送信したりすることができます.コードは次のとおりです.
    export default class Dep {
        constructor() {
            this.subs = []
        }
        addSub (sub) {
            this.subs.push(sub)
        }
        removeSub (sub) {
            remove(this.subs, sub)
        }
        depend () {
            if (window.target) {
                this.addSub(window.target)
            }
        }
        notify() {
            const subs = this.subs.slice();
            for(let i = 0, l = subs.length; i < l; i++) {
                subs[i].update()
            }
        }
    }
    
    function remove (arr, item) {
        if (arr.length) {
            const index = arr.indexOf(item)
            if (index > -1) {
                return arr.splice(index, 1)
            }
        }
    }

その後再改造下Dep:
    function defineReactive (data, key, val) {
        let dep = new Dep() //   
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function () {
                dep.depend() //   
                return val
            },
            set: function (newVal) {
                if(val === newVal){
                    return
                }
                val = newVal
                dep.notify() //   
            }
        })
    }

誰かに頼って
上のコードでは、私たちが収集した依存はdefineReactiveですが、それはいったい何ですか.私たちはいったい誰を集めますか?
誰を収集するか、言い換えれば、属性が変化した後、誰に通知するかです.
データを使用する場所を通知しますが、このデータを使用する場所は多く、タイプが異なり、テンプレートである可能性もあれば、ユーザーが書いたwindow.targetである可能性もあります.この場合、これらの状況を集中的に処理できるクラスを抽象化する必要があります.次に,収集に依存する段階でこのカプセル化されたクラスのインスタンスのみを収集し,通知も1つだけ通知する.次に、他の場所に通知する責任を負います.だから、抽象的なものはまず良い名前をつける必要があります.うん、それをwatchと呼んでください.
今すぐ上の質問に答えることができます.誰を集めますか?Watcher !
WatcherとはWatcherは、データが変化したときに通知され、他の場所に通知される仲介者の役割です.Watcherについては、まず古典的な使い方を見てみましょう.
    // keypath
    vm.$watch('a.b.c', function (newVal, oldVal) {
    //     
    })

このコードは、Watcher属性が変化すると、2番目のパラメータの関数がトリガーされることを示す.
どうやってこの機能を実現するのか考えてみましょう.このwatcherインスタンスをdataに追加するだけのようです.a.b.c属性のDepでいいです.そしてdata.a.b.cの値が変化した場合、Watcherに通知します.次に、Watcherはパラメータ内のこのコールバック関数を再実行する.
export default class Watcher {
    constructor (vm, expOrFn, cb) {
        this.vm = vm
        //   this.getter(),     data.a.b.c    
        this.getter = parsePath(expOrFn)
        this.cb = cb
        this.value = this.get()
    }
    get() {
        window.target = this
        let value = this.getter.call(this.vm, this.vm)
        window.target = undefined
        return value
    }
    update () {
        const oldValue = this.value
        this.value = this.get()
        this.cb.call(this.vm, this.value, oldValue)
    } 
}

このコードは自分からdata.a.b.cdata.a.b.cに追加することができますが、不思議ではありませんか?
私はDepメソッドでgetwindow.targetに設定したので、現在のthisインスタンスであり、watcherの値をもう一度読んでみると、必ずdata.a.b.cがトリガーされます.getterがトリガーされると、収集依存の論理がトリガーされます.収集依存については、前述したように、getterから依存が読み込まれ、window.targetに追加されます.
これにより、Depwindow.targetを割り当ててから値を読み、thisをトリガすれば、getterthiskeypathにアクティブに追加することができる.不思議な感じはありますか?
依存がDepに注入されると、Depの値が変化するたびに、依存リスト内のすべての依存サイクルがdata.a.b.cメソッド、すなわちupdateWatcherメソッドをトリガする.一方、updateメソッドは、updateおよびvalueをパラメータに転送するパラメータのコールバック関数を実行します.
したがって、実際には、ユーザが実行するoldValueであっても、テンプレートで使用されるvm.$watch('a.b.c', (value, oldValue) => {})であっても、dataによって変化が必要か否かが通知される.
ここでは、上のコードのparsePathが文字列のWatcherをどのように読み取ったのか、気になる人もいるかもしれません.次に、その実現原理をコードで説明します.
/**
*       
*/
const bailRE = /[^w.$]/
export function parsePath (path) {
    if (bailRE.test(path)) {
        return
    }
    const segments = path.split('.')
    return function (obj) {
        for (let i = 0; i < segments.length; i++) {
            if (!obj) return
                obj = obj[segments[i]]
            }
            return obj
        }
   }

これは複雑ではないことがわかります.まずkeypathを使います.配列に分割し、配列をループして1層ずつデータを読み、最後に手に入れたobjはkeypathで読みたいデータです.
すべてのkeyを再帰的に検出
現在、実際には変化検出機能を実現することができますが、前に紹介したコードはデータのいずれかの属性しか検出できません.データのすべての属性(サブ属性を含む)を検出したいので、1つのkeypathクラスをカプセル化します.このクラスの役割は、1つのデータ内のすべての属性(サブ属性を含む)をObserverの形式に変換し、それらの変化を追跡することです.
    /**
* Observer             object  。
*       ,Observer   object         getter/setter    
*         ,                 
*/
export class Observer {
    constructor (value) {
        this.value = value
        if (!Array.isArray(value)) {
            this.walk(value)
        }
    }
/**
* walk            getter/setter         
*             Object     
*/
    walk (obj) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i], obj[keys[i]])
        }
    }
}
function defineReactive (data, key, val) {
    //   ,     
    if (typeof val === 'object') {
        new Observer(val)
    }
    let dep = new Dep()
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            dep.depend()
            return val
        },
        set: function (newVal) {
            if(val === newVal){
                return
            }
            val = newVal
            dep.notify()
        }
    })
}

上記のコードでは、通常のgetter/setterを検出されたObserverに変換するためにobjectクラスを定義した.
次に、objectタイプのデータのみがObjectを呼び出し、各属性をwalkに変換して変化を検出する.
最後に、getter/setterdefineReactiveを追加してサブ属性を再帰し、new Observer(val)のすべての属性(サブ属性を含む)をdataの形式に変換して変化を検出することができる.getter/setterの属性が変化すると、その属性に対応する依存が通知を受信する.
すなわち、我々が1つのdataobjectに伝達すれば、このObserverは応答式のobjectになる.
Objectに関する質問
いくつかの文法はデータが変化してもvue.jsもモニタリングできません.例えば、Objectに属性を追加したり削除したりします.
Es 6 proxy方式データ応答を傍受する方式
    let obj = {
        a: 1,
        b: 2,
        c: 3
    }
    
    let reactive = new Proxy(obj, {
        get: function(target, key, receiver) {
            console.log(`getting ${key}`);
            return Reflect.get(target, key, receiver)
        }
        set: function(target, key, receiver) {
            console.log(`setting ${key}`);
            return Reflect.set(target, key, receiver)
        }
    })
    
    
    reactive.a      // getting a  // 1
    reactive.a = 4  // setting a
    reactive.a      // getting a  // 4

総括変化検出は検出データの変化であり、データが変化した場合、検出して通知を送信することができる.
ObjectはObjectを通過することができる.definePropertyは、属性をgetter/setterの形式に変換して変化を追跡します.データを読み込むとgetterがトリガーされ、データを変更するとsetterがトリガーされます.
getterでは携帯電話がどのような依存関係でデータを使用しているのか.setterがトリガーされると、getterで収集された依存データが変化したことを通知します.
依存ストレージを収集する場所は、依存を収集したり、依存を削除したり、依存メッセージを送信したりするためのDepを作成することです.
依存はwatcherであり,watcherがトリガしたgetterだけが依存を収集し,どのwatcherがgetterをトリガするか,どのwatcherをDepに収集する.データが変化すると、依存リストがループし、すべてのwatcherが通知されます.
watcherの原理は、まず自分をグローバルで一意の指定位置(例えばwindow.target)に設定し、データを読み出すことです.データを読み込んだので、このデータのgetterがトリガーされます.次にgetterでグローバル唯一のwindowから.targetは、現在データを読み出しているwatcherを読み出し、このwatcherをDepに収集する.
また、Object内のすべてのデータを応答式に変換する役割を果たすObserveクラスを作成します.
Data、Observe、Dep、Watcherの関係:DataはObserveからgetter/setterに変換することで変化を追跡する.外部がwatcherを介してデータを読み出すと、getterがトリガーされ、watcherが依存に追加されます.データが変化するとsetterがトリガーされ、Depの依存(watcher)に通知が送信されます.watcherは通知を受信すると外部に通知を送信し,外部に変化通知するとビュー更新をトリガーしたり,ユーザのコールバック関数をトリガーしたりする可能性がある.