Time is ticking away.

 Now that you’re here, keep looking.


As we all know now is the era of MVVM prevalence, from the early Angular to the current React and Vue, and then from the initial three worlds to the current two tigers.


Undoubtedly does not give us the development of an unprecedented new experience, say goodbye to the operation of the DOM thinking, replaced by a data-driven page of thought, really the progress of the times, changed us a lot of many.

 It’s not good to talk too much. Let’s get to the topic of the day

 


MVVM bi-directional data binding is handled in Angular 1.x through dirty value detection.


And now whether it’s React or Vue or the latest Angular, the implementations are actually much more similar

 That’s through data hijacking + publish subscription model


The real realization of the real rely on the ES5 Object.defineProperty, of course, this is not compatible so Vue and so on only supports IE8 +!

 Why is it?


Object.defineProperty() to be honest, we all do not use much in the development, most of the internal characteristics of the modification, but that is to define the object on the properties and values? Why do you make so much effort (purely personal thoughts)?


But in the implementation of frameworks or libraries but played a big role in the time, this is not much to say, only a piece of light boat, but not to write the strength of the library!

 Knowing what you know is necessary to know what you don’t know, so let’s see how to use the

let obj = {};
let song = 'fa'; 
obj.singer = 'jj';  

Object.defineProperty(obj, 'music', {
    // 1. value: 'qilixiang',
    configurable: true,    
    // writable: true,        
    enumerable: true,        
    get() {    
        return song;
    },
    set(val) {      
        song = val;   
    }
});

console.log(obj);   

delete obj.music;  
console.log(obj);   

obj.music = 'ting';   
console.log(obj);   
for (let key in obj) {    
    console.log(key);   // singer, music    4
}

console.log(obj.music);   
obj.music = 'yequ';       
console.log(obj.music);  


The above is about the usage of Object.defineProperty


Let’s write an example to see, here we use Vue as a reference to realize how to write MVVM

// index.html
<body>
    <div id="app">
        <h1>{{song}}</h1>
        <p>《{{album.name}}》{{singer}}</p>
        <p>{{album.theme}}</p>
        <p>{{singer}}</p>
        {{album.theme}}
    </div>
    <script src="mvvm.js"></script>
    <script>
        let mvvm = new Mvvm({
            el: '#app',
            data: {     // Object.defineProperty(obj, 'song', 'faruxue');
                song: 'faruxue',
                album: {
                    name: 'shiyi',
                    theme: 'ye'
                },
                singer: 'jj'
            }
        });
    </script>
</body>


The above is written in html, and I believe that those who have used Vue are not unfamiliar with it.

 So start implementing an MVVM of your own now!

 Building MVVM

function Mvvm(options = {}) {   
    this.$options = options;
    let data = this._data = this.$options.data;
    
    observe(data);
}

 data hijacking

 Why data hijacking?


  • Observe the object, add Object.defineProperty to the object

  • The vue feature is that you can’t add non-existing properties Non-existing properties don’t have get and set

  • Deep Response Because each time a new object is given it adds defineProperty to that new object (data hijacking)

 No point in talking about it, let’s look at the code together

function Observe(data) {
    for (let key in data) {    
        let val = data[key];
        observe(val);  
        Object.defineProperty(data, key, {
            configurable: true,
            get() {
                return val;
            },
            set(newVal) {  
                if (val === newVal) { 
                    return;
                }
                val = newVal;   
                observe(newVal);    
            }
        });
    }
}


function observe(data) {

    if (!data || typeof data !== 'object') return;
    return new Observe(data);
}


The above code realizes the data hijacking, but there may be some puzzling places such as: recursion

 To elaborate a bit more on why recursion is necessary, look at this chestnut

    let mvvm = new Mvvm({
        el: '#app',
        data: {
            a: {
                b: 1
            },
            c: 2
        }
    });

 Let’s look at it in the console.


Marked place is through the recursive observe(val) data hijacking added get and set, recursive continue to a inside the object to define the attributes, pro-tested through the safe to eat!


Next, a word on why observe(newVal) is also recursive here


Still on the lovely console, knock down this code mvvm._data.a = {b:’ok’}

 And then continue to look at the pictures.   By observing(newVal) added the
Now you understand why you need to recursively observe the new value you set, haha, so easy!

 Data hijacking is done. Let’s do another data proxy.

 data agent


The data proxy is so that each time we take the data in data, we don’t have to write a long string each time, such as mvvm._data.a.b. We can actually just write mvvm.a.b in this obvious way

 Read on below, the + sign indicates the realization part

function Mvvm(options = {}) {  
    observe(data);
+   for (let key in data) {
        Object.defineProperty(this, key, {
            configurable: true,
            get() {
                return this._data[key];     // 如this.a = {b: 1}
            },
            set(newVal) {
                this._data[key] = newVal;
            }
        });
+   }
}

console.log(mvvm.a.b);   // 1
mvvm.a.b = 'ok';    
console.log(mvvm.a.b);  // 'ok'


At this point, both data hijacking and data proxying have been implemented, so the next step is to compile it and parse out the contents of {{}}

 Data compilation

function Mvvm(options = {}) {
    // observe(data);
        
+   new Compile(options.el, this);    
}

function Compile(el, vm) {
    vm.$el = document.querySelector(el);
    let fragment = document.createDocumentFragment();
    
    while (child = vm.$el.firstChild) {
        fragment.appendChild(child);   
    }
    function replace(frag) {
        Array.from(frag.childNodes).forEach(node => {
            let txt = node.textContent;
            let reg = /\{\{(.*?)\}\}/g;   
            
            if (node.nodeType === 3 && reg.test(txt)) {
                console.log(RegExp.$1); 
                let arr = RegExp.$1.split('.');
                let val = vm;
                arr.forEach(key => {
                    val = val[key];     // 如this.a.b
                });
                node.textContent = txt.replace(reg, val).trim();
            }
            if (node.childNodes && node.childNodes.length) {
                replace(node);
            }
        });
    }
    
    replace(fragment);  
    
    vm.$el.appendChild(fragment);   
}


See here in the interview can already be the first to show, then a drum, do the whole set of things, to a dragon!


Now the data is ready to compile, but our manually modified data doesn’t change on the page


Let’s see how to deal with it, in fact, here we use a particularly common design pattern, publish-subscribe pattern

 Post a subscription


Publish-subscribe relies heavily on the array relationship, subscribe is to put in the function, publish is to let the function in the array execute

function Dep() {
    this.subs = [];
}
Dep.prototype = {
    addSub(sub) {   
        this.subs.push(sub);    
    },
    notify() {
        this.subs.forEach(sub => sub.update());
    }
};
function Watcher(fn) {
    this.fn = fn;   
}
Watcher.prototype.update = function() {
    this.fn();  
};

let watcher = new Watcher(() => console.log(111));  // 
let dep = new Dep();
dep.addSub(watcher);  
dep.addSub(watcher);
dep.notify();   //  111, 111

 Data Update View


  • Now we want to subscribe to an event that needs to refresh the view when the data changes, which needs to be handled in the replace replacement logic

  • Subscribe to the data via a new Watcher, and perform a change as soon as the data changes.
function replace(frag) {
    node.textContent = txt.replace(reg, val).trim();
+   new Watcher(vm, RegExp.$1, newVal => {
        node.textContent = txt.replace(reg, newVal).trim();    
+   });
}

function Watcher(vm, exp, fn) {
    this.fn = fn;
+   this.vm = vm;
+   this.exp = exp;
+   Dep.target = this;
+   let arr = exp.split('.');
+   let val = vm;
+   arr.forEach(key => {    
+      val = val[key];     
+   });
+   Dep.target = null;
}


The get method is automatically called when the value is fetched, so let’s go find the get method there in the data hijacker

function Observe(data) {
+   let dep = new Dep();
    Object.defineProperty(data, key, {
        get() {
+           Dep.target && dep.addSub(Dep.target);   
            return val;
        },
        set(newVal) {
            if (val === newVal) {
                return;
            }
            val = newVal;
            observe(newVal);
+           dep.notify();   
        }
    })
}


The dep.notify method is executed when set modifies the value, and this method executes the update method of the watcher, so let’s modify update a bit more

Watcher.prototype.update = function() {
+   let arr = this.exp.split('.');
+   let val = this.vm;
+   arr.forEach(key => {    
+       val = val[key];  
+   });
    this.fn(val);   
};


Now we can modify the view for data changes, which is good, there is one last point left, let’s take a look at the two-way data binding that is often tested in interviews

 Bidirectional data binding

    <input v-model="c" type="text">
    
    data: {
        a: {
            b: 1
        },
        c: 2
    }
    
    function replace(frag) {
+       if (node.nodeType === 1) { 
            let nodeAttr = node.attributes; 
            Array.from(nodeAttr).forEach(attr => {
                let name = attr.name;   // v-model  type
                let exp = attr.value;   // c        text
                if (name.includes('v-')){
                    node.value = vm[exp];   // this.c 为 2
                }
                new Watcher(vm, exp, function(newVal) {
                    node.value = newVal;   
                });
                
                node.addEventListener('input', e => {
                    let newVal = e.target.value;
                    vm[exp] = newVal;   
                });
            });
+       }
        if (node.childNodes && node.childNodes.length) {
            replace(node);
        }
    }


Great job, the interview asked Vue stuff but that’s all it is, what two-way data binding how to achieve, ask a little bit of heart, poor!!!!


Sir, please stay, I should have stopped, but on a whim (hand itch), write some more functions, and add a computed and mounted.


computed(computed property) && mounted(hook function)


    <p>{{sum}}</p>
    
    data: { a: 1, b: 9 },
    computed: {
        sum() {
            return this.a + this.b;
        },
        noop() {}
    },
    mounted() {
        setTimeout(() => {
            console.log('ok');
        }, 1000);
    }
    
    function Mvvm(options = {}) {
+       initComputed.call(this);     
        new Compile(options.el, this);
+       options.mounted.call(this);
    }
    
    function initComputed() {
        let vm = this;
        let computed = this.$options.computed;    {sum: ƒ, noop: ƒ}
        Object.keys(computed).forEach(key => {  
            Object.defineProperty(vm, key, {
                get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
                set() {}
            });
        });
    }


It’s not a small amount of content to write about, so let’s do a formal summary at the end

 In total, mvvm contains the following things through its own implementation


  1. Data hijacking via get and set of Object.defineProperty
  2.  Data proxying to this by traversing the data data
  3.  Compilation of data via {{}}
  4.  Synchronization of data and views through publish-subscribe pattern

By hbb

Leave a Reply

Your email address will not be published. Required fields are marked *