reprintedShallow exploration of VUE’s MVVM pattern implementation

1. MVVM mode

MVVM design idea: pay attention to the change of Model (data), let MVVM framework to automatically update the DOM state, the mainstream implementation is: Angular vUE (dirty value detection) VUE (data hijacking) VUE (publish subscribe) Angular VUE (data hijacking) VUE (publish subscribe

2. Understanding the core method Object.defineProperty

Let’s take a quick look at this method, which is also the key method used to implement our data hijacking. We know that the Vue framework is not compatible with IE6~8, mainly because it uses the Object.defineProperty method in ES5, and it doesn’t have a good degradation scheme at the moment.

    var a = {};
    Object.defineProperty(a, 'b', {value: 123, // Set the property value writable:false// Enumerable:false, // Supports additional control: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

The first two parameters are easier to understand, but the third parameter is an object, and let’s see what properties are defined

  • 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)
Var obj = {}; Object.defineProperty(obj,'school', {
        enumerable: true,
        get: function(){// Get method is called when the attribute value is obtained},set: function(newVal){// is called when the property value is setsetmethodsreturn 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

Now that we know about the Object.defineProperty method, we’re going to use it to implement our data hijacking method, Obaerve, as shown in the following code

    functionMyVue(options = {}){// Mount all attributes to$optionsOn this.$options= options; Var data = this._data = this.$options.data; Observe (data)} // Add object.defineProperty to all objects that need to be observedfunctionObserve(data){    
        for (let key in data) {        
            letval = data[key]; 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 nothingreturnObserve (newval); // Observe (newval); }})} functionobserve(data){// Only reference the data typeif(typeof data ! ='object') return
        returnnew 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

Data :{a: {b:2}, c:{q:12,k:{name:'binhemori'}} , f:'mvvvm'O:,5,9,8 [12]}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> Data here 1======<spanstyle="color: red;"> {{a.}} < / span > < / div > < div > here is the data 2 = = = = = = < spanstyle ="color: green;">{{c}}</span></div>
         </div>
         <inputtype="text"v-model="a.b"value=""></div><! -- introduce your own MVVM module --><scriptsrc="./mvvm.js"></script><scripttype="text/javascript">
         var myvue = new MyVue({
             el: '#app',
             data: { a: { b: 'hello' }, c: 12, o: [12, 5, 9, 8] }
         }) 
    </script>
Copy the code

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

    functionMyVue(options = {}){// Mount all attributes to$optionsOn this.$options= options; Var data = this._data = this.$options.data; observe(data); // this is the proxy data this._datafor (const key in data) {        
        Object.defineProperty(this, key, {
                enumerable: true.getReturnthis._data[key]} () {returnthis._data[key]};set(newVal) {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 togetherfunctionCompile(el, vm){// el indicates the replacement scope 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 backletfragment = document.createDocumentFragment(); // Document fragmentslet child;   
        
        while (child = vm.$el.firstChild) {// Move the contents of the app into memory fragment.appendChild(child); } replace(fragment) functionreplace(fragment){ Array.from(fragment.childNodes).forEach(function(node){// Loop through each layerlet text = node.textContent;            
                letreg = /\{\{(.*)\}\}/g; // Only text nodes are matched, and {{***}} is requiredif(node.nodeType === 3 && reg.test(text)) {// Split the matched contents into arrayslet arr = RegExp.The $1.split('. '); 
                    letval = vm; Arr. ForEach (int arr. ForEach (int arr. ForEach (int arr.functionVal node.textContent = text.replace(/\{\{(.*)\}\}/, } // We make a judgment and use recursion if there are childrenif(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 subscriptionfunctionDep(){this.subs = []} // Subscribe to dep.prototype.addSub =function(sub){this.subs.push(sub)} // Dep.prototype. Notify =function(sub){this.subs.foreach (item => item.update())} // Watcher is a class that creates functions with update methodsfunctionWatcher(fn){    
        this.fn = fn;
    }
    Watcher.prototype.update = function(){    
        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

    functionreplace(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.The $1.split('. ');             
                 let val = vm; 
                 arr.forEach(function(k){val = val[k]}) new Watcher(vm, RegExp);The $1.function(newVal){
                     node.textContent = text.replace(/\{\{(.*)\}\}/, newVal)
                 })
    
                 node.textContent = text.replace(/\{\{(.*)\}\}/, val)
             }         
                 
             if (node.childNodes) {
                 replace(node)
             }
         })
     }
Copy the code

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 proxyfunctionMyVue(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() {                
                    returnthis._data[key] 
                },
                set(newVal) {this._data[key] = newVal}})}} // data hijacking functionfunctionObserve(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 adds the instance watcher created to the subscription queue */ dep.target && dep.addSub(dep.target);return val
                },
                set(newval) { 
                    if (val === newval) {                    
                        return} val = newval; observe(newval); Dep.notify ()}})}} dep.notify()}}functionWatcher(vm, exp, fn){ this.fn = fn; This.vm = vm; this.vm = vm; this.exp = exp; Dep.target = thislet val = vm;    
        let arr = exp.split('. '); /* Execute this step on vm.a, and execute this step on vm._data.a, and execute this step on vm._data.function(k){ val = val[k] }) Dep.target = null; } // Here is the set value operation watcher.prototype.update =function() {let val = this.vm;    
        let arr = this.exp.split('. ');
        arr.forEach(function(k){val = val[k]}) this.fn(val);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)

After storing these attributes, we need to retrieve the value of exp in the string that matches the data, namely vm.a.b, but exp is a string, and you can’t directly value vm[a.b] like this. This is a syntax error, so we need to loop to retrieve 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 iterations, we get the value of the A object on the VM proxy data, which triggers the get method on the proxy data (vm.a.b). This returns this._data[k], which triggers the get method on the vm. The dep. target stores an instance of the current Watcher method (which already has information about how to manipulate data) and passes the latest value to the method

    getDep.target (dep.target) {dep.target (dep.target); dep.target (dep.target);returnNew Watcher(vm, RegExp); // Update the view layer with a new Watcher(vm, RegExp).The $1.function(newVal){
        node.textContent = text.replace(/\{\{(.*)\}\}/, newVal)
    })
    
    Watcher.prototype.update = function() {let val = this.vm;    
        let arr = this.exp.split('. ');
        arr.forEach(function(k){val = val[k]}) this.fn(val);Copy the code

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> Data here 1======<spanstyle="color: red;"> {{a.}} < / span > < / div > < div > here is the data 2 = = = = = = < spanstyle ="color: green;">{{c}}</span></div>
        </div>
        <inputtype="text"v-model="a.b"value=""> </div> <! -- introduce your own MVVM module --> <scriptsrc="./mvvm.js"></script>
    <scripttype="text/javascript">
        var myvue = new MyVue({
            el: '#app',
            data: { a: { b: 'hello'}, c: 12 o: [12, 5, 9, 8]}}) < / script > / / get all the element nodeif (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
    
            if (name.indexOf('v-') > = 0) {let val = vm;            
                let arr = exp.split('. ');
                arr.forEach(function(n){val = val[n]}) node.value = val; } new Watcher(vm, exp, exp, vmodel, vmodel, vmodel, vmodel, vmodel, vmodel, vmodel, vmodel)functionNode.addeventlistener (newVal){node.value = newVal})'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
                            }
                        }
                    })
                }
            })
        })
    }
Copy the code

Node.addeventlistener (‘ input ‘) is the same as the logic in section 6 of associating views with data. It is important to note that there is a reference to the datatype, otherwise it will loop to the lowest datatype value.

(val = val[k]) (val = val[k])

Index === arr. Length -1; if so, assign the value from the input to the current data

    arr.forEach(function(k,index){     
        if (typeof val[k] === 'object'Val = val[k]}else{        
            if(index === arr. Length -1) {// if (index == arr. Length -1) {val[k] = newVal}}}Copy the code

This is a simple implementation of MVVM bidirectional data binding.