The development of MVVM

MVVM two-way data binding was handled by dirty detection in Angular1.x. Now React, Vue, and Angular are implemented in much the same way: data hijacking + publish/subscribe. DefineProperty has compatibility issues, which is why Vue only supports IE8+

defineProperty

In real development, few people modify internal properties directly by manipulating Object.defineProperty(), but it can be very useful when implementing frameworks or libraries.

So how does defineProperty work

let obj = {};
let hero = 'Luban No. 2';
obj.player = 'pupil';

Object.defineProperty(obj, 'hero', {
    // value: 'ruban'
    configurable: true.// You can configure objects to delete properties
    // writable: true, // objects can be modified
    enumerable: true.// Can be enumerated
    			       // Get,set cannot set writable and value, they are mutually exclusive instead of both
    get() {                    // Get is called when obj. Game is retrieved
        return hero;
    },
    set(val) {     			   // Reassign the changed value to herohero = val; }});// The following printed parts are the corresponding code to write the order of execution
console.log(obj);   // {player: 'hero ',hero:' hero '}

delete obj.hero;   // If you want to delete any property in the OBj, set the 64x to true
console.log(obj);   {player: 'pupil '}

obj.hero = 'his';   // If you want to modify obj properties, writable must be set to true
console.log(obj);   // {player: 'hero ', hero:' hou yi '}

for (let key in obj) {
    // Attributes defined by defineProperty cannot be enumerated (iterated) by default
    // Set Enumerable to true
    // You can't get hero, you can only get player
    console.log(key);   // player, hero
}

console.log(obj.hero);   // 'Houyi'
obj.hero = "Concubine";       // Call set to set the new value
console.log(obj.hero);   // 'Yu Ji'
Copy the code

DefineProperty (object.defineProperty ())

Let’s write an example. Here we are using Vue as a reference to implement how to write MVVM

// index.html
<body>
    <div id="app">
        <h1>{ {hero} }</h1>
        <p>Height {{info.height}}, IQ {{info.IQ}} canyon hero</p>
        <p>The player that operates it is {{player}}.</p>
    </div>
    <! -- implement MVVM -->
    <script src="mvvm.js"></script>
    <script>
        // Write the same as Vue
        let mvvm = new Mvvm({
            el: '#app'.data: {     
                hero: 'Luban No. 2'.info: {
                    height: '150cm'.IQ: 250
                },
                player: 'pupil'}});</script>
</body>
Copy the code

Above is the HTML writing method, I believe that the students who have used Vue is not unfamiliar

So let’s go from zero to one and simply implement your own MVVM

To build the MVVM

// Create an Mvvm constructor
// Es6 defaults are used here
function Mvvm(options = {}) {
    $options Vue is where all attributes are mounted
    // So we do the same and mount all the attributes to $options
    this.$options = options;
    // this._data is the same as Vue
    let data = this._data = this.$options.data;

    // Data hijacking
    observe(data);
}
Copy the code

The data was hijacked

Why data hijacking? Whenever we access or set a property of an object, we fire the corresponding function, which then returns or sets the value of the property. In this case, we can of course do something we want to do when we fire a function, which is called hijacking.

  • Observe the Object and add Object.defineProperty to the Object
  • Vue adds non-existent attributes that are non-reactive, that is, no get and set
  • Depth response because every time we assign a new object we add defineProperty to that new object

There’s no point in talking. Look at the code

// Create an Observe constructor
// Write main logic for data hijacking
function Observe(data) {
    // Data hijacking is to add get,set to an object
    // Iterate over the object first
    for (let key in data) {     // Define the data attribute via defineProperty
        let val = data[key];
        observe(val);   // Continue to recurse down to achieve deep data hijacking
        Object.defineProperty(data, key, {
            configurable: true.get() {
                return val;
            },
            set(newVal) {   // Change the value
                if (val === newVal) {   // Set the same value as before and ignore it
                    return;
                }
                val = newVal;   // If you get the value later, return the value set earlier
                observe(newVal);    // When set to a new value, you also need to define the new value as an attribute}}); }}// Write another function outside
// Do not write a new for each call
// Also convenient for recursive calls
function observe(data) {
    // Return if it is not an object
    // Prevent recursion overflow
    if(! data ||typeofdata ! = ='object') return;
    return new Observe(data);
}
Copy the code

The above code implements data hijacking, but there may be some confusion such as: recursion

Let’s talk a little bit more about recursion. See this chestnut

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

Let’s take a look at it in the console

The marked area is data hijacking via the recursion observe(val), adding get and set, recursion continues to define attributes for the object in A, and the test passes

Observe (newVal) why is it recursive here

Comment out observe(newVal) and on the lovely console, type mvvm._data.a = {b:’ OK ‘}

Then continue to look at the picture and talk

Uncomment observe(newVal)

Now you understand why it is necessary to recursively observe the set new value.

Data hijacking is complete, if we need to get data every time mvm. _data.b, it will be very troublesome, make a data proxy to write mvm. a.b the obvious way

Data brokers

Moving on, the + sign represents the implementation partfunction Mvvm(options = {}) {
    // Data hijacking
    observe(data);
    // this proxies this._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; }}); +}}// Now we can simplify
console.log(mvvm.a.b);   / / 1
mvvm.a.b = 'ok';
console.log(mvvm.a.b);  // 'ok'
Copy the code

Data hijacking and data proxying have been implemented, so the next step is to compile the HTML and parse out the content of the data used in it

Data compilation

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

// create the Compile constructor

function Compile(el, vm) {
    // Mount el to the instance for convenience
    vm.$el = document.querySelector(el);
    // Get all the contents in the el range, but not one by one
    // You can choose to move it to memory and then put it in the document fragment to save overhead
    let fragment = document.createDocumentFragment();

    while (child = vm.$el.firstChild) {
        fragment.appendChild(child);    // Put the contents of el into memory
    }
    // Replace the contents of el
    function replace(frag) {
        Array.from(frag.childNodes).forEach(node= > {
            let txt = node.textContent;
            let reg = / \ {\ {(. *?) \}\}/g;   // Regular match {{}}
            if (node.nodeType === 3 && reg.test(txt)) { {{}}}
                function replaceTxt() {
                  node.textContent = txt.replace(reg, (matched, placeholder) = > {   
                      console.log(placeholder);   // Hexo, paly, info.height...
                      new Watcher(vm, placeholder, replaceTxt);   // Listen for changes to match the replacement content
                      return placeholder.split('. ').reduce((val, key) = > {
                          return val[key]; 
                      }, vm);
                  });
              };
              replaceTxt();
            }
            // If there are children, continue to recurse replace
            if(node.childNodes && node.childNodes.length) { replace(node); }}); } replace(fragment);// Replace the content

    vm.$el.appendChild(fragment);   // Put the document fragment into the EL
}
Copy the code

The data is now ready to compile, but the data we manually modified has not changed on the page, and the final response is still missing.

So let’s take a look at how we do that, and we’re actually using a very common design pattern here, publish and subscribe, right

Release subscription

Publish subscriptions rely on array relationships, subscription is to put functions, publish is to let the functions in the array execute

Subscribe and publish such as [fn1, fn2, fn3]
function Dep() {
    // An array (event pool for functions)
    this.subs = [];
}
Dep.prototype = {
    addSub(sub) {
        this.subs.push(sub);
    },
    notify() {
        // The bound method has an update method
        this.subs.forEach(sub= >sub.update()); }};// Listen to the function
// All instances created using the Watcher class have update methods
function Watcher(fn) {
    this.fn = fn;   // Place fn on the instance
}
Watcher.prototype.update = function() {
    this.fn();
};

let watcher = new Watcher(() = > console.log(111));
let dep = new Dep();
dep.addSub(watcher);    // Put watcher in an array,watcher has its own update method, => [watcher]
dep.addSub(watcher);
dep.notify();   //  111, 111
Copy the code

Data update view

  • Now we need to subscribe to an event that needs to be refreshed when the data changes, and this needs to be handled in the replace logic
  • Subscribe to the data with New Watcher and change the content as the data changes
function replace(frag) {
    / / to omit...
    // Replace the logic
    node.textContent = txt.replace(reg, val).trim();
    // Listen for changes
    // Add two more parameters to Watcher to take the new value (newVal) to the callback function
+   new Watcher(vm, RegExp. $1,newVal= >{ node.textContent = txt.replace(reg, newVal).trim(); +}); }// Override the Watcher constructor
function Watcher(vm, exp, fn) {
    this.fn = fn;
+   this.vm = vm;
+   this.exp = exp;
    // Add an event
    // Here we define an attribute
+   Dep.target = this;
+   let arr = exp.split('. ');
+   let val = vm;
+   arr.forEach(key= > {    / / value
+      val = val[key];     // When this.a.b is obtained, the default get method is called+}); + Dep.target =null;
}
Copy the code

The get method is automatically called when the value is fetched, so let’s look for the get method in the data hijacking area

function Observe(data) {+let dep = new Dep();
    / / to omit...
    Object.defineProperty(data, key, {
        get() {
+           Dep.target && dep.addSub(Dep.target);   // Add watcher to the subscription event [watcher]
            return val;
        },
        set(newVal) {
            if (val === newVal) {
                return;
            }
            val = newVal;
            observe(newVal);
+           dep.notify();   // Let all of watcher's update methods execute}})}Copy the code

Update watcher update (dep. Notify); update watcher update (dep. Notify); update watcher update (deP. Notify)

Watcher.prototype.update = function() {
    // Notify when the value has changed
    // get the new value from vm exp
+   let arr = this.exp.split('. ');
+   let val = this.vm;
+   arr.forEach(key= > {
+       val = val[key];   // Get the new value+});this.fn(val);   // Replace {{}} with each new value
};
Copy the code

Two-way data binding

   / / HTML structure
    <input v-model="c" type="text">

    // Data section
    data: {
        a: {
            b: 1
        },
        c: 2
    }

    function replace(frag) {
        / / to omit...
+       if (node.nodeType === 1) {  // Element node
            let nodeAttr = node.attributes; // Get all the attributes in the DOM, an array of classes
            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 to 2
                }
                // Listen for changes
                new Watcher(vm, exp, function(newVal) {
                    node.value = newVal;   // When watcher triggers, it automatically puts the content in the input box
                });
                node.addEventListener('input'.e= > {
                    let newVal = e.target.value;
                    // assign a new value to this.c
                    // Set is called, and notify is called. Watcher update is calledvm[exp] = newVal; }); }); +}if(node.childNodes && node.childNodes.length) { replace(node); }}Copy the code

A basic MVVM is done. This extends to evaluating properties and hook functions

Computed (Computed attributes) && Mounted (hook functions)

/ / HTML structure<p>The sum is {{sum}}</p>data: { a: 1, b: 9 }, computed: { sum() { return this.a + this.b; {}}}, it (), mounted () {setTimeout (() = > {the console. The log (' all things are done '); }, 1000); }Copy the code
    function Mvvm(options = {}) {
        // Initialize computed and point this to the instance
+       initComputed.call(this);
        / / compile
        new Compile(options.el, this);
        // Execute the mounted hook function after everything is done
+       options.mounted.call(this); // This implements the mounted hook function
    }

    function initComputed() {
        let vm = this;
        let computed = this.$options.computed;  // Get computed attributes from options {sum: ƒ, noop: ƒ}
        Keys can be converted to an array using object. keys
        Object.keys(computed).forEach(key= > {  // key is sum,noop
            Object.defineProperty(vm, key, {
                // Determine whether a key in computed is an object or a function
                // If it is a function, the get method is called directly
                // If it is an object, manually call the get method
                Sum () {return this.a + this.b; }, they get the values of a and b and call the get method
                // No need for new Watcher to listen for changes
                get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
                set(){}}); }); }Copy the code

conclusion

The MVVM you implement yourself contains the following

  • Data hijacking via get and set of Object.defineProperty
  • Data is proxyed to this by traversing the data
  • Compile the data with {{}}
  • Data is synchronized with the view through the publish-subscribe mode