Writing in the front

The MVVM framework takes Web development to a new level. In the old days, business code data manipulation was mixed with DOM manipulation, requiring both the correctness of the data manipulation and the efficiency of the DOM manipulation. When business logic is complex, it’s easy to say how complex it is.

With a framework like this, we don’t have to worry about DOM details anymore. Focusing on maintaining business logic, data structures, arguably takes a lot of pressure off developers.

Let’s get down to business. Let’s implement a simple responsive framework to analyze the principle point by plane.

Framework implementations

Vue. js adopts data hijacking combined with publish/subscribe mode. It hijacks the setter and getter of each attribute through Object.defineProperty(), releases messages to subscribers when data changes, and triggers corresponding listening functions

Framework implementation mainly includes the following three aspects:

  1. Data hijacking (proxy)
  2. Publish/subscribe
  3. Template compilation

The first two points are responsive, and the third point parses the custom DOM structure, parses the custom instructions, and adds the subscription to realize the response

Data proxy/hijacking

Two main methods are used:

  1. Vue2 use Object. DefineProperty ()
  2. Vue3 uses a Proxy

The Observer class

First we create an Observer class that defines accessor properties for the data. The main function is to define accessor properties for each property of the object passed in, which is the first step towards being reactive.

Class Observer {constructor (data) {this.observe(data)} class Observer {constructor (data) {this.observe(data)} More precise and the Object. The prototype. ToString. Call ()if(typeof data === 'object'&& typeof data ! == null) {// Iterate over data object attributes and call defineReactivefor(let key inData) {this.definereactive (data, key, data[key])}}} // defineReactive simply converts the data attribute to the accessor attribute defineReactive(data, Const dep = new dep () // Define accessor property, Object.defineproperty (Data, key, {enumerable:true,
            configurable: true.get() {// Collect dependencies here dep.target && dep.add(dep.target)returnVal // uses the closure},set: newVal => {// Checks whether the data is updatedif(val ! == newVal){// Observe the new value, This.observe(newVal) // Update the closure val = newVal // Notify all Watcher dep.notify() // notify the subscriber of the update}}})}}Copy the code

Due to language limitations, we can only listen on the general properties of a data object. However, objects are different from normal types. Data of reference type is stored in heap memory, which is more complex and cannot be completed directly with equal sign. This is why Vue may not respond when modifying an attribute of the object, so it is necessary to use Vue. Alternatively, a new object can be created to override the original object.

Publish/subscribe to events

At this point, we have implemented the Observer class, and the accessor properties are ready. But the basics aren’t enough. We still need some scheduling — Dep classes and Watcher classes.

Dep, which stands for Dependency, is a bridge for collecting dependencies and connecting accessor properties to Watcher. Whenever a setter finds a data update, it notifies the deP instance of the property to notify all watcher names to update the data. This is the response, which was dep.notify().

Dep class

/** * class Dep {/** * class Dep {constructorWatcher = []} add (sub) {this.watcher. Push (sub)}notify() {// Inform all collected observers that the data is updated! ForEach (watcher => watcher.update())}}Copy the code

The Dep class is simple, stores Watcher and then calls update() when appropriate

Watcher class

/** ** @param {*} vm expr {param {*} vm expr Data. The style. The color, Data. name * @param {*}} class Watcher {constructor (vm, expr, {fn) enclosing the callback = fn this. Vm = vm enclosing expr = exp / / similar to the data. The name of the string / / added to the subscription - * * * * * * * * the following three lines highlighted * * * * * * * * * -- - Dep.target = this this.fire() dep.target = null Add (dep.target) // to associate watcher with an Observer. Does the function name itself matterfire() {// Use reduce to peel off attribute paths (such as data.address.city) layer by layerreturn this.expr.split('. ').reduce((result, key) =>{
            return result[key]
        }, this.vm.$data)}updateConst val = this.fire() this.callback(val)}}Copy the code

Let’s focus on these three lines:

Dep.target = this
this.fire()
Dep.target = null
Copy the code

First of all, we set accessors for data attributes, so that the corresponding logic can be executed when the data changes. Why do we need to use publish/subscribe mode to package a layer?

Due to the limitation of modularization, we cannot pass the corresponding callback function into the getter and setter of data property, because we do not know where the content in data will be used in the web page. It cannot be written in advance, but can only be added dynamically.

So you need an instance of deP to collect the dependencies and dep.notify() when appropriate (in setters).

How do you collect dependencies?

Dep.target && dep.add(Dep.target)
Copy the code

Combined with the above three lines of code:

Dep.target = this // Adds the current instance to dep.target, exposing the external this.fire() // fires the getter to add dep.target to the Dep instance, Collect the dependency dep. target = null // to prevent possible accidents, such as accidentally adding the same WatcherCopy the code

That’s about it. Here’s how the framework works

Template compilation

The process of compiling a Vue template is to compile the content of the template into a render function. The RENDER function generates the DOM structure. The argument passed to the Render function is actually the virtual DOM. Don’t think of the virtual DOM as something that’s just a JSON object. Each object has three main properties:

  1. Tag: The name of the tag, such as div
  2. Properties: DOM properties such as style, class, and ID
  3. Children: an array containing children that have the same structure as the parent.

The resulting structure is actually a tree, so it is also called a DOM tree. After this processing, the content in the template becomes the virtual DOM format and is passed to the render function Render (Tag, properties, children).

But it’s not that complicated here, just walking through the DOM structure, demonstrating the use of the Watcher class and the Dep class

Vue class

Constructor (options) {// Bind the root element this.$el =  document.querySelector(options.el)
        this.$dataNew Observer(this) = options.data // Set accessor property for data using Observer.$dataCall object. assign(this, options.methods); // Assign (this, options.methods); // Assign (this, options.methods); Reg = new RegExp(/\{\{(.+?)) \} \} /,'g') // Cache DOM nodes. Why use document fragments, as discussed laterlet fragment = this.createFragment(this.$el// Start compiling this.compile(fragment) // Replace the compiled text node with this.$el.appendChild(fragment) // Delegate data to this, most of the time we use this call directlyfor (let key in this.$data) {
            Object.defineProperty(this, key, {
                enumerable: true.get () {
                    return this.$data[key]
                },
                set (newVal) {
                    this.$data[key] = newVal}})}} compile (fragment) {// Compile (fragment) {...fragment.childNodes]. ForEach (node => {// Recursively iterate if element nodes are presentif (node.nodeType === 1) {
                this.compileElement(node)
                this.compile(node)
            } elseThis.piletext (node)}})} fetch element (node) {this.piletext (node)}})} fetch element (node) {this.piletext (node)}} [...node.attributes]. ForEach (attr => {// expr is short for expression // name is the attribute name, value is the attribute value, // Rename value to expr by destructing assignmentlet{name, value: expr} = attr // check whether the instruction is v-bind, V-model, V-onif (name.startsWith(The '@') || name.startsWith(':') || name.startsWith('v-'// If the attribute contains these characters, it is a custom attribute. This.pileinstruction (node, name, expr) // Delete node.removeAttribute(name)}})} * @param node DOM node * @param name property name * @param expr expression */ compileInstruction (node, name, compileInstruction, Expr) {// To match on in v-ON: (for example)letreg = new RegExp(/v-(.+?) \ /) :if(reg.test(name)) {// For example v-on:click/v-bind:class //typeIs on orbind
            let [, type] = name.match(reg) // Get match content // Get event name or attribute name, such as click or classlet prop = name.substr(name.indexOf(':'// Call the algorithm encapsulated by the strategy pattern. The related code is in the following Instructions[type](this, node, prop, expr)
        } else if (name.startsWith('v-')) {
            // typeFor the instruction oflet [, type] = name.split(The '-')
            Instructions[type](this, node, expr)
        } else{// prop is the corresponding event type or propertylet [type. prop] = nameif (type= = =The '@') {
                Instructions['on'](this, node, prop.join(' '), expr)
            } else if (type= = =':') {
                Instructions['bind'](this, node, prop.join(' '), expr)}}} // compileText (node) {const nodeValue = node.nodeValue // match {{}}if(this.reg.test(nodeValue)) {this.updatetext (node, nodeValue)}} updateText(node, nodeValue) OriginText) {node.nodeValue = originText.replace(this.reg, (match, content) => { My name is {{name}}, and the matching content is'name'// this expression is used to getData from the getData function, New Watcher(this, content.trim(), () => {// ******* note here ******* // this.getContentValue returns the latest value // This Node.nodevalue = this.getContentValue(originText)}) // Node.nodeValue = this.getContentValue(originText)} Get the corresponding value based on the expression content and replace itreturnThis.getdata (content.trim())})} You can see that the logic is basically the same here, because if you recursively call updateText, not only will the subscription be added repeatedly, but the originText value will change, {{name}} // This will not match and update the value of the expression // Only the text can be replaced with a new value // If recursively called, this value will become like: GetContentValue (originText) {getContentValue(originText) {getContentValue(originText) {getContentValue(originText) {getContentValue(originText) {return originText.replace(this.reg, (match, content) => {
            returnGetData (expr) {this.getData(content.trim())}} getData(expr)return expr.split('. ').reduce((data, key) => {
            return data[key]
        }, this.$data} // Set the corresponding value in data according to the expressionsetData (expr, value) {
        expr.split('. ').reduce((data, current, index, arr) => {
            if (index === arr.length - 1) {
                return data[current] = value
            }
            return data[current]
        }, this.$data} // Create a document fragment createFragment (node) {let fragment = document.createDocumentFragment()
        doAdd fragment.appendChild(node.firstChild)}while (node.firstChild)
        return fragment
    }
}
Copy the code

Instructions object

Encapsulates common instructions:

Model (VM, node, expr) {// input => VM.$data
        node.addEventListener('input', event => {
            vm.setData(expr, event.target.value)
        })
        // vm.$data => input
        node.value = vm.getData(expr)
    },
    // v-on / The '@'On (VM, node, eventType, handler) {// Handler binds this, Otherwise this points to event.target node.addeventListener (eventType, vm[handler].bind(vm))}, // v-bind /':'Binding propertiesbind (vm, node, prop, expr) {
        switch (prop) {
            // v-bind:style
            case 'style': New Watcher(vm, expr, value => {// CSS attribute Object.assign(node.style, value)}) object.assign (node.style, value) vm.getData(expr))break
            // v-bind:class
            case 'class': // If you are interested, you can explore how to respond to an array. // New Watcher(vm, expr, list => {node.classList = list.join(vm, expr, list => {node.classList = list.join(' ')
                })
                node.classList = vm.getData(expr).join(' ')
                break
            default:
                throw new Error('can\'t resolve the value ' + expr) } } }Copy the code

The sample

First, the DOM structure:

<style>
    .red {
        color: red
    }
</style>
<div id="app">
    <div class="header"
         :style="style"
         v-on:click="sayHello"> name: {{name}} < / div > < div > < div > provinces: {{address. Province}}, downtown: {{address. City}} < / div > < div class ="myClass"> City: {{address.city}}</div> </div> <div :class="myClass"> I call: {{name}}</div> <inputtype="text" id="input" v-model="name">
    <button @click="onClick"</button> </div>Copy the code

new Vue({
    el: '#app',
    data: {
        style: {
            color: 'green',
            fontSize: '10px'
        },
        myClass: ['red'],
        name: 'Ming',
        address: {
            province: 'Shaanxi',
            city: 'Hanzhong'
        }
    },
    methods: {
        onClick (e) {
            this.style = {
                color: 'red',
                fontSize: '30px'
            }
        },
        sayHello (e) {
            console.log('Hello, my name is.', this.name)
        }
    }
})
Copy the code

Some of the problems

1. Disadvantages of Object.defineProperty

Responsiveness is implemented using Object.defineProperty in Vue2, but this approach has three disadvantages:

  1. For large objects that need to recurse all at once, it is inefficient
  2. There’s no way to listen for new or deleted properties, because property hijacking is only done once during initialization, which is why, even if it’s null, the properties that need to be used have to be written in data, right
  3. You can’t listen on an array natively, you need to wrap the array

2. How to use Object.defineProperty to listen on an array?

Listening to the array requires some extra processing, replacing the prototype

Create (array.prototype) const methods = [array.prototype) const methods = ['push'.'pop'.'splice'.'shift'.'unshift'ForEach (method => {arrProto[method] =function(... args) { console.log('Update the view! 'Prototype [method].apply(this, args)}}) defineReactive (data, key, Val) {// Recursively observe the child property this.observe(val) // listen on the array, replace the new prototypeif(Array. IsArray (value)) {value. __proto__ = arrProto}......}Copy the code

3. Vue3 uses Proxy to achieve responsiveness

/** * Vue3 uses Proxy to implement reactive */functionProxyObserve (target = {}) {// Return if not an object or arrayif( Object.prototype.toString.call(target) ! = ='[object Object]' &&
        !Array.isArray(target)
    ) {
        return target
    }

    return new Proxy(target, {
        get (target, key, receiver) {
            const result = Reflect.get(target, key, receiver)
            if (Reflect.ownKeys(target).includes(key)) {
                console.log('get', key) // listen only on objects, not on prototypes}returnProxyObserve (result)},set (target, key, value, receiver) {
            if (target[key] === value) {
                return true} console.log('set', key, value)
            return Reflect.set(target, key, value, receiver)
        },
        deleteProperty (target, key) {
            console.log('delete', key)
            return Reflect.deleteProperty(target, key)
        }
    })
}
Copy the code

4. Advantages and disadvantages of Proxy

The advantages correspond to Object.defineProperty:

  1. Proxy natively supports listening on arrays, which is very convenient
  2. The Proxy can listen to add and delete attributes
  3. In the case of large objects, it does not recurse all at once and only proxies the first level by default. Further listening is done when an object property or array property is accessed, which is more efficient

The main disadvantages are that the Proxy is slightly less compatible and cannot be remedied with polyfill.

5. About Reflect objects

The Reflect object is briefly mentioned here. There are a number of utility methods in JavaScript that are defined in Object objects, but in reality, these methods have nothing to do with the Object data type. For example, Object.definProperty, Object.hanownProperty, etc.

Object takes on a lot of responsibilities that it shouldn’t, and Reflect comes along to take those methods out of Object and make Object just a data type. And for one-to-one correspondence with the Proxy. To sum up, there are the following points:

  1. Put some of the methods on Object that are obviously internal to the language on the Reflect Object (they exist at the same time, but will be removed later).
  2. Modify the return results of some Object methods to make them more reasonable. For example,Object.definePropertyUnable to define attributes is a throw error, whileRefelct.definePropertyFalse is returned.
  3. Make Object operations functional (functional programming). Some operations are imperative, for example:Delete obj[name]. The Reflect object makes its vi a function, as in:Reflect.has(obj, name) and reflect. deleteProperty(obj, name)
  4. Reflect’s methods correspond to Proxy methods, which can be found on Reflect as long as they are Proxy object methods. This makes it easy for the Proxy to implement the default behavior of the object using the Reflect method.

6. Which core modules are needed to implement an MVVM? What is the relationship between the core modules?

  1. Model: Data
  2. View: The structure, layout, and appearance (UI) that the user sees on the screen.
  3. Viewmodel: Connect views to data, convert views to data, or convert data to views. How to convert? View-to-data needs to be listened on via DOM events, and data to view needs to be observed. The View model layer is responsible for scheduling these processes.

    The relationship is as follows:

    Compare Vue’s official image:

    Is it exactly the same. The focus is on how to add subscriptions using the Dep class and the Watcher class. If you don’t understand, refer to the defineReactive function above

7. Why use document fragments to manipulate the DOM?

Everyone knows that DOM manipulation is expensive and performance-intensive, but they don’t know why. This is mainly because it triggers repaint and reflow in the browser.

Redraw is when part of the element style changes and needs to be redrawn. Rearrangement is when element positions, sizes, and so on change and the browser recalculates the render tree. Causes some or all of the render tree to change. After the render tree is rebuilt, the browser redraws the affected elements on the page.

These operations require a lot of computation to complete, so they eat performance. But with document fragmentation, putting DOM elements into memory is just an operation on the object, not the browser. If there is a performance impact, it is only inserted into the documentation at the end. In the same way, you can set display to None as long as the browser displays the same interface, which is usually not the case…

reference

  1. An Introduction to ES6 Standards by Yifeng Ruan — Reflect
  2. 【 Nuggets 】 Write a complete set of MVVM principles based on Vue
  3. Vue2.1.7 source code learning
  4. 50 lines of MVVM, feel the art of closures