A brief overview of Proxy and Reflect

Proxy can be understood as a layer of “interception” before the target object. All external access to the object must pass this layer of interception. Therefore, Proxy provides a mechanism for filtering and rewriting external access. The word Proxy is used to mean that it acts as a Proxy for certain operations. From ruan Yifeng’s introduction to ECMAScript 6, click es6.ruanyifeng.com/#docs/proxy for details

Such as:

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}! `);returnReflect.set(target, key, value, receiver); }});Copy the code

The code above sets up a layer of interception on an empty object, redefining the read (get) and set (set) behavior of the property. I won’t explain the syntax here, but just look at the results. To read and write properties of obj, the object with interception behavior set, the following result is obtained.

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

The target parameter represents the target object to intercept, and the Handler parameter is also an object used to customize the interception behavior.

Note that for the Proxy to work, you must operate on the Proxy instance (the Proxy object in this example), not the target object (the empty object in this example).

Reflect objects, like Proxy objects, are a new API provided by ES6 for manipulating objects.

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

Also put ruanyifeng teacher link es6.ruanyifeng.com/#docs/refle…

Initialization structure

See here, I think you have a better understanding of Proxy (Proxy) is what to do, and then let’s look at the final figure to cheat.

Looking at the image above, first we create a new index.html and the code inside it looks like this. Very simple

<! DOCTYPE html> <html lang="en">
<head>
    <meta charset="UTF-8"> <title> simple MVVM </title> </head> <body> <div id="app"{{language}}</h1> < H2 > < / h2 > < ul > < li > {{makeUp. One}} < / li > < li > {{makeUp. Two}} < / li > < li > {{makeUp. Three}} < / li > < / ul > < h2 > description: < / h2 > < p > {{the describe}} < / p > < p > computational attributes: {{sum}} < / p > < input placeholder ="123" v-module="language"/> </div> <script> const MVVM = new MVVM ({el:'#app',
    data: {
        language: 'Javascript',
        makeUp: {
            one: 'ECMAScript',
            two: 'Document Object Model (DOM)',
            three: 'Browser Object Model (BOM)'
        },
        describe: 'There's no product you can't write about.',
        a: 1,
        b: 2
    },
    computed: {
        sum() {
        return this.a + this.b
    }
})
</script>
</body>
</html>

Copy the code

Seeing the code above, which is probably similar to vue, let’s implement the Mvvm constructor

Implement the Mvvm constructor

First declare an Mvvm function and pass in options as an argument. Options is the configuration of the code above, with EL, data, computed~~

functionMvvm(options = {}) {// Assign options to this.$options
    this.$options// Assign options.data to this._datalet data = this._data = this.$options.data
    let vm = initVm.call(this)
    return this._vm
}
Copy the code

$options and options.data to this._data. Then call initVm and change this to call call. It then returns a this._vm, which is generated in the initVm function.

So let’s continue with the initVm function,


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

Copy the code

This init function uses Proxy to intercept this object, produce the instance of Proxy and assign it to this._vm, and return this._vm,

As we said, in order for a Proxy to work, you have to target the Proxy instance.

In the proxy, it intercepts get and set, returns the key of this, and does not get a key from this._data, or a key from this._computed. The set function returns the key corresponding to this._data.

Do these kinds of interceptions. We can directly from the strength of the petition to ask us the corresponding value. (MVVM made our first code generation instance)

mvvm.b // 2
mvvm.a // 1
mvvm.language // "Javascript"
Copy the code

See the console above. You can set values, you can get values, but it’s not reactive.

Open the console and take a look

You can see it in detail. Only _VM is a proxy, so what we need is for all data under _data to have an intercepting proxy; So let’s do that.

Implement all data broker interception

We first add an initObserve to the Mvvm as follows

function Mvvm(options = {}) {
    this.$options = options
    let data = this._data = this.$options.data
    letCall (this, data) // Initialize data observed. call(this, data)return this._vm
}
Copy the code

InitObserve basically adds this._data to the proxy. The following


functionInitObserve (data) {this._data = observe(data) // Assign all observe to this._data} // Split this for the following recursive callfunction observe(data) {
    if(! data || typeof data ! = ='object') returnData // If not the object returns the value directlyreturnNew Observe(data) // Object call Observe}Copy the code

The following focuses on implementing the Observe class

Class Observe {constructor(data) {this.dep = new dep () // Subscribe classfor (let key inData) {data[key] = observe(data[key]) // Recursive call child 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)) // Add observe to the newly added objectreturn result  
        }
      })
    }
  }

Copy the code

So, let’s add all our _data objects as we recursively add the proxy, and look at the console again

Very good, _data also has proxy, very Wang Zulan perfect.

Look at our HTML interface, there is no data, above we have all the data ready, now we start to combine the data into the HTML interface.

Set of data to achieve HMTL interface

First, I will comment out the HTML for the calculated attributes, and then implement them

<! {{sum}}</p> -->Copy the code

Then add a compiler function to the Mvvm function, with ➕ indicating 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 compiler functionreturn this._vm
}
Copy the code

Above we added a Compile constructor. Pass in the configured EL as a parameter, and pass in the instance VM that generated the proxy, so that we can get the data under the VM, let’s implement it. Just read the comments sequentially. It’s easy to understand

// class Compile {constructor (el, vm) {this.vm = vm // Save the passed vm, because this vmletElement = document.querySelector(el) // Get the appletFragments = document. CreateDocumentFragment () / / create fragment code snippet fragments. Append (element) / / add app node to create fragments code snippet Enclosing the replace (fragments) / / set of data document () function. The body. The appendChild (fragments) / / finally added to the body} the replace (frag) {letVm = this.vm // Retrieve the previously saved VM // loop frag.childNodes array. from(frag.childNodes).foreach (node => {letTXT = node.textContent // Get text for example:"Language: {{language}}"
            letreg = /\{\{(.*?) \}\}/g // defines the matching reif (node.nodeType === 3 && reg.test(txt)) {
            
                replaceTxt()
                
                function replaceTxtTextContent = txt.replace(reg, (matched, placeholder) => {return placeholder.split('. ').reduce((obj, key) => {
                            returnObj [key] // for example, go to vm.makeup. One object and get the value}, vm)})}} // if there are bytes, and the length is not 0if(node.childnodes && node.childnodes.length) {// Direct recursive matching replaces this.replace(node)}})}}Copy the code

The above compiler function, in short, does its best to replace {{XXX}} placeholders with real data by re.

Then refresh the browser, bang, bang, bang, and there you have it.

That’s good, that’s good, but we don’t just change the numbers. You need subscriptions and publishing and Watcher to do that and the data changes. Let’s implement subscription publishing first.

Implementing subscription publishing

Subscribing to publish is a common programming pattern that can be put simply:

Push the function into an array and loop through the data to call the function.

Let me give you a straightforward example

let arr = [] 
let a = () => {console.log('a')} arr.push(a) // Subscribe to arr.push(a) // subscribe to arr.push(a) // double subscribe to arr.foreach (fn => fn()) // publish all // print three a'sCopy the code

That’s easy. Now let’s implement our code

// Subscribe class Dep {constructor() {this.subs = [] // define array} // Subscribe function addSub(sub) {this.subs.push(sub)} // publish functionnotify() { this.subs.filter(item => typeof item ! = ='string').forEach(sub => sub.update())
    }
}
Copy the code

Subscribe to publish is written, but when to subscribe, when to publish? In this case, we subscribe to Watcher when the data is fetched and publish watcher when the data is set. In the Observe class above, look at the code ➕. .

. // omit code... proxy(data) {let dep = this.dep
    returnNew Proxy(data, {// Intercept get Get: (target, prop, receiver) => {+if(dep.target) {// If the target was pushed before, there is no need to repeat the pushif(! Dep.subs.exp) {dep.addSub(dep.exp) // Dep.exp includes(dep.exp). Push to sub array, subscribe dep.addSub(dep.target) // add dep.target. Push into sub array, subscribe to} +}returnReflect.get(target, prop, receiver)}, // interceptset
        set: (target, prop, value) => {const result = reflect.set (target, prop, observe(value)) + dep.notify() // publishreturn result  
        }
    })
}
Copy the code

It says, “What the hell is watcher?” What the hell is sub.update()?

We came to Watcher with a lot of questions

Realize the watcher

Look at the details

Class constructor {constructor (vm, exp, constructor) Fn) {this.fn = fn // the passed fn this.vm = vm // the passed VM this.exp = exp // the passed vm matches exp for example:"language"."makeUp.one"Dep. Target = this // Attach a watcher object to Deplet arr = exp.split('. ')
        letVal = vm arr. ForEach (key => {val = val[key] // get(); Dep. Target = null // Dep. Target = null}update() {// Set the value to vm.proxy.set, and then call the notify function. // Update, which then calls this.fn(val).let exp = this.exp
        let arr = exp.split('. ')
        let val = this.vm
        arr.forEach(key => {
            val = val[key]
        })
        this.fn(val)
    }
}

Copy the code

The Watcher class is the Watcher we want to subscribe to. It has fn, the callback function, fn, the update function,

We’re all set. But where do you add Watcher? The following code

In the Compile

. .function replaceTxt() { node.textContent = txt.replace(reg, (matched, placeholder) => { + new Watcher(vm, placeholder, replaceTxt); // Listen for changes to match the replacement contentreturn placeholder.split('. ').reduce((val, key) => {
            return val[key]
        }, vm)
    })
}

Copy the code

Now that I’ve added something nice, let’s look at the console. The revision found that it worked.

Then we review all the procedures and see an old drawing that I got from somewhere else.

Help understand

Now that we’ve done the reactive data, let’s do the bidirectional binding.

Implement bidirectional binding

replace(frag) {
    let vm = this.vm
    Array.from(frag.childNodes).forEach(node => {
        let txt = node.textContent
        letreg = /\{\{(.*?) \}\}/g // Determine nodeType +if(node.nodeType === 1) {const node.attributes = node.attributes // Array.from(nodeAttr).foreach (item => {letName = item.name // Attribute nameletExp = item.value // Attribute value // If attribute has v-if (name.includes('v-')){
                    node.value = vm[exp]
                    node.addEventListener('input', e => {// assigns a new value to this.language and the value change is calledset.setVm [exp] = e.troge.value})} vm[exp] = e.troge.value}); +}... . }}Copy the code

The method above is to bind our input node to an input event, and then change our value when the input event is triggered. The change in value is called set, which in turn is called notify. In notify, watcher’s Update method is called to update.

And then let’s look at the interface

We’re almost done with two-way data binding, and don’t forget, we have a commented out calculation property above.

Calculate attribute

Count

first: {{sum}} < / p > annotation, thought the above initVm function in the beginning, we add the code to return this [key] | | this. _data while forming [key] | | this. _computed [key], here everyone see, Just add a Watcher to this._computed as well.

function Mvvm(options = {}) {
    this.$options = options
    let data = this._data = this.$options.data
    letCall (this, data) + initobserve.call (this, data) // Add a calculation function and change this to new Compile(this).$options.el, vm)
    return this._vm
}


function initComputed() {
    let vm = this
    let computed = this.$options. Computed // Get configured computed vm._computed = {}if(! computed)return// Return object.keys (computed). ForEach (key => {// This in sum points to this._vm, A, this, b this._computed[key] = computed[key]. Call (this._vm) // Add new Watcher new Watcher(this._vm, key, Val => {// Computes this._computed[key] = computed[key].call(this._vm)})})}Copy the code

InitComputed above is just adding a Watcher, and roughly how it works:

This. _vm change — – > the vm. The set () – > notify () – > update () – > update interface

And finally look at the picture

Everything seems to be all right ~~~~

Add mounted hook

Adding Mounted is also simple

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

Copy the code

Add Mounted to new Mvvm and add function Mvvm

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._vmreturn_vm} // Run mounted +function mounted() {
    let mounted = this.$options.mounted
    mounted && mounted.call(this)
+ }

Copy the code

It will be printed after execution

i am mounted 1
Copy the code

End ~~~~ scatter flowers

Ps: compile inside, refer to this big god operation. @chenhongdong, thank you big guy

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

Source code address: github.com/naihe138/pr…

Gitblog.naice. Me /proxy-mvvm/…