Try to implement a simple mvvm with Proxy

Posted by deadparrot on Fri, 19 Jun 2020 12:46:44 +0200

A brief overview of Proxy and Reflect

Proxy can be understood as setting up a layer of "interception" before the target object, through which the external access to the object must first be intercepted, so it provides a mechanism to filter and rewrite the external access. The original meaning of the word proxy is proxy. It is used here to indicate that it is used to "proxy" certain operations, which can be translated as "proxy".
From Mr. Ruan Yifeng's ECMAScript 6 introduction, click http://es6.ruanyifeng.com/#docs/proxy

For example:

var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    console.log(`setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
});

The above code sets up a layer of interception for an empty object, redefining the read (get) and set (set) behaviors of properties. For the time being, we will not explain the specific syntax, but only see the running results. For the object obj with intercepting behavior set, read and write its properties, and you will get the following results.

obj.count = 1
//  setting count!
++obj.count
//  getting count!
//  setting count!
//  2
var proxy = new Proxy(target, handler);

There are two parameters here. The target parameter represents the target object to be intercepted, and the handler parameter is also an object to customize the intercepting behavior.

Note that for proxy to work, you must operate on the proxy instance (the example above is a proxy object), not the target object (the example above is an empty object).

The Reflect object, like the Proxy object, is a new API provided by ES6 for manipulating objects.

The method of the Reflect object corresponds to the method of the Proxy object one by one. As long as it is the method of the Proxy object, the corresponding method can be found on the Reflect object. This allows the Proxy object to easily call the corresponding Reflect method to complete the default behavior as the basis for modifying the behavior. That is, no matter how the Proxy modifies the default behavior, you can always get the default behavior on Reflect.

Also put the link of teacher Ruan Yifeng http://es6.ruanyifeng.com/#docs/reflect

Initialization structure

See here, I think you have a better understanding of what Proxy is used for, and then let's take a look at the final scam.

See the picture above. First, let's create a new one index.html , and the code inside is like this. It's simple

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Simple Edition mvvm</title>
</head>
<body>
<div id="app">
    <h1>Development language:{{language}}</h1>
    <h2>component:</h2>
    <ul>
        <li>{{makeUp.one}}</li>
        <li>{{makeUp.two}}</li>
        <li>{{makeUp.three}}</li>
    </ul>
    <h2>Description:</h2>
    <p>{{describe}}</p>
    <p>Calculation properties:{{sum}}</p>
    <input placeholder="123" v-module="language" />
</div>
<script>
// It's the same as Vue
const mvvm = new Mvvm({
    el: '#app',
    data: {
        language: 'Javascript',
        makeUp: {
            one: 'ECMAScript',
            two: 'Document object model( DOM)',
            three: 'Browser object model( BOM)'
        },
        describe: 'No product can't be written',
        a: 1,
        b: 2
    },
    computed: {
        sum() {
        return this.a + this.b
    }
})
</script>
</body>
</html>

See the above code. It's about the same as vue. Next, implement the Mvvm constructor

Implement the Mvvm constructor

First, declare an Mvvm function. options are passed in as parameters. options is the configuration of the above code, including el, data and computed~~

function Mvvm(options = {}) {
    // Assign options to this.$options
    this.$options = options
    // hold options.data Assign to this_ data
    let data = this._data = this.$options.data
    let vm = initVm.call(this)
    return this._vm
}

The above Mvvm function is very simple, that is, assign the parameter options to this.$options, and options.data Assign to this_ Data, then call the initialization initVm function, and use call to change the direction of this to facilitate initVm operation. Then return to this_ VM, this is generated in the initvm function.

Continue to write the initVm function,

function initVm () {
    this._vm = new Proxy(this, {
        // Intercept get
        get: (target, key, receiver) => {
            return this[key] || this._data[key] || this._computed[key]
        },
        // Intercept set
        set: (target, key, value) => {
            return Reflect.set(this._data, key, value)
        }
    })
    return this._vm
}

This init function uses Proxy to intercept this object, and then assigns a value to this_ VM, and finally this_ vm,

As we said above, in order for Proxy to work, we must target the Proxy instance.

In the agent, get and set are blocked. In the get function, the value of the key corresponding to this object is returned. If you don't have this, go to this_ Take the corresponding key from the data object, and do not go to this again_ The corresponding key value is removed from the computed object. Set function is to directly return and modify this_ Data corresponds to key.

Do a good job in these interceptions. We can directly access our corresponding value from the strength. (mvvm makes our first code generation instance)

mvvm.b // 2
mvvm.a // 1
mvvm.language // "Javascript"

See the console as shown above. You can set values, you can get values, but this is not reactive.

Open the console and have a look

You can see it in detail. Only_ vm, this is proxy. What we need is_ All the data under data has an interception agent; let's implement it.

Implement all data agent interception

We first add an initObserve to the Mvvm, as follows

function Mvvm(options = {}) {
    this.$options = options
    let data = this._data = this.$options.data
    let vm = initVm.call(this)
+   initObserve.call(this, data) // Initialize the Observe of data
    return this._vm
}

The initObserve function is mainly to set this_ Data plus agents. as follows

function initObserve(data) {
    this._data = observe(data) // Assign all observe rs to this_ data
}

// This is separated mainly for the following recursive calls
function observe(data) {
    if (!data || typeof data !== 'object') return data // If it is not an object, return the value directly
    return new Observe(data) // Object call Observe
}

The following mainly implements the Observe class

// Observe class
class Observe {
    constructor(data) {
        this.dep = new Dep() // Subscription class, which will be introduced later
        for (let key in data) {
            data[key] = observe(data[key]) // Recursive call sub object
        }
        return this.proxy(data)
    }
    proxy(data) {
      let dep = this.dep
      return new Proxy(data, {
        get: (target, key, receiver) => {
          return Reflect.get(target, key, receiver)
        },
        set: (target, key, value) => {
          const result = Reflect.set(target, key, observe(value)) // For newly added objects, observe should also be added
          return result  
        }
      })
    }
  }

In this way, by adding proxy layer by layer, we can_ Add all data objects, and then look at the console

Very good_ data also has a proxy, which is perfect in Wang Zulan's style.

See our html interface, there is no data. We have prepared all the data above, and we will start to combine the data into the html interface.

Set of data to realize hmtl interface

First, annotate the html of calculation attribute, and then implement it

<!-- <p>Calculation properties:{{sum}}</p> -->

Then add a compile function to the Mvvm function, A kind of The sign indicates the added function

function Mvvm(options = {}) {
    this.$options = options
    let data = this._data = this.$options.data
    let vm = initVm.call(this)
+   new Compile(this.$options.el, vm) // Add a compile function
    return this._vm
}

We added a Compile constructor above. Send the configured el as a parameter to the machine, and the instance vm that generates the proxy, so that we can get the data under the vm, and then we will implement it. Just read the notes in order. It's easy to understand

// Compile class
class Compile {
    constructor (el, vm) {
        this.vm = vm // Save the vm passed in, because the vm.a = 1 is OK
        let element = document.querySelector(el) // Get the app node
        let fragment = document.createDocumentFragment() // Create fragment code fragment
        fragment.append(element) // Add app node to create fragment code fragment
        this.replace(fragment) // Set of data functions
        document.body.appendChild(fragment) // Finally added to body
    }
    replace(frag) {
        let vm = this.vm // Get the vm saved before
        // loop frag.childNodes
        Array.from(frag.childNodes).forEach(node => {
            let txt = node.textContent // Get the text for example: "development language: {{language}}"
            let reg = /\{\{(.*?)\}\}/g // Define matching regularity
            if (node.nodeType === 3 && reg.test(txt)) {
            
                replaceTxt()
                
                function replaceTxt() {
                    // Replace text if it matches
                    node.textContent = txt.replace(reg, (matched, placeholder) => {
                        return placeholder.split('.').reduce((obj, key) => {
                            return obj[key] // For example: to vm.makeUp.one Object get value
                        }, vm)
                    })
                }
            }
            // If there is a word node and the length is not 0 
            if (node.childNodes && node.childNodes.length) {
                // Direct recursive match replacement
                this.replace(node)
            }
        })
    }
}

The above compiled function, in a word, does everything possible to replace the placeholder of {{xxx}} with real data through regular.

Then refresh the browser, Dangdang gear Dangdang gear, the data we want appears.

It's very good, but our current data doesn't change. In order to change the data, we need to cooperate with the watcher. Let's implement subscription publishing first.

Implement subscription Publishing

Subscription publishing is actually a common programming pattern, which is simple and straightforward:

push the function into an array, and then loop the data to call the function.

For example: a very straightforward example

let arr = [] 
let a = () => {console.log('a')}

arr.push(a) // Subscribe to a function
arr.push(a) // Subscribe to a function again
arr.push(a) // Double subscription a function

arr.forEach(fn => fn()) // Publish all

// Three a's are printed

It's very simple. Let's implement our code

// Subscription class
class Dep {
    constructor() {
        this.subs = [] // Defining arrays
    }
    // Subscription function
    addSub(sub) {
        this.subs.push(sub)
    }
    // Publish function
    notify() {
        this.subs.filter(item => typeof item !== 'string').forEach(sub => sub.update())
    }
}

Subscription publishing is finished, but when to subscribe and when to publish?? At this time, we subscribe to the watcher during data acquisition, and then publish the watcher during data setting. In the above Observe class, see A kind of Code for. .

... //Omit code
...
proxy(data) {
    let dep = this.dep
    return new Proxy(data, {
        // Intercept get
        get: (target, prop, receiver) => {
+           if (Dep.target) {
                // If you've pushed before, you don't need to push again
                if (!dep.subs.includes(Dep.exp)) {
                    dep.addSub(Dep.exp) // hold Dep.exp .  push to sub array, subscribe
                    dep.addSub(Dep.target) // hold Dep.target .  push to sub array, subscribe
                }
+           }
            return Reflect.get(target, prop, receiver)
        },
        // Intercept set
        set: (target, prop, value) => {
            const result = Reflect.set(target, prop, observe(value))
+           dep.notify() // release
            return result  
        }
    })
}

The above code says, what the hell is a watcher? And then release the sub.update() what the hell is it??

We came to the watcher with a bunch of questions

Implement watcher

See detailed notes

// Watcher class
class Watcher {
    constructor (vm, exp, fn) {
        this.fn = fn // Incoming fn
        this.vm = vm // Incoming vm
        this.exp = exp // The passed match to exp for example: "language"“ makeUp.one "
        Dep.exp = exp // Mount an exp to the Dep class
        Dep.target = this // Attach a watcher object to the Dep class, which will be used when updating
        let arr = exp.split('.')
        let val = vm
        arr.forEach(key => {
            val = val[key] // Get the value, which will be issued roughly vm.proxy Add addSub subscription function in get()
        })
        Dep.target = null // After adding a subscription, put Dep.target empty
    }
    update() {
        // Set value will trigger vm.proxy.set Function, and then call the published notify.
        // Finally calling update, update continues to invoke. this.fn(val)
        let exp = this.exp
        let arr = exp.split('.')
        let val = this.vm
        arr.forEach(key => {
            val = val[key]
        })
        this.fn(val)
    }
}

The watcher class is the watcher we want to subscribe to. It has a callback function fn, an update function call FN,

We're all done. But where can I add a watcher?? The following code

In Compile

...
...
function replaceTxt() {
    node.textContent = txt.replace(reg, (matched, placeholder) => {
+       new Watcher(vm, placeholder, replaceTxt);   // Monitor changes, match and replace contents
        return placeholder.split('.').reduce((val, key) => {
            return val[key]
        }, vm)
    })
}

Now that we've added something, let's take a look at the console. The changes worked.

 

Then we look back at all the processes and see a picture of the old (I also got it from somewhere else).

Help understand

 

 

We have finished the responsive data. Let's finish the two-way binding.

Implement two-way binding

We see that there is a < input placeholder = "123" v-module = "language" / > in our html. V-module binds a language, and then the replace function in the Compile class. We add

replace(frag) {
    let vm = this.vm
    Array.from(frag.childNodes).forEach(node => {
        let txt = node.textContent
        let reg = /\{\{(.*?)\}\}/g
        // Judge nodeType
+       if (node.nodeType === 1) {
            const nodeAttr = node.attributes // Property collection
            Array.from(nodeAttr).forEach(item => {
                let name = item.name // Property name
                let exp = item.value // Property value
                // If the attribute has v-
                if (name.includes('v-')){
                    node.value = vm[exp]
                    node.addEventListener('input', e => {
                        // Equivalent to this.language Assigned a new value
                        // The change of value calls set, and set calls notify. The update method invoking watcher in notify implements the update operation.
                        vm[exp] = e.target.value
                    })
                }
            });
+       }
        ...
        ...
    }
  }

The above method is to bind our input node to a input event, then change our value when the input event is triggered, and the change of value will call set, and notify will be invoked in set. The update method invoked watcher in notify will update operation.

Then let's take a look at the interface

We have basically completed the two-way data binding. Don't forget that we have a commented out calculation property above.

Calculation properties

First, remove the < p > calculation attribute: {sum} < / P > comment, thinking that in the initVm function at the beginning, we added the code return this [key] | | this_ data[key] || this._ Calculated [key], now that we all understand, we just need to put this_ Just add a watcher to computed.

function Mvvm(options = {}) {
    this.$options = options
    let data = this._data = this.$options.data
    let vm = initVm.call(this)
    initObserve.call(this, data)
+   initComputed.call(this) // Add calculation function, change this point
    new Compile(this.$options.el, vm)
    return this._vm
}


function initComputed() {
    let vm = this
    let computed = this.$options.computed // Get the configured computed
    vm._computed = {}
    if (!computed) return // Return directly without calculation
    Object.keys(computed).forEach(key => {
        // It's equivalent to pointing this in sum to this_ VM, then you can get this.a, this, b
        this._computed[key] = computed[key].call(this._vm)
        // Add a new Watcher
        new Watcher(this._vm, key, val => {
            // It will be calculated every time it is set
            this._computed[key] = computed[key].call(this._vm)
        })
    })
}

The above initComputed is to add a watcher. The general process is as follows:

this._vm change -- > vm.set () --- > notify() --- > update() --- update interface

Last look at the picture

Nothing seems to be wrong~~~~

Add mounted hook

It's also easy to add mounted

// It's the same as Vue
let mvvm = new Mvvm({
    el: '#app',
    data: {
        ...
        ...
    },
    computed: {
        ...
        ...
    },
    mounted() {
        console.log('i am mounted', this.a)
    }
})

Add mounted in the new Mvvm,
Then go to function Mvvm and add

function Mvvm(options = {}) {
    this.$options = options
    let data = this._data = this.$options.data
    let vm = initVm.call(this)
    initObserve.call(this, data)
    initComputed.call(this)
    new Compile(this.$options.el, vm)
+   mounted.call(this._vm) // Add mounted, change direction
    return this._vm
}

// Run mounted
+ function mounted() {
    let mounted = this.$options.mounted
    mounted && mounted.call(this)
+ }

Print out after execution

i am mounted 1

End ~ ~ ~ scatter flowers

ps: for the compilation, refer to the operation of the God. @ chenhongdong Thank you

Finally attached, the source code address, download and run it directly.

Source address: https://github.com/naihe138/proxy-mvvm

Preview address: http://gitblog.naice.me/proxy-mvvm/index.html

Topics: Fragment Vue Attribute ECMAScript