Dopro. IO/vue-mvVM-re…

1. MVVM mode

2. Core methodsObject.definePropertyThe understanding of the

var a = {};

Object.defineProperty(a, 'b', {    value: 123.// Set the property value    writable: false.// Whether it is read-only    enumerable: false.// Whether enumerable    configurable: false //

});

console.log(a.b); / / 123Copy the code

The method is simple to use; it takes three parameters, all of which are required

  • The first parameter: the target object
  • Second argument: The name of the property or method to define.
  • The third parameter: the properties owned by the target property
  • Value: Indicates the value of the attribute.
  • Writable: If false, the value of the property cannot be overridden and must be read-only.
  • Enumerable: Not enumerable, default is false (usually set to true)
  • The control switch, writable and Enumerable, works without any additional control system. Once the control is false, no additional control can be set.
  • Get () : function used to get property values (cannot coexist with writable and value properties)
  • Set () : function that executes when setting property values (cannot coexist with writable and value properties)
// Common definitions

var obj = {};

Object.defineProperty(obj, 'school', {    enumerable: true,    get: function(a) {        

       // The get method is called when the property value is obtained    },    set: function(newVal) {        

       // The set method is called when the property value is set        return newVal    } });Copy the code

DefineProperty (object.defineProperty) ¶ We can use object.defineProperty to implement listening on defined reference data types. The get and set methods of parameter three in Object.defineProperty are triggered, respectively.

3. Data hijacking

Obaerve



function
MyVue(options = {})
{    

   // Mount all attributes to $options    this.$options = options;    

   // Get data (Model)    var data = this._data = this.$options.data;    

   // Hijacking data    observe(data) }



// Add object.defineProperty to all the objects you want to observe

function Observe(data) {    

for (let key in data) {        

       let val = data[key];        

       // Recursive = "to implement deep data listening        observe(val)        

Object.defineProperty(data, key, {            enumerable: true,            get() {                

               return val            },            set(newval) {                if (val === newval) { // Is the value set the same as before, if so, do nothing                    return                }                val = newval // Add a data hijacking for depth responseobserve(newval); }}}})

function observe(data) {

   // Only reference data types are used for data hijacking    if (typeofdata ! ='object') return    return new Observe(data) }Copy the code

1) The above code does this by defining the initial transform constructor MyVue to get the data we passed in and the DOM node scope we defined, and then passing the data to the specified data hijacking method Observe

2) Observe realizes the overall logic of data monitoring. There is a detail here. Instead of using the constructor Observe to hijack our data, it writes an extra Observe method to use new Observe and makes a judgment of reference data type in it. The purpose of this is to facilitate the recursive implementation of the deep listening of the data structure, because our data structure must be complex and diverse, such as the following code

// There are a lot of nested data structures here. To implement deep data monitoring, we use recursive implementation mode

data: { a: {b:2} , c:{q:12,k:{name:'binhemori'}} , f:'mvvvm',o:[12.5.9.8]}Copy the code

Observe observe observe observe observe observe observe observe observe observe observe observe observe observe Can’t add nonexistent attributes and can’t exist attributes that don’t have get and set methods. If the value assigned to the new attribute refers to a data type, it will replace the address of the object where we performed the previous data hijacking, and the new object is not data hijacking, that is, there is no get and set methods. So when we set the new value, we need to perform the observe data hijacking again to ensure that the developer can be hijacked regardless of setting the value


Having said that, let’s use it to see if we can achieve data hijacking (data listening)

<div id="app">     

   <div>         <div>The data here is 1======<span style="color: red;">{{a.b}}</span></div>         <div>Here's data 2======<span style="color: green;">{{c}}</span></div>     </div>     <input type="text" v-model="a.b" value=""> </div> <! -- Introduce your own MVVM module <script src="./mvvm.js"></script> Copy the code

<script type="text/javascript">     var myvue = new MyVue({         el: '#app',         data: { a: { b: 'hello' }, c: 12, o: [12.5.9.8]}})

</script>

We can see that we already have get and set methods for the data in our defined data, so we can listen to the data changes in the data

4. Data broker

Data agent, we have used vUE all know, in the actual use is directly through the instance + attribute (vm.a) directly to obtain data, and our above code to obtain data also need to myvue._data. A to obtain data, there is a _data link in the middle, so it is not very convenient to use. Myvue. A = myvue. A = myvue. A = myvue

function MyVue(options = {}) {    

   // Mount all attributes to $options    this.$options = options;    

   // Get data (Model)    var data = this._data = this.$options.data;    observe(data);    

   

   // this is the proxy data this._data    for (const key in data) {        

   Object.defineProperty(this, key, {            enumerable: true,            get() {                

               // this.a is actually the value of _data                return this._data[key]            },            set(newVal) {                // Change the value of this._data.a                this._data[key] = newVal            }        })    } }Copy the code

The above code implements our data proxy, that is, when building the instance, we iterate the data in data and add it to this one by one. Don’t forget to add Object.defineProperty during the adding process, we need to add listener for any data. We have implemented the proxy to the data in the figure below

5. Compile the template

Now that we’ve hijacked the data and implemented this as a proxy for the data, what we need to do is compile the data to our DOM node, so that the view layer will display our data

// Mount data and nodes together

function Compile(el, vm) {    

   // el indicates the range of substitution    vm.$el = document.querySelector(el);    

   Note that we are not manipulating the DOM directly. Instead, we are moving this step into memory. This operation does not cause the DOM node to flow back    let fragment = document.createDocumentFragment(); // Document fragments    let child;  

   

   while (child = vm.$el.firstChild) {

       // Move the contents of the app into memory        fragment.appendChild(child);    }

           replace(fragment)    

   function replace(fragment) {        

       Array.from(fragment.childNodes).forEach(function (node) { // Loop through each layer            let text = node.textContent;            

           let reg = /\{\{(.*)\}\}/g;

                       

           // Only text nodes are matched, and {{***}} is required            if (node.nodeType === 3 && reg.test(text)) {  

               // Split the matched contents into arrays              

               let arr = RegExp. $1.split('. ');                let val = vm;                

               

               // The array of definitions that we match will be traversed in turn to implement the deep assignment to the instance                arr.forEach(function (k) { // this.a.b this.c                    val = val[k]                })      

                         

               // Replace val with the string replace method                node.textContent = text.replace(/ \ {\ {(. *) \} \} /, val)            }        

                  

           // Use recursion if there are child nodes            if (node.childNodes) {                replace(node)            }        })    }    

   // Finally add the compiled DOM to the app element    vm.$el.appendChild(fragment) }Copy the code

The above code implements our Compile of data as shown in the figure below. It can be seen that we store all the child nodes obtained below EL in the document fragment and store them temporarily (in memory). Since DOM manipulation and DOM search are frequently needed here, we move them to the memory for operation

  • AppendChild (child); fragment.appendChild(child);
  • 2) Then we use the replace method to traverse all the child nodes in the document, get all the contents in their text nodes (node.nodeType = 3) with {{}} syntax, split the matched values into arrays, and then traverse to find and obtain data in sequence. The traversal node continues to use replace until undefined if it has children
  • $el.appendChild(fragment); replace {{}} with {{}};

6. Associating Views with Data

After successfully binding our data to the DOM node, we need to associate our view layer with our model layer, which is not actually related yet, because we can’t change the data value to cause the view to change. Publish subscribe is also a key step in vUE’s implementation of bidirectional data binding

Publish subscribe model (also known as observer model)

Let’s just manually implement one.

// Publish a subscription

function Dep(a) {    

   this.subs = [] }

/ / subscribe

Dep.prototype.addSub = function (sub) {    

   this.subs.push(sub) }

/ / notice

Dep.prototype.notify = function (sub) {    

   this.subs.forEach(item => item.update()) }

   

// Watcher is a class that creates functions with update methods

function Watcher(fn) {    

   this.fn = fn; } Watcher.prototype.update = function (a) {    

   this.fn() }Copy the code

In this class, we have addSub and notify. We add what we want to do to an array by addSub, and when the time is right, we execute all of the notify methods

And you’ll see why we have to define a method that creates a function, watcher, instead of just throwing it into addSub ok, that’s a dead end, isn’t it? This has its purposes, and one of the benefits is that every function we create with Watcher will have an update method that we can easily call. And the other thing I’m going to talk about, let’s put it to use

function replace(fragment) {     

Array.from(fragment.childNodes).forEach(function (node) {         let text = node.textContent;        

        let reg = /\{\{(.*)\}\}/g;  

             

        if (node.nodeType === 3 && reg.test(text)) {            

            let arr = RegExp. $1.split('. ');            

            let val = vm;             arr.forEach(function (k) {                 val = val[k]             })            

            // Here we use the Watcher function to add things to do             new Watcher(vm, RegExp. $1.function (newVal) {                 node.textContent = text.replace(/ \ {\ {(. *) \} \} /, newVal)             }) Copy the code

            node.textContent = text.replace(/ / {{(. *)}}, val)         }        

           

        if (node.childNodes) {             replace(node)         }     }) }

In replace, we have added the function “watcher” method to the replace method, but we have added two more parameters, vm, RegExp.$1, and we have added some new elements to the method, because new Watcher causes several operations to occur.

// where the VM does the data proxy

function MyVue(options = {}) {    

this.$options = options;    

   var data = this._data = this.$options.data;    observe(data);    

   for (const key in data) {        

       Object.defineProperty(this, key, {            enumerable: true,            get() {                

               return this._data[key]            },            set(newVal) {                this._data[key] = newVal            }        })    } }

// Data hijacking function

function Observe(data) {          let dep = new Dep();    

for (let key in data) {        

let val = data[key];        observe(val)        

       Object.defineProperty(data, key, {            enumerable: true,            get() {                

               /* Dep.target when fetching the value

For watcher's instance, add the instance he created to the subscription queue

* /
               Dep.target && dep.addSub(Dep.target);                return val            },            set(newval) {                if (val === newval) {                    

                   return                }                val = newval;                observe(newval);                

               // Let all the watcher.update methods execute while setting the value to trigger all data updates                dep.notify()            }        })    } }



function
Watcher(vm, exp, fn)
{    

   this.fn = fn;    

   // Here we add some content to get the correct data    this.vm = vm;    

   this.exp = exp;    Dep.target = this    let val = vm;    

   let arr = exp.split('. ');    

   /* execute this step on vm.a,

This step is actually the operation of the vm._data.a operation,

Triggers data from this agent and data above _data

* /
   arr.forEach(function (k) {        val = val[k]    })    Dep.target = null; }

// Here is the set value operation

Watcher.prototype.update = function (a) {    

   let val = this.vm;    

   let arr = this.exp.split('. ');    arr.forEach(function (k) {        val = val[k]    })    

   this.fn(val) // Pass a new value

}Copy the code

It’s going to get a little convoluted at first, but it’s important to understand the get and set of the data on that instance that you’re manipulating

1) The Watcher constructor has some new private properties representing:

  • Dep.target = this (constructor dep. target temporarily stores the current instance of Watcher)
  • This.vm = vm (vm = myvue instance)
  • This. exp = exp (exp = match the search object “a.b” is a string value)

Now that we’ve stored these properties, what we’re going to do is get the string that we’re going to match with exp for the data
vm.a.bExp is a string. You can’t just value vm[a.b] like this. This is a syntax error, so loop to get the correct value

arr.forEach(function (k) {        

       // arr = [a,b]        val = val[k] })Copy the code
  • The first loop is vm[a] = {b:12}, and the assignment changes the current val to a
  • On the second loop, val becomes a, k becomes B, and val becomes a[b] = 12

After two walks we get the value is the A object on top of the VM proxy data,
That is, the method that triggers a get for the data on the proxy data(vm.a.b) return this._data[k], which triggers the get method on vm._data. A.b, which goes to the get function in the Observe function. The dep. target stores an instance of the current Watcher method (which already has the information to manipulate the data) and passes the latest value to the method

get() {    

   // Dep.target already stores the current instance of Watcher, adding the instance he created to the subscription queue    Dep.target && dep.addSub(Dep.target);    return val },

   

// Use Watcher to define the actions to be done to update the view layer

new Watcher(vm, RegExp. $1.function (newVal) {    node.textContent = text.replace(/ \ {\ {(. *) \} \} /, newVal) }) Copy the code

Watcher.prototype.update = function (a) {    

let val = this.vm;    

let arr = this.exp.split('. ');    arr.forEach(function (k) {        val = val[k]    })    this.fn(val) // Pass the new value to the method

}

A = vm.a = vm.a = vm.a = vm.a = vm.a = vM. a = vM. a = vM. a = vM. a = vM. a Let’s see if we can implement data changes that trigger changes in the view layer

This is where the data change triggers the view layer update operation

7, input two-way data binding implementation

The last step is to realize the change of the view layer to trigger the change operation of the data structure. Above, we have explained the core code of the view and data association, and the rest of the view to trigger the change of data is easier to achieve

<div id="app">    

   <div>        <div>The data here is 1======<span style="color: red;">{{a.b}}</span></div>        <div>Here's data 2======<span style="color: green;">{{c}}</span></div>    </div>    <input type="text" v-model="a.b" value="">

</div>



<! -- Introduce your own MVVM module

<script src="./mvvm.js"></script>

<script type="text/javascript">    var myvue = new MyVue({        el: '#app',        data: { a: { b: 'hello' }, c: 12, o: [12.5.9.8]}})

</script>
Copy the code
// Get all element nodes

if (node.nodeType === 1) {    

   let nodeAttr = node.attributes    

   Array.from(nodeAttr).forEach(function (attr) {        

       let name = attr.name; // v-model="a.b"        let exp = attr.value; // a.b Copy the code

       if (name.indexOf('v-') > =0) {            

           let val = vm;            

           let arr = exp.split('. ');            arr.forEach(function (n) {                val = val[n]            })            

           // This is easy to handle, just set the corresponding value to input.value            node.value = val;        }        

       // We need to define a Watcher here, too, because when data changes, we also need to change the value with the v-Model attribute name        new Watcher(vm, exp, function (newVal) {            node.value = newVal        })        

       // Change the value of the data structure when the view changes        node.addEventListener('input'.function (e) {            

           let newVal = e.target.value            

           if (name.indexOf('v-') > =0) {                

               let val = vm;                

               let arr = exp.split('. ');                arr.forEach(function (k,index) {                        if (typeof val[k] === 'object') {                        val = val[k]                    } else{                        

                   if (index === arr.length-1) {                            val[k] = newVal                        }                    }                })            }        })    }) }

Node.addeventlistener (‘input’); node.addeventListener (‘input’); It is important to note that there is a reference to the datatype, otherwise the loop will reach the lowest datatype value (that is, the base datatype). Index === arr. Length -1 index === arr. Length -1 index === arr

arr.forEach(function (k,index) {     
    if (typeof val[k] === 'object') {        

       // If there is nesting, continue to search        val = val[k]    } else{        

       if (index === arr.length-1) {            

       // Assign the value directly after finding the last one            val[k] = newVal        }    } })Copy the code

The above is the whole MVVM bidirectional data binding simple implementation principle, some of the content is not well explained or better comments welcome to leave a comment 🙂

Welcome to “Tencent DeepOcean” wechat public account, weekly push you front-end, artificial intelligence, SEO/ASO and other related original quality technical articles:

See xiaobian handling so hard, pay attention to a bai 🙂