This is the first day of my participation in Gwen Challenge
Vue data bidirectional binding principle
Vue data bidirectional binding is implemented through data hijacking combined with the publisher-subscriber pattern.
Let’s start by looking at what objects are defined on Vue initialization data
Code:
new Vue({
data: {
obj: {
foo: 1,}},created() {
console.log(this.obj)
},
})
Copy the code
Results:
You can see that the property foo has two corresponding get and set methods. How do these two methods appear? This is because Vue implements data hijacking via Object.defineProperty(), which controls specific operations on Object attributes, such as read and write permissions, enumerability, etc. The focus here is on get and set functions. (For more on Object.defineProperty(), click mdN-object.defineProperty.)
First, we initialize an object property in literal form
Code:
let book = {
name: 'JavaScript Advanced Programming ',}console.log(book.name)
console.log(book)
Copy the code
Results:
We redefined Object attributes using Object.defineProperty() and overwrote the get and set methods
Code:
let book = {}
Object.defineProperty(book, 'name', {
get: () = > {
return ' ' ' + this.name + ' ' '
},
set: (value) = > {
this.name = value
console.log('Set the name property to:' + value)
},
})
book.name = 'JavaScript Advanced Programming '
console.log(book.name)
console.log(book)
Copy the code
Results:
As you can see, the printed data looks a lot like the data structure initialized by Vue. It also has get and set functions, which is how Vue implements data hijacking.
MVVM implementation idea analysis
There are two main aspects to implementing MVVM:
- Data changes update the view
- View changes update data
The view updates data only by listening to events. How does the view update data?
In fact, the data hijacking example above shows the answer. Set a set function to the property via Object.defineProperty(). The set function is triggered when data changes, so we just need to write the update view method here and data updates the view.
Implement a listener Observer
The purpose of a listener is to listen for every attribute of the data, and we saw from the above example that this can be done with the Object.defineProperty() method.
Notifies the subscriber Watcher to perform an update function to update the view when it hears a change in the property value.
In this process, there will be many subscribers, so we also need to create a subscriber Dep for unified management. The subscriber Dep has a container subs to collect subscribers.
function defineReactive(data, key, value) {
// Recursively iterate over all child attributes
observe(value)
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true.configurable: true.get: function reactiveGetter() {
if (Dep.target) {
dep.addSub(Dep.target)
}
return value
},
set: function reactiveSetter(newValue) {
if (value === newValue) {
return
}
value = newValue
dep.notify()
}
})
}
function observe(data) {
if(! data ||typeofdata ! = ='object') {
return
}
Object.keys(data).forEach(key= > {
defineReactive(data, key, data[key])
})
}
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub(sub) {
this.subs.push(sub)
},
notify() {
this.subs.forEach(sub= > {
sub.update()
})
}
}
Dep.target = null
Copy the code
Implement a subscriber Watcher
The subscriber Watcher’s role is to receive notification of property changes and then perform view update functions.
When the subscriber Watcher is initialized, it needs to add itself to the subscriber Dep. We already know that the subscriber Wather is added through the subscriber Dep in the listener Observer get function, so we only need to trigger the Observer GET function when Watcher is initialized. To trigger the GET function, we just need to get the corresponding property value, which is precisely because we use Object.defineProperty() for data listening.
function Watcher(vm, exp, cb) {
this.vm = vm
this.exp = exp
this.cb = cb
this.value = this.get()
}
Watcher.prototype = {
update() {
const value = this.vm.$data[this.exp]
const oldValue = this.value
if(value ! == oldValue) {this.value = value
this.cb(value)
}
},
get() {
Dep.target = this
const value = this.vm.$data[this.exp]
Dep.target = null
return value
}
}
Copy the code
Implement a parser Compile
The parser Compile does two things:
- Parsing DOM nodes, identifying template instructions, and replacing template data
- Add a subscriber to the node of the template directive and bind the update function
To implement these two functions, we have the following code:
function Compile(vm) {
this.vm = vm
this.el = vm.$el
this.fragment = null
this.init()
}
Compile.prototype = {
init() {
this.fragment = this.nodeToFragment(this.el)
this.compileNode(this.fragment)
this.el.appendChild(this.fragment)
},
nodeToFragment(el) {
const fragment = document.createDocumentFragment()
let child = el.firstChild
while (child) {
fragment.appendChild(child)
child = el.firstChild
}
return fragment
},
compileNode(fragment) {
let childNodes = fragment.childNodes
const childNodesArray = [...childNodes]
childNodesArray.forEach(node= > {
if (this.isElementNode(node)) {
this.compile(node)
}
let reg = / \ {\ {(. *) \} \} /
let text = node.textContent
if (reg.test(text)) {
let prop = reg.exec(text)[1]
this.compileText(node, prop)
}
if (node.childNodes && node.childNodes.length) {
this.compileNode(node)
}
})
},
compile(node) {
let nodeAttrs = node.attributes
const nodeAttrsArray = [...nodeAttrs]
nodeAttrsArray.forEach(attr= > {
let name = attr.name
if (this.isDirective(name)) {
let value = attr.value
if (name === 'v-model') {
this.compileModel(node, value)
}
}
})
},
compileModel(node, prop) {
let val = this.vm.$data[prop]
this.updateModel(node, val)
new Watcher(this.vm, prop, value= > {
this.updateModel(node, value)
})
node.addEventListener('input'.event= > {
let newValue = event.target.value
if (val === newValue) {
return
}
this.vm.$data[prop] = newValue
})
},
compileText(node, prop) {
let text = this.vm.$data[prop]
this.updateView(node, text)
new Watcher(this.vm, prop, value= > {
this.updateView(node, value)
})
},
updateModel(node, value) {
node.value = typeof value === 'undefined' ? ' ' : value
},
updateView(node, value) {
node.textContent = typeof value === 'undefined' ? ' ' : value
},
isDirective(attr) {
return attr.indexOf('v-')! = = -1
},
isElementNode(node) {
return node.nodeType === 1
},
isTextNode(node) {
return node.nodeType === 3}},Copy the code
The core purpose of this longer piece of code is to implement the above two functions.
The fragment will be cached in a fragment and then parsed. After parsing and compiling, the fragment will be added to the page.
Here we simply realize the difference expression {{}} and v-model recognition.
Create MyVue
Here we create a MyVue class that connects Observer, Watcher, and Compile.
function MyVue(options) {
this.$el = document.querySelector(options.el)
this.$data = options.data
this.init()
}
MyVue.prototype = {
init() {
this.proxyData(this)
observe(this.$data)
new Compile(this)},proxyData(vm) {
Object.keys(vm.$data).forEach(key= > {
Object.defineProperty(vm, key, {
get: function proxyGetter() {
return vm.$data[key]
},
set: function proxySetter(value) {
vm.$data[key] = value
}
})
})
}
}
Copy the code
Replace Vue with MyVue
Now we use MyVue instead of Vue taking over our view.
<! DOCTYPEhtml>
<html lang="en">
<head>
<! -... -->
</head>
<body>
<div id="app">
<input v-model='name'>
<p>{{name}}</p>
</div>
</body>
<! -- <script src="vender/vue.js"></script> -->
<script src="js/observer.js"></script>
<script src="js/watcher.js"></script>
<script src="js/compile.js"></script>
<script src="js/index.js"></script>
<script>
// let app = new Vue({
// el: '#app',
// data: {
// name: 'JavaScript',
/ /}
// })
// console.log(app)
let myApp = new MyVue({
el: '#app'.data: {
name: 'JavaScript',}})console.log(myApp)
</script>
</html>
Copy the code
At this point, we have implemented a simple two-way data binding.
Complete source code
The full source code can be viewed at github-vue-mvvm