preface
We may often wonder how Vue implements data updates and view changes. The view changes and the data can be updated as well.
Response principle
So, let’s look at the following picture first:
Overall process:
- We need to create a VUE object. Option to record the parameters of the options passed through _proxyData data attributes in the injection Vue instances, you can pass this. Call XXX
- Create an observer for data hijacking to convert properties of data to GET and set. When the data changes, trigger the change and tell the Dep: I have changed the data and need to call your notify method
- And the notify method of Dep is going to call the updater method of Watcher, which is going to send a notification saying, my data has changed, you’re updating the view, and Watcher’s updater is going to go back and update the view
- When we create a Watcher object, we also add the current Watcher object to the SUBs array of the Dep. That is, collect the dependencies and let Dep record them
- After the observer is created, Compiler objects are also created to parse instructions, differential expressions, etc. When the page is loaded for the first time, methods in Compiler are called to update the view. Also, subscribe to data changes (Dep notifies Watcher when data changes) and bind the update view (the callback function passed when Watcher was created to update the view).
- In general: The Compiler updates the view when the page is first loaded, and Watcher updates the view when the data changes
Create a VUE object
Record the parameter options, through _proxyData data attributes in the injection Vue instances, can pass this. Call XXX
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 and 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)
}
_proxyData (data) {
// Iterate over all attributes in data
Object.keys(data).forEach(key= > {
// This is injected into the vue instance for later use
Object.defineProperty(this, key, {
enumerable: true.configurable: true.get() {
return data[key]
},
set(newval) {
if (newval === data[key]) return
data[key] = newval
}
})
})
// Inject the data attribute into the vue instance}}Copy the code
Create the observer
It’s data hijacking, converting properties in data to GET and set
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
// 1. Determine whether data is an object to ensure code robustness
if(! data ||typeofdata ! = ='object') return
// 2. Iterate over all properties of the data object
Object.keys(data).forEach(key= > {
this.defineReactive(data, key, data[key])
})
}
defineReactive(obj, key, val) {
const that = this
// Collect dependencies and send notifications
let dep = new Dep()
// If val is an object, recursive traversal is required
this.walk()
// This.$data is injected into this
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get() {
// Collect dependencies
Dep.target && dep.addSub(Dep.target)
// if obj[key] is passed, the get method will be triggered, which will cause stack overflow
return val
},
set(newval) {
if (newval === val) return
val = newval
that.walk()
// Send notifications
dep.notify()
}
})
}
}
Copy the code
Create Dep:
Store all observers and send notifications
class Dep {
constructor () {
// Store all observers
this.subs = []
}
// Add an observer
addSub (sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// Send notifications
notify() {
this.subs.forEach(sub= > {
sub.update()
})
}
}
Copy the code
Create a Watcher object:
When the data changes, the callback function is called to update the view
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
// Attributes in data
this.key = key
// Call back to update the view
this.cb = cb
// Record the watcher object to the Dep static property target
Dep.target = this
// Trigger the get method, in which addSub is called
this.oldValue = vm[key]
Dep.target = null // Needs to be reset to prevent future reassignments
}
// Update the view when data changes
update() {
// when update is called, the data has changed, so newValue is the newValue
let newValue = this.vm[this.key]
if (newValue === this.oldValue) return
this.cb(newValue)
}
}
Copy the code
Create the compiler:
Compile templates, work with 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) {
let childNodes = el.childNodes
Array.from(childNodes).forEach(node= > {
if (this.isTextNode(node)) {
this.compileText(node)
} else if (this.isElementNode(node)) {
this.compileElement(node)
}
// Check whether the current node has child nodes, if so, recurse
const flag = node.childNodes && node.childNodes.length
if (flag) this.compile(node)
})
}
// Compile element node, process instruction
compileElement(node) {
// Iterate over all attribute nodes
Array.from(node.attributes).forEach(attr= > {
// Determine if it is a command
let attrName = attr.name
if (this.isDirective(attrName)) {
attrName = attrName.substr(2) // The suffix name of the directive
const key = attr.value
this.update(node, key, attrName)
}
})
}
// Process all instructions
update(node, key, attrName) {
const updateFn = this[attrName + 'Updater']
updateFn && updateFn.call(this, node, this.vm[key], key)
}
textUpdater(node, value, key) {
node.textContent = value
new Watcher(this.vm, key, newValue= > {
node.textContent = newValue
})
}
modelUpdater(node, value, key) {
node.value = value
new Watcher(this.vm, key, newValue= > {
node.value = newValue
})
// Bidirectional binding
node.addEventListener('input'.() = > {
console.log(value, node.value)
// Assign a value to the current form
this.vm[key] = node.value
})
}
// Handle text nodes, interpolating expressions
compileText(node) {
const reg = / \ {\ {(. +?) \} \} /
let 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
})
}
}
// Determine if the element attribute is an instruction
isDirective (attrName) {
return attrName.startsWith('v-')}// Determine whether it is a text node
isTextNode(node) {
return node.nodeType === 3
}
// Determine if it is an element node
isElementNode(node) {
return node.nodeType === 1}}Copy the code
Two-way binding
- With the reactive principle explained above, the principle of bidirectional binding is easy to understand
- For example, for forms, bind an input event to the form, obtain the changed value of the view through Node. value, reassign the value to the attribute value on the VUE object, and then trigger the reactive mechanism to complete the bidirectional binding