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:
- 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
- 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)