The preparatory work
To get a sense of these:
- Data driven
- Responsive core principles
- Publish subscribe mode and observer mode
Data driven
- Data responsiveness: The data model is a normal JS object, but when the data is modified, the view is updated with it, avoiding manual work by the developer
DOM
operation - Two-way data binding: When data changes, so does the view; As the view changes, so does the data. Embodiment is for form elements, you can use
v-model
Directive to create a two-way data binding - Data-driven: Development only needs to focus on the data itself, rather than how the data should be rendered
Responsive core principles
In VUe2, responsivity is implemented based on Object.defineProperty:
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">msg</div>
<script>
let data = {
msg: 'hello world'
}
let vm = {}
Object.defineProperty(vm, 'msg', {
/ / can be enumerated
enumerable: true./ / can be configured
configurable: true.// this is triggered when reading vm. MSG and data. MSG is read
get() {
return data.msg
},
// This is triggered when the value of vm. MSG is set, and dom manipulation can be done here to modify the view
set(v) {
if (v === data.msg) return
data.msg = v
document.getElementById('app').textContent = data.msg
}
})
// When the vm. MSG is reassigned, the view will also re-render the new value
</script>
</body>
</html>
Copy the code
This method requires traversing properties in the object to set the getter and setter
In VUe3, you use a proxy, which listens on the entire object rather than adding a getter and setter to each property separately.
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">msg</div>
<script>
const data = {
msg: 'hello world'.count: 0
}
// proxy is a constructor
// The first parameter is the proxied object and the second parameter is the handler
// new returns the proxy object
const vm = new Proxy(data, {
get(target, key) {
return target[key]
},
set(target, key, value) {
if (target[key] === value) return
target[key] = value
document.getElementById('app').textContent = target[key]
}
})
</script>
</body>
</html>
Copy the code
Publish subscribe mode and observer mode
- Publish subscribe pattern: This pattern has three objects: publisher, subscriber, and message center (event center). When an event is triggered, the publisher passes a signal to the message center, and all tasks subscribed to the message center begin to execute upon receiving the signal.
In VUE, you can experience the publish-subscribe model by customizing events:
// Create an empty vue instance
// Message center
const vm = new Vue()
/ / subscriber
vm.$on('change'.() = > {
console.log('Event triggerred')})/ / publisher
vm.$emit('change')
Copy the code
Sibling components in VUE can communicate using this method
The custom event mechanism can be easily simulated:
class EventEmitter {
This class contains two methods, $emit and $ON, as well as an object that can hold published events
constructor() {
// Create the object that holds the event
this.subs = {}
}
$on- subscriber
// Type is the event name and handler is the event handler function
$on(type, handler) {
// Determine if the event is present in subs, and if not, assign it to an empty array to store multiple events
this.subs[type] = this.subs[type] || []
this.subs[type].push(handler)
}
// $emit- publisher$emit(type, ... args) {// Execute the event if it exists
if (this.subs[type]) {
this.subs[type].forEach(handler= >{ handler(... args) }) } } }Copy the code
Let’s test it again:
const event = new EventEmitter()
event.$on('change'.(arg1, arg2) = > {
console.log(arg1, arg2)
})
event.$emit('change'.1.2)
Copy the code
There will also be printing on the console
- The Observer pattern: Compared to the publish-subscribe pattern, the Observer pattern has fewer message centers. Within the publisher (target)
subs
Array to store all the subscribers (observers),addSub
Function add observer,notify
Function to call the subscriber (observer) when the event is triggeredupdate
Function to perform the corresponding task.That is, unlike the “zero coupling” of publisher and subscriber in the publish-subscribe model, there is a dependency between target and observer in the observer model.
Simple implementation (no consideration of parameter passing)
// target - publisher
class Dep {
constructor() {
// Initialize an array of subs to store observers
this.subs = []
}
// addSub method - adds the observer to subs
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// notify - Call the update method for all observers
notify() {
this.subs.forEach(sub= > {
sub.update()
})
}
}
// Observer - subscriber
class Watcher {
update() {
// The corresponding task for the observer to perform
console.log('updated')}}Copy the code
Test it out:
const dep = new Dep()
const watcher = new Watcher()
dep.addSub(watcher)
dep.notify()
Copy the code
To sum up:
The observer mode is scheduled by the target, and there is a dependent relationship between the observer and the target. The publish subscribe model is uniformly scheduled by the message center, with no connection between publishers and subscribers.
Simulate the Vue responsive principle
The overall structure is as follows
Now implement these five aspects step by step (mini-version)
Vue
Main functions of Vue:
- Receives initialization parameters
- The data in data is injected into the VUE instance and converted into getters and setters
- Call observer to listen for changes in data
- The Compiler is called to resolve the difference expressions and instructions
Take a look at the class diagram:
Implement it according to the function:
class Vue {
constructor(options) {
// Save the data in options
// 1. Save options to $options
this.$options = options || {}
// 2. Save options.data to $data
this.$data = options.data || {}
// 3. Save options.el to $el
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// Convert data into getters and setters and inject them into vue instances
this._proxyData(this.$data)
// Call observer to monitor data changes
// Call compiler to parse instructions and differential expressions
}
_proxyData(data) {
/ / traverse data
Object.keys(data).forEach(key= > {
// Inject attributes from data into vue instances
Object.defineProperty(this, key, {
enumerable: true.configurable: true.get() {
return data[key]
},
set(newVal) {
if (newVal === data[key]) return
data[key] = newVal
}
})
})
}
}
Copy the code
Also reference this in the template HTML file:
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</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 vm = new Vue({
el: '#app'.data: {
msg: 'hello'.count: 1}})console.log(vm)
</script>
</body>
</html>
Copy the code
The printed result is
See that $data, $EL, $options and getters and setters are already in the vue instance.
Observer
The Main functions of the Observer:
- Convert properties in data to responsive data (if a property is also an object, convert it to responsive object)
- Send notifications when data changes (with observer mode)
Observer class diagram:
Implementation:
class Observer {
constructor(data) {
// Call walk to convert the getter setter
this.walk(data)
}
walk(data) {
// Determine if data is an object
if(! data ||typeofdata ! = ='object') return
// Iterate over the attributes in data
Object.keys(data).forEach(key= > {
this.defineReactive(data, key, data[key])
})
}
defineReactive(obj, key, val) {
// Call the walk method on a value to fix the bug that val cannot be converted to a getter for its internal properties when it is an object
this.walk(val)
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get() {
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
// When assigning a value to a property in data, if the value is an object, convert its internal property to a getter setter
that.walk(newVal)
// Send notifications}}}})Copy the code
DefineReactive returns val directly when converting the getter because if obj[key] is returned, a recursive call is generated and an infinite loop is generated:
Also create an Observer instance in vue.js and pass in $data
constructor(options) {
// Save the data in options
// 1. Save options to $options
this.$options = options || {}
// 2. Save options.data to $data
this.$data = options.data || {}
// 3. Save options.el to $el
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// Convert data into getters and setters and inject them into vue instances
this._proxyData(this.$data)
// Call observer to monitor data changes
new Observer(this.$data)
// Call compiler to parse instructions and differential expressions
}
Copy the code
In the HTML file, we introduce observer.js before we introduce vue.js, open the browser and print the VM to see that the properties in $data have been converted to getters and setters
Compiler
Compiler’s main functions:
- Compile templates, parse instructions, and differential expressions
- Render the page and render the data again after the data is updated
Class diagram:
Implementation:
class Compiler {
// The constructor receives the vue instance
constructor(vm) {
this.el = vm.$el
this.vm = vm
this.compiler(this.el)
}
// Compile templates, handle text nodes and element nodes
compiler(el) {
const childNodes = el.childNodes
Array.from(childNodes).forEach(node= > {
// Determine the node type and operate on the corresponding node
if (this.isTextNode(node)) {
this.compilerText(node)
} else if (this.isElementNode(node)) {
this.compilerElement(node)
}
// Determine whether there are child nodes, if there are recursive
if (node.childNodes && node.childNodes.length) {
this.compiler(node)
}
})
}
// Process element nodes
compilerElement(node) {
// Iterate over all attributes
Array.from(node.attributes).forEach(attr= > {
let attrName = attr.name
// Determine if this attribute is an instruction
if (this.isDirective(attrName)) {
attrName = attrName.substr(2)
let key = attr.value
this.update(node, key, attrName)
}
})
}
update(node, key, attrName) {
const updateFn = this[attrName + 'Updater']
updateFn && updateFn(node, this.vm[key])
}
textUpdater(node, value) {
node.textContent = value
}
modelUpdater(node, value) {
node.value = value
}
// Process text nodes
compilerText(node) {
const reg = / \ {\ {(. +?) \} \} /
let value = node.textContent
if (reg.test(value)) {
let key = RegExp.$1.trim()
// replace {{XXX}} with the value of the variable
node.textContent = value.replace(reg, this.vm[key])
}
}
// Determine if it is a command
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
Dep
Function:
- Collect dependencies and add observers
- Notify all observers
The class diagram
implementation
class Dep {
constructor() {
// subs is used to store all observers
this.subs = []
}
// Add an observer
addSubs(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// Send notifications
notify() {
this.subs.forEach(sub= > {
sub.update()
})
}
}
Copy the code
Dep.js is introduced at the top of the HTML file import JS file, then a Dep object is created in the defineReactive method in the Observer class, and dependencies are collected and notifications are sent in the GET and set methods
defineReactive(obj, key, val) {
// Call the walk method on a value to fix the bug that val cannot be converted to a getter for its internal properties when it is an object
this.walk(val)
const that = this
// Create a DEP object to collect dependency + send notifications
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get() {
// Collect dependencies
Dep.target && dep.addSub(Dep.target)
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
// When assigning a value to a property in data, if the value is an object, convert its internal property to a getter setter
that.walk(newVal)
// Send notifications
dep.notify()
}
})
}
Copy the code
watcher
Watcher and DEP diagram:
Function:
- When data changes trigger dependencies, DEP notifies all Watcher instances to update the view
- Add yourself to the DEP object when instantiating itself
The class diagram
implementation
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
// The attribute name in data
this.key = key
// Update the view callback function
this.cb = cb
// Log the watcher instance to the Dep static attribute target
Dep.target = this
// Old value -- This triggers the get method, which calls addSubs
this.oldValue = vm[key]
/ / clear the target
Dep.target = null
}
// Update the view
update() {
let newValue = this.vm[this.key]
if (newValue === this.oldValue) return
this.cb(newValue)
}
}
Copy the code
Then modify the text node processing method and instruction processing method in the Compiler class:
// Process text nodes
compilerText(node) {
const reg = / \ {\ {(. +?) \} \} /
let value = node.textContent
if (reg.test(value)) {
let key = RegExp.$1.trim()
// replace {{XXX}} with the value of the variable
node.textContent = value.replace(reg, this.vm[key])
// Create a watcher instance
new Watcher(this.vm, key, newValue= > node.textContent = newValue)
}
}
update(node, key, attrName) {
const updateFn = this[attrName + 'Updater']
// Use call to change the direction of this inside a method call to the current Compiler instance
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)
}
Copy the code
This way, when the data changes, the view updates with it:
After the update:
Two-way binding
Effect: When the data in the text box changes, the data and views in the VM are updated
In the v-model handler, add an input event to Node: After triggering the input, change the value of the corresponding attribute in the VM to the current value:
modelUpdater(node, value, key) {
node.value = value
new Watcher(this.vm, key, newValue= > node.value = newValue)
// Implement bidirectional binding
node.addEventListener('input'.() = > {
this.vm[key] = node.value
})
}
Copy the code
Reassignment triggers the set method, which in turn calls the notify method of the Dep instance to update the corresponding view