A few days ago, THE MVVM simply implemented bidirectional data binding and parsed-vUe-related instructions in VUE. In fact, after writing it, I felt that the important publish and subscribe mode was not written clearly (on the one hand, MY level is limited). In order to make it more convenient for myself to refer to it in the future, so I will write a small MVVM today. Add computed and watch as well

Dep

Dep is a constructor used to collect all data objects that depend on a certain item of data. All objects that access data through the GET method are dependent on this data, so we collect it and notify each object that depends on this item of data to update every time the data changes. So we have to proxy the data in data first,

functionObserve (data) {object.keys (data). ForEach (key => {// Check whether it is an Object. If yes, perform another ~if (typeof data[key] === 'object') {
            observe(data[key])
        }
        let v = data[key]
        Object.defineProperty(data, key, {
            get() {
                return v
            },
            set(newVal) {// Determine whether the new value is equal to the old value to avoid repeated operationsif(newVal ! == v) {v = newVal // Proceed with data hijacking observe(v)}}})})}Copy the code

  1. Dep collects dependencies at get(), so we need to inject dependencies every time we listen for data, and we need to implement Dep as well
  2. So how do you notify Watcher of updates? If we want to update the data, we will naturally go to the set() method, so we will write the update place in the set

    functionObserve (data) {object.keys (data). ForEach (key => {// Check whether it is an Object. If yes, perform another ~if (typeof data[key] === 'object') {
                observe(data[key])
            }
            letDep = new Dep() object.defineProperty (data, key, {Dep = new Dep() object.defineProperty (data, key, {get() {
                    dep.add()
                    return v
                },
                set(newVal) {// Determine whether the new value is equal to the old value to avoid repeated operationsif(newVal ! Observe (v) // Remind update dep.notify()}}})})} class dep {constructor() {
            this.depArr = []
        }
        add() {// Add watcher to depArr this.deparr.push ()}notifyThis.deparr.foreach (watcher => {watcher.update()})}}Copy the code

Watcher

Watcher’s function is to be “alerted” by the Dep to execute the callback function passed to it when the data in the data changes.

functionObserve (data) {object.keys (data). ForEach (key => {// Check whether it is an Object. If yes, perform another ~if (typeof data[key] === 'object') {
            observe(data[key])
        }
        letDep = new Dep() object.defineProperty (data, key, {Dep = new Dep() object.defineProperty (data, key, {get() {
                dep.add()
                return v
            },
            set(newVal) {// Determine whether the new value is equal to the old value to avoid repeated operationsif(newVal ! == v) {v = newVal // Then continue data hijacking observe(v) // Remind update dep.notify()}}})}return data
}

class Dep {
    constructor() {
        this.depArr = []
    }
    add() {// Put the Watcher into the depArrif (Dep.target) {
            this.depArr.push(Dep.target)
        }
    }
    notifyThis.deparr.foreach (watcher => {watcher.update()})}} class watcher { Constructor (cb) {this.cb = cb this.get()} // Bind the current Watcher instance to the Depget() {dep.target = this // Get the current data value and save this.value = this.cb() dep.target = nullreturn this.value
    }
    update() {
        this.get()
    }
}Copy the code

To tease out how it works, take an example:

var data = observe({
    msg: 'Who is the front dropper?',
})

new Watcher(() => {
    document.getElementById('app').innerHTML = `msg is ${data.msg}`})Copy the code

  1. When we run the program, even without the new Watcher, it first observes observe and then passes in a data, and when we hijack data, we pass

    let dep = new Dep() Copy the code

    Create a DEP instance, but there is no Watcher instance, so there is no subscription binding (after all, we are human, if you don’t new Watcher, we don’t bind subscriptions to you)

  2. And then the function goes down, you go to new Watcher, you pass in the function as an argument,

    new Watcher(() => {
        document.getElementById('app').innerHTML = `msg is ${data.msg}`})Copy the code

    So this is where the subscription is actually injected, the callback function is saved, and the internal method is executed:

    this.cb = cb
    this.get()Copy the code

    The point is this get()

    get() {dep.target = this // Get the current data value and save this.value = this.cb() dep.target = nullreturnThis. value // If you do not return a value, remove this.value.Copy the code

    We’re going to go back, and when this. Cb is passed in, it’s going to access the data in data, and we’ve already assigned the Watcher instance to dep. target, so observe

    get() {
        dep.add()
        return v
    },Copy the code

    Now that it’s ready to execute, add the Watcher instance to the depArr, and subsequent changes to data.msg will invoke the Observe set method if the new value is not equal to the old value:

    dep.notify()
    notifyThis.deparr.foreach (watcher => {watcher.update()})} {// This.deparr.foreach (watcher => {watcher.update()})}Copy the code

Fixed a small BUG:

  1. Object. DefineProperty called on none-object if you do not observe whether the incoming data is object, it will report object. defineProperty called on none-object
  2. In the Watcher class, if yes

    update() {
        this.get()
    }Copy the code

    There will be many more watcher each time we change the value, for example, we add a syntax to the JS file

    data.msg = '111'Copy the code

    Then modify it again on the console, as shown below.

    So let’s change this code to execute the callback directly, and we can change it at this point

    update() {this.cb()},Copy the code

     

computed

We can think of computed as an enhanced version of Watcher because it collects the data-dependent Watcher into its DEP instance, so we write the functionality of computed in Watcher,

Class Watcher {constructor(cb, options = {}) {this.cb = cb this. puted = options.computed This.dep = new dep () : this.get()} // bind the current Watcher instance to the dePget() {dep.target = this // Get the current data value and save this.value = this.cb() dep.target = nullreturn this.value
    }
    update() {// If computed exists, notify() is triggered because there is at least one watcher in computed DEPif (this.computed) {
            this.dep.notify()
        } else {
          this.cb()        }
    }
    depend() {
        this.dep.add()
    }
}

function computed(cb) {
    let computedWatcher = new Watcher(cb, { computed: true }),
        obj = {}
    Object.defineProperty(obj, 'value', {
        get() {// When accessing attributes in computed, call depend to add computedWatcher to the DEP queue. Depend () // corresponds to computed taking over Watcherreturn computedWatcher.get()
        }
    })
    return obj
}Copy the code

To make things a little more complicated, you can set computed to pass in an object, do data hijacking and determine whether the type is an object or a function, because computed also supports passing in objects. If it is an object, you can call the object’s GET method to get the value you want to use

watch

Watch also receives a method, and just like computed, you pass a switch to the Watcher to tell you that you’re using the Watcher,

class Watcher { constructor(cb, options = {}) { this.cb = cb this.computed = options.computed this.watch = options.watch this.callback = This.callback this.value = null // Determine whether computed is used, and if so, generate a deP instance this.puted? This.dep = new dep () : this.puted? This.dep = new dep () : This.get ()} // bind the current Watcher instance to the Depget() {dep.target = this // Get the current data value and save this.value = this.cb() dep.target = nullreturn this.value
    }
    update() {// If computed exists, notify() is triggered because there is at least one watcher in computed DEPif (this.computed) {
            this.dep.notify()
        } else if (this.watch) {
            let oldVal = this.value
            this.get()
            this.callback(oldVal, this.value)
        } else {
            this.cb()
        }
    }
    depend() {
        this.dep.add()
    }
}

function computed(cb) {
    let computedWatcher = new Watcher(cb, { computed: true }),
        obj = {}
    Object.defineProperty(obj, 'value', {
        get() {// When accessing attributes in computed, call depend to add computedWatcher to the DEP queue. Depend () // corresponds to computed taking over Watcherreturn computedWatcher.get()
        }
    })
    return obj
}

function watch(cb, callback) {
    new Watcher(cb, { watch: true, callback })
}Copy the code