Manually simulate a Min-VUE

By manually implementing a Min-VUE, we can master the principle of data responsiveness in VUE.

Ready to HTML

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Min-vue</title>
</head>
<body>
    <div id="app">
        <h1>Difference expression</h1>
        <h3>{{msg}}</h3>
        <h3>{{count}}</h3>
        <h1>v-text</h1>
        <div v-text="msg"></div>
        <h1>v-model</h1>
        <input type="text" v-model="msg">
        <input type="text" v-model="count">
    </div>
    <script src="./js/vue.js"></script>
    <script>
        const vue = new Vue({
            el: '#app'.data: {
                msg: 'hello vue'.count: 10}})</script>
</body>
</html>
Copy the code

Writing HTML makes it easier to verify that each time we write it, we’re doing it right.

vue

In index. HTML we have introduced the js file vue.js in which we will implement vUE initialization

First of all, vue is a class. When instantiated, the class receives an object parameter, which contains the EL attribute and data

class Vue {
  constructor(options){}}Copy the code

1. Save the option data

We need to save the incoming data for future use.

class Vue {
  constructor(options) {
    // 1. Save the data of the option through properties
    this.$options = options || {}
    this.$data = options.data || {}
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
  }
}
Copy the code

Options and this.options and this.options and this.data are both handled with exceptions. If there is no value, an empty object is assigned

This. option caches data for the current option, this.option caches data for the current option, this.data caches data for the current option, and this.$el is the current DOM instance.

If el is a string in the current option’s data, we will manually retrieve the DOM object.

2. Convert data to getter/setter

We cache data on the current VUE instance, but we don’t convert it to getters/setters.

class Vue {
  constructor(options) {
    // 1. Save the data of the option through properties
    this.$options = options || {}
    this.$data = options.data || {}
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
    // 2. Convert data members into getters/setters and inject them into vue instances
    this._proxyData(this.$data)
  }
  // Proxy data
  _proxyData(data) {
    Object.keys(data).forEach(key= > {
      Object.defineProperty(this, key, {
        configurable: true.enumerable: true.get() {
          return data[key]
        },
        set(newValue) {
          if(newValue === data[key]) {
            return
          }
          data[key] = newValue
        }
      })
    })
  }
}
Copy the code

Methods that begin with _ represent private methods in the current class.

In the vue class, we define the _proxyData private method, which converts the key property in the data into an array using object. keys and then traverses it with forEach. Object defineProperty is used in traversal to convert an Object from a data property to an accessor property, that is, getter/setter. The important thing to note here is that we need to inject the getter/setter into the vue instance, and this of the Vue type is the current instance, so the first argument to Object.defineProperty is this.

validation

Run the code to see if the Vue instance caches option data and converts data into getters/setters to be injected into the Vue instance.

3. Monitor data change Observer

In the previous step, we converted the members in data into getters/setters and injected them into the Vue instance. But data itself does not convert to getters/setters

We will create a new Observer class that converts all members of data into getters/setters to listen for data changes.

class Observer {
    constructor(data) {
        this.walk(data)
    }
    walk(data) {
        if(data && typeof data === 'object') {
            Object.keys(data).forEach(key= > {
                this.defineReactive(data, key, data[key])
            })
        }
    }
    defineReactive(data, key, value) {
        // Handle nested objects recursively
        this.walk(value)
        Object.defineProperty(data, key, {
            configurable: true.enumerable: true.get() {
                return value
            },
            set(newValue) {
                if(newValue === value) {
                    return
                }
                value = newValue
            }
        })
    }
}
Copy the code

The Observer class defines the walk method, which handles exceptions for arguments and only calls defineReactive if the arguments are objecJT and not null.

DefineReactive is responsible for converting data into getters/setters.

test

HTML is introduced into the observer
<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Min-vue</title>
</head>
<body>
    <div id="app">
        <h1>Difference expression</h1>
        <h3>{{msg}}</h3>
        <h3>{{count}}</h3>
        <h1>v-text</h1>
        <div v-text="msg"></div>
        <h1>v-model</h1>
        <input type="text" v-model="msg">
        <input type="text" v-model="count">
    </div>
    <script src="./js/observer.js"></script>
    <script src="./js/vue.js"></script>
    <script>
        const vue = new Vue({
            el: '#app'.data: {
                msg: 'hello vue'.count: 10}})</script>
</body>
</html>
Copy the code
Call an observer in vue
class Vue {
	constructor(options) {
		// 1. Save the data of the option through properties
		this.$options = options || {}; //
		this.$data = options.data || {};
		this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
		// 2. Convert data members into getters/setters and inject them into vue instances
		this._proxyData(this.$data);
		// 3. Call an Observer to listen for data changes
		new Observer(this.$data)
	}
	// Proxy data
	_proxyData(data) {
		Object.keys(data).forEach((key) = > {
			Object.defineProperty(this, key, {
				configurable: true.enumerable: true.get() {
					return data[key];
				},
				set(newValue) {
					if (newValue === data[key]) {
						return; } data[key] = newValue; }}); }); }}Copy the code
Check the actual running result

To reassure

Why is value not obtained by data[key]

In the _proxyy method of Vue, the get method of Object.defineProperty obtains attributes via data[key], whereas here values need to be passed in directly.

This is because the first argument to object.defineProperty in _proxyData, this, will inject the getter/setter into the vue instance, DefineReactive Object. DefineProperty’s first argument is data itself, which converts itself from a data attribute to an access attribute. If the get method gets the attribute via data[key], then the get method is looped through until an exception message is thrown: observer.js:21 Uncaught RangeError: Maximum call stack size exceeded

The value passed in will not be cleared

It is not cleared because the GET method keeps a reference to the value, generating closures that delay the scope of the value.

To optimize the Observer

Vue initializes by iterating over properties in data and converting them to getters/setters. If we change a property from a raw data type to an object after initialization, the object is a data property, not an access property.

We need to call the walk method again in the set method to solve this problem

class Observer {
    constructor(data) {
        this.walk(data)
    }
    walk(data) {
        if(data && typeof data === 'object') {
            Object.keys(data).forEach(key= > {
                this.defineReactive(data, key, data[key])
            })
        }
    }
    defineReactive(data, key, value) {
        // Handle nested objects recursively
        this.walk(value)
        const _this = this
        Object.defineProperty(data, key, {
            configurable: true.enumerable: true.get() {
                return value
            },
            set(newValue) {
                if(newValue === value) {
                    return
                }
                value = newValue
              // Add a new object
                _this.walk(value)
            }
        })
    }
}
Copy the code

validation

4. Compile the templatecompiler

We cached the data and converted it into a getter/setter. Now we’re going to deal with dom.

Create a new Compiler class

class Compiler {
    constructor(vm) {
        this.el = vm.$el
        this.vm = vm
        this.compile(this.el)
    }
    // Compile templates to handle text nodes and element nodes
    compile(el){}// Compile element node, process instruction
    compileElement(node){}// Compile text nodes to handle interpolation expressions
    compileText(node){}// Determine if element attributes are directives
    isDirective(attrName) {
        return attrName.startsWith('v-')}// Determine whether the node is a text node
    isTextNode(node) {
        return node.nodeType === 3
    }
    // Determine if the node is an element node
    isElementNode(node) {
        return node.nodeType === 1}}Copy the code

In Compiler, VM is the current VUE instance, and EL is the DOM element corresponding to the current VUE instance.

Compile – Compiles templates, processing text nodes and element nodes

class Compiler {
    constructor(vm) {
        this.el = vm.$el
        this.vm = vm
        this.compile(this.el)
    }
    // Compile templates to handle text nodes and element nodes
    compile(el) {
      	const childNodes = el.childNodes
        Array.from(childNodes).forEach(node= > {
            // Process file nodes
            if(this.isTextNode(node)) {
                this.compileText(node)
            } else if (this.isElementNode(node)) {
                // Process element nodes
                this.compileElement(node)
            }

            // Process the deep nodes
            if(node.childNodes && node.childNodes.length) {
                this.compile(node)
            }
        })
    }
		...
}
Copy the code

The child node is obtained by el.childNoedes, because childNodes is a pseudo-class Array, all needs to be converted to an Array by array. from. If it is a text node, call the compileText method. If it is an element node, call the compileElement method. Recursively calls itself if there are deep nodes.

CompileText – compiles text nodes and handles difference expressions

class Compiler {
    constructor(vm) {
        this.el = vm.$el
        this.vm = vm
        this.compile(this.el)
    }
  	...
    // Compile text nodes to handle interpolation expressions
    compileText(node) {
        // {{ msg }}
        const reg = / {\ {(. +?) \} \} /
        const value = node.textContent
        if(reg.test(value)) {
          	// Clear Spaces
            const key = RegExp.$1.trim()
            node.textContent = value.replace(reg, this.vm[key])
        }
    }
		...
}
Copy the code

Check whether it is an interpolation expression, if so, then replace it

CompileElement – compileElement nodes and process instructions

class Compiler {
    constructor(vm) {
        this.el = vm.$el
        this.vm = vm
        this.compile(this.el)
    }
  	...
    // Compile element node, process instruction
    compileElement(node) {
        // console.log(node.attributes)
        Array.from(node.attributes).forEach(attr= > {
            // Check whether it is an instruction
            let attrName = attr.name
            if(this.isDirective(attrName)) {
                // v-text --> text
                attrName = attrName.substr(2)
                const key = attr.value
            }
        })
    }
		...
}
Copy the code

Node to compileElement is an element node, which is a pseudo-array that we need to convert to an array before we can iterate.

For each instruction, we define a method to handle it, so as to keep the function as simple as possible. However, there are a lot of instructions to handle. If you use the enumeration method to fetch each one of these elements, you’ll need to change two things when you add them later. Let’s now optimize the code so that it only needs one change to function.

class Compiler {
    constructor(vm) {
        this.el = vm.$el
        this.vm = vm
        this.compile(this.el)
    }
  	...
    // Compile element node, process instruction
    compileElement(node) {
        // console.log(node.attributes)
        Array.from(node.attributes).forEach(attr= > {
            // Check whether it is an instruction
            let attrName = attr.name
            if(this.isDirective(attrName)) {
                // v-text --> text
                attrName = attrName.substr(2)
                const key = attr.value
                this.updader(node, key, attrName)
            }
        })
    }
    / * * * /
    updader(node, key, attrName) {
        const updateFn = this[attrName + 'Updater']
        updateFn && updateFn(node, this.vm[key])
    }
    // Process the V-text instruction
    textUpdater(node, value) {
        node.textContent = value
    }
    // Process the V-model directive
    modelUpdater(node, value) {
        node.value = value
    }
		...
}
Copy the code

The updader method takes three arguments,

The first is that the argument is the current section;

The second parameter is the key in the data data corresponding to the current instruction;

The third parameter is the instruction name after deleting v-, for example: V-text => text, v-model => model.

In the updader method, attrName + ‘Updater’ concatenates the processing instruction method we’ve defined. For example, textUpdater equals text + ‘Updater’.

validation

HTML into Comlier
<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Min-vue</title>
</head>
<body>
    <div id="app">
        <h1>Difference expression</h1>
        <h3>{{msg}}</h3>
        <h3>{{count}}</h3>
        <h1>v-text</h1>
        <div v-text="msg"></div>
        <h1>v-model</h1>
        <input type="text" v-model="msg">
        <input type="text" v-model="count">
    </div>
    <script src="./js/compiler.js"></script>
    <script src="./js/observer.js"></script>
    <script src="./js/vue.js"></script>
    <script>
        const vue = new Vue({
            el: '#app'.data: {
                msg: 'hello vue'.count: 10}})</script>
</body>
</html>
Copy the code
Comlier is called in vue
class Vue {
    constructor(options) {
        // 1. Save the data of the option through properties
        this.$options = options || {}; //
        this.$data = options.data || {};
        this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
        // 2. Convert data members into getters/setters and inject them into vue instances
        this._proxyData(this.$data);
        // 3. Call an Observer to listen for data changes
        new Observer(this.$data)
        // 4. Invoke the Compiler object to parse instructions and differential expressions
        new Compiler(this)}// Proxy data
    _proxyData(data) {
        Object.keys(data).forEach((key) = > {
            Object.defineProperty(this, key, {
                configurable: true.enumerable: true.get() {
                    return data[key];
                },
                set(newValue) {
                    if (newValue === data[key]) {
                            return; } data[key] = newValue; }}); }); }}Copy the code
View comlier results

5. Establish a communication bridge between observer and COMLIER

Now the data in the data is translated into getters/setters, and the data in the data is bound to HTML using the Compiler. The next step is to implement automatic HTML modification when the data in the data is modified.

Dep collects dependencies
class Dep {
    constructor() {
       // Store dependencies
        this.subs = []
    }
  	// Add dependencies, convention: Each dependency has an update method
    addSub(sub) {
        if(sub && sub.update) {
            this.subs.push(sub)
        }
    }
  	// Trigger notification to call the update method in each dependency
    notify() {
        this.subs.forEach(sub= > {
            sub.update()
        })
    }
}
Copy the code

In the Dep class, it is important to note that when adding a dependency, the dependency needs to have an update method. When the notification is triggered, the data is updated by calling the UPDATE method.

Watcher updates the data
class Watcher {
    constructor(vm, key, cb) {
        this.vm = vm
        this.key = key
        this.cb = cb
      	this.oldValue = vm[key]
    }
    update() {
      const newValue = this.vm[this.key]
      if(this.oldValue === newValue) {
        return
      }
      this.cb(newValue)
    }
}
Copy the code

Watch receives three arguments:

The first is the current VUE instance

The second parameter is the key in data, such as MSG and count in data

The third is a callback function that updates the view.

The update method does two things:

  1. Comparing the old data with the new data, the old data is obtained when the Watch is instantiated. The new data is retrieved when the UPDATE is called
  2. If the old and new data are not equal, the callback function is called and the new data is passed in
Integrate Dep and Watch

Now that’s the hardest part.

We use Obsever to convert data to getters/setters. When we get a value of data, we call get. When we update a value in data, we call set.

class Observer {
    constructor(data) {
        this.walk(data)
    }
    walk(data) {
        if(data && typeof data === 'object') {
            Object.keys(data).forEach(key= > {
                this.defineReactive(data, key, data[key])
            })
        }
    }
    defineReactive(data, key, value) {
        // Handle nested objects recursively
        this.walk(value)
        const _this = this / / this cache
        const dep = new Dep() // Instance collector
        Object.defineProperty(data, key, {
            configurable: true.enumerable: true.get() {
              	// Collect dependencies
              	// dep. target Specifies the Watcher instance for the current key
                Dep.target && dep.addSub(Dep.target)
                return value
            },
            set(newValue) {
                if(newValue === value) {
                    return
                }
                value = newValue
                _this.walk(value)
              	// Notifications depend on updates
                dep.notify()
            }
        })
    }
}
Copy the code

What does the dep. target stand for? Target is a static property in the Dep class.

When was it assigned? The next step is to implement the target assignment to the Dep class

Modify the Watcher
class Watcher {
    constructor(vm, key, cb, ) {
        this.vm = vm
        this.key = key
        this.cb = cb
        // Record the watcher object to the Dep static attribute target
        Dep.target = this
        // Trigger the get method and call addSub in the get method
        this.oldValue = vm[key]
        Dep.target = null
    }
    update() {
        const newValue = this.vm[this.key]
        if(this.oldValue === newValue) {
            return
        }
        this.cb(newValue)
    }
}
Copy the code
Modify comoiler
class Compiler {
    constructor(vm) {
        this.el = vm.$el
        this.vm = vm
        this.compile(this.el)
    }
		...
    // Compile element node, process instruction
    compileElement(node) {
        // console.log(node.attributes)
        Array.from(node.attributes).forEach(attr= > {
            // Check whether it is an instruction
            let attrName = attr.name
            if(this.isDirective(attrName)) {
                // v-text --> text
                attrName = attrName.substr(2)
                const key = attr.value
                // v-text calls the textUpdater method
                // v-vModel calls modelUpdater
                this.updader(node, key, attrName)
            }
        })
    }
  	// General instruction processing method, easy to expand
    updader(node, key, attrName) {
        const updateFn = this[attrName + 'Updater']
        updateFn && updateFn.call(this, node, this.vm[key], key)
    }
    // Process the V-text instruction
    textUpdater(node, value, key) {
        node.textContent = value
        new Watcher(this.vm, key, (newValue) = > {
            node.textContent = newValue
        })
    }
    // v-model
    modelUpdater(node, value, key) {
        node.value = value
        new Watcher(this.vm, key, (newValue) = > {
            node.value = newValue
        })
    }


    // Compile text nodes to handle interpolation expressions
    compileText(node) {
        // {{ msg }}
        const reg = / {\ {(. +?) \} \} /
        const value = node.textContent
        if(reg.test(value)) {
            const key = RegExp.$1.trim()
            node.textContent = value.replace(reg, this.vm[key])

            // Create a Watcher object to update the view when data changes
            new Watcher(this.vm, key, (newValue) = > {
                node.textContent = newValue
            })
        }
    }
  ...
}
Copy the code

When a compiled template handles a text node, it calls the method compileText and instantiates a Watcher. The value of the data that is accessed through the key of the text, and the getter method is triggered when it is accessed. The getter calls dep.addSub() to collect the Watcher instance, which is the dependency, and then the code goes back to the wather stack to clear the target property of the DEP class because the dependencies have been collected and the communication channel has been established. This is done to prevent memory leaks. This is the process of dependency collection.

When the data update changes, the setter method is triggered, and dep.notify is called, which iterates through the subs array and fires water’s update method. Each watcher holds the update view return function. When the update method is triggered, The view is updated as well. This is the process of updating dependencies

When traversing the template to process instructions, it is necessary to call updader method first, find the corresponding method of the instruction, and then execute the same logic as compiler.

6. Two-way data binding

Two-way data binding is implemented by the V-model directive, which I have implemented. When data is updated, the view corresponding to the V-Model will be updated. We just have to implement, when the view updates, update the values on the data and we can implement two-way data binding.

class Compiler {...// v-model
  modelUpdater(node, value, key) {
    node.value = value
    // Data change notification view update
    new Watcher(this.vm, key, (newValue) = > {
      node.value = newValue
    })
    // View update, update data on data
    node.addEventListener('input'.() = > {
        this.vm[key] = node.value
    })
  }
  ...
}
Copy the code

This is done by listening for the input thing.

Complete source address: CS – Notes/code/JavaScript/vue2 / min – vue at main Buzhifanji/CS – Notes (github.com)