index.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>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model='value.a'>
{{value.a}}
<button @click='button'>Add 1</button>
</div>
</body>
<script src="./mvvm.js"></script>
<script src="./observer.js"></script>
<script src="./watcher.js"></script>
<script src="./dep.js"></script>
<script src="./compile.js"></script>
<script>
var app = new mvvm({
el:'#app'.data: {value: {a:'123'}},methods: {button(){
this.value.a++
},
}
})
</script>
</html>
Copy the code
mvvm.js
class mvvm {
constructor(config) {
// Bind the template element node to the instance's $el attribute
this.$el = document.querySelector(config.el)
if (!this.$el) {
throw new Error('Component DOM root element cannot be empty')}this.$data = config.data
this.methods = config.methods
// Data broker
this.proxyData(config.data)
// Data monitoring
new Observer(config.data)
// Compile the template
new Compile(this.$el, this)}// Proxy methods
proxyData(data) {
Object.keys(data).forEach(key= > {
Object.defineProperty(this, key, {
configurable: true.get() {
return this.$data[key]
},
set(newValue) {
this.$data[key] = newValue
}
})
})
}
}
Copy the code
observer.js
class Observer {
constructor(data) {
this.data = data
this.observer(data)
}
observer(data) {
if(! data ||typeofdata ! = ='object') { return }
Keys returns an array of the keys of the Object
// Loop through object.defineProperty to listen for property changes
Object.keys(data).forEach(key= > {
this.defineReactive(data, key, data[key]) // Data monitoring
// If data[key] is an object that contains objects, continue to listen for deeper data
this.observer(data[key])
})
}
defineReactive(data, key, value) {
let dep = new Dep()
Object.defineProperty(data, key, {
configurable: true.enumerable: true.get: () = > {
// The subscriber Dep is designed to add a subscriber in the getter so that Watcher initialization triggers it, so it needs to decide whether to add a subscriber.
Dep.target && dep.addSub(Dep.target)
console.log('Got data' + value, key);
return value
},
// The argument to set is the new value assigned to the property
set: (newValue) = > {
// Replace the old value
if(newValue ! = value) {console.log('Updated data' + newValue);
// The new value also needs to be listened again
this.observer(newValue)
value = newValue
// Trigger watcher update()
dep.notify()
}
}
})
}
}
Copy the code
watcher.js
class Watcher {
constructor(vm, exp, cb) {
this.vm = vm
this.exp = exp{{value. A}}} exp is value. A.
this.cb = cb
// Store a value
this.oldValue = this.get()
}
get() {
Dep.target = this // Cache the watcher instance to dep.target
// Get the data on the VM and get the getter for the data object, which reads the Watcher instance from dep. target and adds it to the Dep
// let value = this.vm.$data[this.exp
let value = this.getValue(this.vm, this.exp)
Dep.target = null
return value
}
update() {
// let newValue = this.vm.$data[this.exp]
let newValue = this.getValue(this.vm, this.exp)
if (this.oldValue ! == newValue) {this.oldValue = newValue
this.cb(newValue) // Execute the callback function}}getValue(vm, exp){ exp = exp? .split('. ') // [message.a]
returnexp? .reduce((prev, next) = > {
return prev[next]
}, vm.$data)
}
}
Copy the code
dep.js
class Dep {
constructor() {
// Use an array to store watcher
this.subs = []
}
addSub(watcher) {
// Save watcher
this.subs.push(watcher)
}
notify() {
// Call update() to find the corresponding watcher when updating the value
this.subs.forEach(watcher= > {
watcher.update()
})
}
}
// This is a global Watcher. Only one global Wathcer can be calculated at a time
Dep.target = null
Copy the code
compile.js
class Compile {
constructor(el, vm) {
this.el = el
this.vm = vm
// Generate a virtual DOM (representing nodes as objects or arrays)
this.complierNodes();
// Regenerate nodes based on the virtual DOM
this.createElement()
}
// Generate the virtual DOM
complierNodes() {
// All children of the dom element of the root node
var nodeList = this.el.childNodes;
// Generate vmNodes virtual DOM
this.vm.vmNodes = this.complierNodesChild(nodeList)
}
// Generate the virtual DOM function recursively
complierNodesChild(nodeList) {
// Initialize the vmNodes array, which is finally used to return
var vmNodes = []
// Use an object to concatenate our data
var data = {}
//nodeList is a pseudo-array and cannot be traversed directly
Array.from(nodeList).forEach(node= > {
// Matches the contents of the interpolation in the string
var reg = /\{\{[^\{\}]*\}\}/g;
data = {
node: node,// Store node elements
nodeName: node.nodeName,// The node name
nodeValue: node.nodeValue,// The element value
nodeType: node.nodeType,// The node type of the element, 1 for normal elements, 3 for text nodes, and 8 for comments
data: [].// Store the contents of the interpolation expression in the node
attrs: node.attributes,// Attributes of the element node
props: {},// All attributes except those beginning with v-
directives: {},// An array of instructions
children: [].// To store child nodes
events: {},// Store node events
}
// If the current node is a text node
if (node.nodeType === 3) {
// if the nodeValue value is null, the value is returned
if (node.nodeValue.trim() === ' ') {
return false
} else {
// The match method of the string returns an array based on the re
var arr = node.nodeValue.match(reg) || [];
// Loop through the braces
arr.forEach(v= > {
v = v.replace(/[/{/}]/g."");
data.data.push(v)
})
}
}
// If the current node is a normal node
if (node.nodeType === 1) {
// Get the attributes of the element node
varattrObj = { ... node.attributes }Object.keys(attrObj).forEach(index= > {
var prop = attrObj[index]
// Determine if the attribute starts with a v-
if (/^(v-)+/.test(prop.name)) {
// Save the directives to data's directives
data.directives[prop.name] = prop.value
} else if (+ / / ^ (@).test(prop.name)) {
// If the attribute starts with @, it is an event
data.events[prop.name.replace(The '@'.' ')] = prop.value
} else {
// Add attributes that do not start with v- to data.props
data.props[prop.name] = prop.value
}
})
}
// If the node has children, the current function is executed recursively
if (node.childNodes.length > 0) {
data.children = this.complierNodesChild(node.childNodes)
}
// Put objects on virtual nodes
vmNodes.push(data)
})
return vmNodes
}
/** * reduce executes callbacks for each element in the array, excluding elements that were deleted or never assigned. * Takes four arguments: the initial value (or the value returned by the last callback), the current element value, the current index, and the array that called Reduce. * /
// Use the reduce function to get the lowest level key
getValue(exp){ exp = exp? .split('. ')
returnexp? .reduce((prev, next) = > {
return prev[next]
}, this.vm.$data)
}
getInputSetValue(vm, exp, value){ exp = exp? .split('. ')
returnexp? .reduce((prev, next, currentIndex) = > {
$data = vm.$data = vm.$data = vm
if (currentIndex == exp.length - 1) {
prev[next] = value
}
return prev[next]
}, vm.$data)
}
// Generate child nodes recursively
createElementChild(parentNode, nodeList) {
// Loop through the virtual node to regenerate the DOM element
nodeList.forEach(node= > {
var newNode;
// Use createElement for element nodes (non-text nodes)
if (node.nodeType === 1) {
newNode = document.createElement(node.nodeName)
// Element nodes may have time to bind events using addEventListener
Object.keys(node.events).forEach(eventName= > {
newNode.addEventListener(eventName, (event) = > {
// The "this" in methods refers to the VM instance
// this.vm.methods[node.events[eventName]](event);
this.vm.methods[node.events[eventName]].call(this.vm, event); })})// If the current node is an input element, the input element also contains v-model
if (node.nodeName == 'INPUT' && node.attrs['v-model']) {
// Get the attribute value of vm.$data to assign the current node value
let value = this.getValue(node.attrs['v-model'].value)
newNode.value = value
// Listen for changes in the data property of the V-model binding
new Watcher(this.vm, node.attrs['v-model'].value, () = > {
newNode.value = this.getValue(node.attrs['v-model'].value)
})
// Handle the input event for the input tag
newNode.addEventListener('input'.(event) = > {
// Get the value of the input box
var inputValue = event.target.value;
// Modifying an attribute under vm.$data triggers the set method to update the page
this.getInputSetValue(this.vm, node.attrs['v-model'].value, inputValue)
})
}
}
// Text node
if (node.nodeType === 3) {
// Replace the interpolation content of the node with the attribute value of vm.$data
var text = this.replaceElementText(node.nodeValue);
newNode = document.createTextNode(text)
// Listen for vm.$data to change the current node attribute value
new Watcher(this.vm, node.data[0].() = > {
node.node.nodeValue = this.replaceElementText(node.nodeValue)
})
}
// Comment the node
if (node.nodeType === 8) {
return
}
// Overwrite the original DOM node with the new DOM node
node.node = newNode
parentNode.appendChild(newNode)
if (node.children.length > 0) {
// Determine if there are child elements, the central execution function, the function will also change relative
this.createElementChild(newNode, node.children)
}
})
}
// Replace the interpolation content of the node with the attribute value of vm.$data
replaceElementText(value) {
// Global regular expression
var reg = /\{\{[^\{\}]*\}\}/g;
// Return the data with two braces to an array
var regArr = value.match(reg);
// If the value is an array, there are two braces inside the value
if (Array.isArray(regArr)) {
// The loop replaces the value of value
regArr.forEach(v= > {
{{value.a}}} {value
var prop = v.replace(/[/{/}]/g."");
value = this.getValue(prop)
})
}
return value;
}
// Regenerate nodes based on the virtual DOM
createElement() {
// Create a virtual template node (document fragment)
var fragment = document.createDocumentFragment();
// Returns the DOM element generated by the last virtual node
this.createElementChild(fragment, this.vm.vmNodes);
// Clear all contents of the root node
this.el.innerHTML = ' ';
// Regenerate the DOM element
this.el.appendChild(fragment)
}
}
Copy the code