Vue source code parsing – hands-on implementation of simplified VERSION of MVVM
The introduction
If you only say that MVVM is a view model, as long as the data changes, the view will also be updated synchronously, it may not reach the level that the interviewer wants. You can even say that it makes the interviewer feel like you don’t know enough and only have a rough idea of two-way binding.
This blog aims to understand MVVM more deeply through a simplified version of the code, if there are any questions, welcome to comment, thank you!
Finally, I hope you can give a like or star: Star: thank you for your support!
Implement source code portal
At the same time, also included in the little lion front note warehouse, remember (°▽°) Blue ✿
Front-end-learning-to-organize
Effect:
Several ways to implement bidirectional binding
At present, several mainstream MVC (VM) frameworks have implemented one-way data binding, that is, data operation view, data update, view synchronous update. Two-way data binding is nothing more than to add a change(input) event to input elements (such as input, textarea, etc.) on the basis of one-way binding, to dynamically modify the model and view, so that the view can be used to manipulate data, namely view update, data synchronization update.
There are several ways to implement data binding:
Backbencher mode (backbencher) Dirty value check (angular.js) compares old values with new ones and updates the view if there are changes, the easiest way to detect data changes is through setInterval() scheduled polling. Data hijacking (vue.js)
Vm. set(‘property’, value); vm.set(‘property’, value)
However, the above method does not meet our needs now. We prefer to update the data through vm.property = value and update the view automatically, so we have the following two methods:
Dirty value detection: Angular. js uses dirty value detection to determine whether to update views by comparing data changes. The simplest method is to detect data changes through setInterval() polling. Of course, it only enters dirty value detection when the specified event is triggered, roughly as follows:
DOM
Events, such as the user entering text, clicking a button, and so on. (ng-click
)XHR
Response event ($http
)- The browser
Location
Change events ($location
) Timer
Events ($timeout
.$interval
)
Data hijacking: Vue. js uses data hijacking combined with publiser-subscriber mode. It hijacks the setter and getter of each attribute through Object.defineProperty () to publish messages to subscribers when data changes and trigger corresponding listening callback.
implementationmvvm
Bidirectional binding of
To implement MVVM bidirectional binding, the following must be implemented:
- Implement an instruction parser
Compile
, scan and parse the instructions of each element node, replace data according to the instruction template, and bind the corresponding update function - Implement a data listener
Observer
, can listen on all attributes of the data object, get the latest value if there is a change and notify the subscriber - To implement a
Watcher
, as a connectionObserver
andCompile
To subscribe to and receive notification of each property change, execute the corresponding callback function of the directive binding, and update the view mvvm
The entry function integrates all three
The integration flow chart is as follows:
Implement the instruction parser Compile
Compile mainly does is parses the template instructions, replaces the variables in the template with data, initializes the render page view, binds the corresponding node of each instruction to update function, adds the subscriber that listens to the data, receives notification once the data changes, updates the view, as shown below:
In the process of traversing parsing, dom nodes are operated for several times. In order to improve performance and efficiency, the EL of the root node of vue instance is first converted into document fragment for parsing and compilation. After parsing, the fragment is added back to the original real DOM node.
The HTML page introduces our rewrite of myvue.js
<script src="./myVue.js"></script>
Copy the code
createmyVue
类
Create a myVue class with the constructor shown below and save the page mount EL, data, and action set options.
class myVue{
constructor(options){
this.$el = options.el
this.$data = options.data
this.$options = options
if(this.$el){
// 1. Implement data observer (omit...)
// 2. Implement instruction parser
new Compile(this.$el,this)}// console.log(this)}}Copy the code
implementationCompile
类
Specific implementation steps:
- Determine if the current mount is an element node or not
query
- Get the document fragment object and put it in memory to manipulate our
dom
Node to reduce page backflow and redraw - Finally, add the compiled template to the root element
class Compile{
constructor(el,vm){
// Check whether it is an element node. If not, query
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
// 1, get the document fragment object, put it in memory, will reduce the page backflow and redraw
const fragment = this.node2Fragment(this.el)
// create a template
this.compile(fragment)
// append the child element to the root element
this.el.appendChild(fragment)
}
Copy the code
To check whether the node is an element node, check whether nodeType is 1
isElementNode(node){
return node.nodeType === 1
}
Copy the code
Through the document. CreateDocumentFragment (), create a document fragments object, by el. Judging if firstChild still exist, and then add the dom node to the document fragments in the object, the last return
node2Fragment(el){
// Create a document shard object
const fragment = document.createDocumentFragment()
let firstChild
while(firstChild = el.firstChild){
fragment.appendChild(firstChild)
}
return fragment
}
Copy the code
Compiling templates
When you parse a template, you get all the child nodes, which are element nodes and text nodes. If the current node has children, you need to iterate through the children through a recursive operation.
compile(fragment){
// get all child nodes
const childNodes = fragment.childNodes;
[...childNodes].forEach(child= >{
// console.log(child)
// If it is an element node, the element node is compiled
if(this.isElementNode(child)){
// console.log(' element node ',child)
this.compileElement(child)
}else{
// Other are text nodes, compile text nodes
// console.log(' text node ',child)
this.compileText(child)
}
if(child.childNodes && child.childNodes.length){
this.compile(child)
}
})
}
Copy the code
Compile element nodes (encounter design patterns)
Node Node has an attributes attribute that gets all the attributes of the current node and determines whether the current attribute name is a directive by starting with a V -. If it is a directive, it needs to be sorted and compiled to drive the view with the data. After the data is updated, the removeAttribute event is used to remove the attribute of the tag on the instruction.
If it is not a directive, such as the event @click=”sayHi”, it can be implemented only with the v-on directive.
For different instructions, we’d better encapsulate, here is the clever use of strategy mode.
compileElement(node){
const attributes = node.attributes;
[...attributes].forEach(attr= >{
// console.log(attr)
const {name,value} = attr;
// console.log(name,value)
// Determine whether the current name value is an instruction by starting with 'v-'
if(this.isDirective(name)){
// console.log(name.split('-'))
const [,directive] = name.split(The '-') // text html model on:click
// console.log(directive)
const [dirName,eventName] = directive.split(':') // text html model on
// Update data data-driven view
complieUtil[dirName](node,value,this.vm,eventName)
// Remove the attribute from the label on the directive
node.removeAttribute('v-' + directive)
}else if(this.isEventName(name)){ // @click="sayHi"
let [,eventName] = name.split(The '@')
complieUtil['on'](node,value,this.vm,eventName)
}
})
}
Copy the code
To check whether the current attrName is a command, you only need to check whether the command starts with v-
isDirective(attrName){
return attrName.startsWith('v-')}Copy the code
To determine if the current attrName is an event, see if it is an event binding starting with ‘@’
isEventName(attrName){
return attrName.startsWith(The '@')}Copy the code
Instruction processing set
const complieUtil = {
getVal(expr,vm){
return expr.split('. ').reduce((data,currentVal) = >{
// console.log(currentVal)
return data[currentVal]
},vm.$data)
},
text(node,expr,vm){
let value;
// Element node
if(expr.indexOf('{{')! = =- 1){
value = expr.replace(/ \ {\ {(. +?) \}\}/g, (... args)=>{return this.getVal(args[1],vm); })}else{ // Text node
value = this.getVal(expr,vm)
}
this.updater.textUpdater(node,value)
},
html(node,expr,vm){
const value = this.getVal(expr,vm)
this.updater.htmlUpdater(node,value)
},
model(node,expr,vm){
const value = this.getVal(expr,vm)
this.updater.modelUpdater(node,value)
},
on(node,expr,vm,eventName){
let fn = vm.$options.methods && vm.$options.methods[expr]
// bind the fn to the original VM
node.addEventListener(eventName,fn.bind(vm),false)
},
bind(node,expr,vm,attrName){
},
// Update the function
updater:{
textUpdater(node,value){
node.textContent = value
},
htmlUpdater(node,value){
node.innerHTML = value
},
modelUpdater(node,value){
node.value = value
}
}
}
Copy the code
Implement a data listener Observer
Using obeject.defineProperty () to listen for property changes, recursively traverse data objects requiring Observe, including properties of child property objects, with setters and getters. So, if you assign a value to this object, it will trigger the setter, so you can listen for changes in the data. The specific code is as follows:
class Observer{
constructor(data){
this.observe(data)
}
observe(data){
if(data && typeof data === 'object') {// console.log(Object.keys(data))
// Data hijacking
Object.keys(data).forEach(key= >{
this.defineReactive(data,key,data[key])
})
}
}
defineReactive(obj,key,value){
// recursive traversal
this.observe(value)
Object.defineProperty(obj,key,{
enumerable: true.configurable: false,
get(){
// Add an observer to the Dep for dependency collection when the subscription data changes
return value
},
// Change this to point to the Class Observer via the arrow function
set:(newVal) = >{
this.observe(newVal)
if(newVal ! == value){ value = newVal } } }) } }Copy the code
The following is an example of data:
data: {
person: {name: 'Chocolate'.age: 20.hobby: 'Write code'
},
msg: 'Technology Blog of Super Ease'.htmlStr: ' Welcome to study ~
'
},
Copy the code
Implement Watcher to update the view
Watcher
Observer
Compile
- To the attribute subscriber (
dep
) inside add yourself - You have to have one of them
update()
methods - Pending attribute change
dep.notify()
Can call its ownupdate()
Method and triggerCompile
The callback bound to.
Watcher
The subscriber
When you instantiate Watcher, call the getOldVal() method to get the old value. Dep.target = watcherInstance (this) marks the subscriber as the current Watcher instance (pointing to itself).
class Watcher{
constructor(vm,expr,cb){
this.vm = vm
this.expr = expr
this.cb = cb
// Save the old value first
this.oldVal = this.getOldVal()
}
getOldVal(){
// Point the current subscriber to yourself
Dep.target = this
// Get the old value
const oldVal = complieUtil.getVal(this.expr,this.vm)
// Set the value
Dep.target = null
return oldVal
}
// Compare the new value with the old value and update the view if there is a change
update(){
const newVal = complieUtil.getVal(this.expr,this.vm)
// If the old and new values are not equal, the new value callback is called
if(newVal ! = =this.oldVal){
this.cb(newVal)
}
}
}
Copy the code
When the get method is executed, the current Watcher instance is added to the subscriber deP of the property, so that the watcherInstance (this) receives an update notification when the property value changes.
// The preceding paragraph omitted...
defineReactive(obj,key,value){
// recursive traversal
this.observe(value)
const dep = new Dep()
Object.defineProperty(obj,key,{
enumerable: true.configurable: false,
get(){
// When subscribing to data attributes, add an observer to the Dep for dependency collection
Dep.target && dep.addSub(Dep.target)
return value
},
// Change this to point to the Class Observer via the arrow function
set:(newVal) = >{
this.observe(newVal)
if(newVal ! == value){ value = newVal// If the old and new values are different, Dep is told to notify the change
dep.notify()
}
}
})
}
Copy the code
subscriberdep
There are two main things to do:
- Collect subscribers
- Notify subscribers of updates
class Dep{
constructor() {this.subs = []
}
// Collect observers
addSub(watcher){
this.subs.push(watcher)
}
// Tell the observer to update
notify(){
console.log('Observer'.this.subs);
this.subs.forEach(watcher= > watcher.update())
}
}
Copy the code
Modify our originalCompile.js
file
At this point, when we modify some data, the data has changed, but the view has not been updated. So when are we going to add the binding Watcher? Continue to look at the image below
That is, when we subscribe to data changes, we bind the update function to let Watcher update the view. At this point we modify our original Compile. Js file as follows:
// Instruction processing set
const complieUtil = {
getVal(expr,vm){
return expr.split('. ').reduce((data,currentVal) = >{
// console.log(currentVal)
return data[currentVal]
},vm.$data)
},
{{a}}--{{b}}
getContentVal(expr,vm){
return expr.replace(/ \ {\ {(. +?) \}\}/g, (... args)=>{// console.log(args[1]);
return this.getVal(args[1],vm);
})
},
text(node,expr,vm){
let value;
if(expr.indexOf('{{')! = =- 1){
value = expr.replace(/ \ {\ {(. +?) \}\}/g, (... args)=>{// Bind watcher to update the view
new Watcher(vm,args[1], () = > {this.updater.textUpdater(node,this.getContentVal(expr,vm))
// console.log(expr);
})
return this.getVal(args[1],vm); })}else{ // it is possible that v-text='obj. Name 'v-text=' MSG'
value = this.getVal(expr,vm)
}
this.updater.textUpdater(node,value)
},
html(node,expr,vm){
const value = this.getVal(expr,vm)
new Watcher(vm,expr,(newVal)=>{
this.updater.htmlUpdater(node,newVal)
})
this.updater.htmlUpdater(node,value)
},
model(node,expr,vm){
const value = this.getVal(expr,vm)
// Bind update function to update view changes when subscription data changes
// Data ==> View
new Watcher(vm,expr,(newVal)=>{
this.updater.modelUpdater(node,newVal)
})
this.updater.modelUpdater(node,value)
},
on(node,expr,vm,eventName){
let fn = vm.$options.methods && vm.$options.methods[expr]
// bind the fn to the original VM
node.addEventListener(eventName,fn.bind(vm),false)
},
bind(node,expr,vm,attrName){
let attrVal = this.getVal(expr,vm)
this.updater.attrUpdater(node,attrName,attrVal)
},
// Update the function
updater:{
textUpdater(node,value){
node.textContent = value
},
htmlUpdater(node,value){
node.innerHTML = value
},
modelUpdater(node,value){
node.value = value
},
attrUpdater(node, attrName, attrVal){
node.setAttribute(attrName,attrVal)
}
}
}
class Compile{
constructor(el,vm){
// Check whether it is an element node. If not, query
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
// 1, get the document fragment object, put it in memory, will reduce the page backflow and redraw
const fragment = this.node2Fragment(this.el)
// create a template
this.compile(fragment)
// append the child element to the root element
this.el.appendChild(fragment)
}
// Check whether the nodeType is 1
isElementNode(node){
return node.nodeType === 1
}
node2Fragment(el){
// Create a document shard object
const fragment = document.createDocumentFragment()
let firstChild
while(firstChild = el.firstChild){
fragment.appendChild(firstChild)
}
return fragment
}
compile(fragment){
// get all child nodes
const childNodes = fragment.childNodes;
[...childNodes].forEach(child= >{
// console.log(child)
// If it is an element node, the element node is compiled
if(this.isElementNode(child)){
// console.log(' element node ',child)
this.compileElement(child)
}else{
// Other are text nodes, compile text nodes
// console.log(' text node ',child)
this.compileText(child)
}
if(child.childNodes && child.childNodes.length){
this.compile(child)
}
})
}
// Compile the element node
compileElement(node){
const attributes = node.attributes;
[...attributes].forEach(attr= >{
// console.log(attr)
const {name,value} = attr;
// console.log(name,value)
// Determine whether the current name value is an instruction by starting with 'v-'
if(this.isDirective(name)){
// console.log(name.split('-'))
const [,directive] = name.split(The '-') // text html model on:click
// console.log(directive)
const [dirName,eventName] = directive.split(':') // text html model on
// Update data data-driven view
complieUtil[dirName](node,value,this.vm,eventName)
// Remove the attribute from the label on the directive
node.removeAttribute('v-' + directive)
}else if(this.isEventName(name)){ // @click="sayHi"
let [,eventName] = name.split(The '@')
complieUtil['on'](node,value,this.vm,eventName)
}
})
}
// Compiles the text node
compileText(node){
// {{}} v-text
// console.log(node.textContent)
const content = node.textContent
if(/ \ {\ {(. +?) \} \} /.test(content)){
// console.log(content)
complieUtil['text'](node,content,this.vm)
}
}
isDirective(attrName){
return attrName.startsWith('v-')}// Check whether the current attrName is an event binding starting with '@'
isEventName(attrName){
return attrName.startsWith(The '@')}}Copy the code
At this point, we can drive the view with data changes, such as changing our age from 20 to 22, as shown below. Watcher updates the view when the data changes.
Rearranging knowledge
With the previous code and flow chart combination, I think for Vue source analysis should be more understanding, so we again to comb the knowledge points we learn. It is still combined with the following flow chart:
Compile
Parse command
{{xxx}}
Initialize the view
Subscribe to data changes and bind update functions
Watcher
Updater
Of course, the premise of creating Watcher was to do data hijacking to listen for all properties, so the observer.js file was created. In the GET method, the Dep needs to be notified of the change, so we need to associate the dependency collection of the Dep and add the subscriber Watcher (which was created when Complie subscribed to the data change and bound the update function). At this point, the Dep subscriber has many watchers, and there should be as many watchers as there are properties.
So, let’s take a simple example to walk through the above flow chart:
If there is an A :1 in the original data and we update it to a:10, the set method will be triggered because of the data hijacking and listening for all attributes of our data. In the set method, the Dep subscriber will be notified of the change. The relevant Watcher is then notified to trigger the update function to update the view. These subscriber Watcher are already created when Complie subscribes to data changes and binds the update function.
View -> Data
Now that we’re done with the data-driven view, let’s go through the process of updating the data with changes to the view to actually implement two-way data binding.
The model module in our complieUtil directive processing collection simply binds us to an input event for the current node. We can get the value of the current input field by calling e.target.value. It then compares the old and new values to see if they are the same, and if they are different, it needs to be updated, calling the setVal method (see the code below).
model(node,expr,vm){
let value = this.getVal(expr,vm)
// Bind update function to update view changes when subscription data changes
// Data ==> View
new Watcher(vm,expr,(newVal)=>{
this.updater.modelUpdater(node,newVal)
})
// View == data
node.addEventListener('input',(e)=>{
var newValue = e.target.value
if(value == newValue) return
/ / set the value
this.setVal(expr,vm,newValue)
value = newValue
})
this.updater.modelUpdater(node,value)
},
Copy the code
There is no difference between setVal and getVal except that set has an inputVal. They all find the bottom key and update the value.
getVal(expr,vm){
return expr.split('. ').reduce((data,currentVal) = >{
// console.log(currentVal)
return data[currentVal]
},vm.$data)
},
setVal(expr,vm,inputVal){
return expr.split('. ').reduce((data,currentVal) = >{
data[currentVal] = inputVal
},vm.$data)
},
Copy the code
Update bug: We left out the binding Watcher step for the V-text instruction above.
text(node,expr,vm){
let value;
if(expr.indexOf('{{')! = =- 1){
value = expr.replace(/ \ {\ {(. +?) \}\}/g, (... args)=>{// Bind watcher to update the view
new Watcher(vm,args[1], () = > {this.updater.textUpdater(node,this.getContentVal(expr,vm))
// console.log(expr);
})
return this.getVal(args[1],vm); })}else{ // it is possible that v-text='obj. Name 'v-text=' MSG'
value = this.getVal(expr,vm)
// Bind watcher to update the view
new Watcher(vm,expr,(newVal)=>{
this.updater.textUpdater(node,newVal)
// console.log(expr);})}this.updater.textUpdater(node,value)
},
Copy the code
Finally, when we change the value in the input field, we find that the other nodes are also changing, which means that our data has been modified, and the relevant subscriber has triggered the UPDATE method, enabling bidirectional binding!
To implement proxy
When we use VUE, we can usually get data directly from vm. MSG, because vue source code has a layer of proxy. This means that all value operations on the data fetch operation VM are delegated to vm.$data.
class myVue{
constructor(options){
this.$el = options.el
this.$data = options.data
this.$options = options
if(this.$el){
// 1. Implement data observer
new Observer(this.$data)
// 2. Implement instruction parser
new Compile(this.$el,this)
// 3. Implement proxy proxy
this.proxyData(this.$data)
}
// console.log(this)
}
proxyData(data){
for(const key in data){
Object.defineProperty(this,key,{
get(){
return data[key]
},
set(newVal){
data[key] = newVal
}
})
}
}
}
Copy the code
For example, if we bind a sayHi() event to a button and set the proxy to a layer, we don’t need to change our data by this.$data.person.name. We can get our data directly from this.person.name.
methods: {
sayHi() {
this.person.name = 'free and natural'
//this.$data.person.name = 'Chaoyi'
console.log(this)}}Copy the code
Big factory interview question
Explain your understanding of MVVM responsiveness
Vue. js uses data hijacking combined with publiser-subscriber mode. It hijacks the getter and setter of each attribute through Object.defineProperty(), releases messages to subscribers when data changes, and triggers corresponding listening callback.
MVVM, as the entry of data binding, integrates Observer, Compile and Watcher, uses Observer to monitor its model data changes, and uses Compile to parse and Compile template instructions. Finally, Watcher is used to build a communication bridge between Observer and Compile to achieve data change -> view update; View Interactive Changes (INPUT) -> Bidirectional binding effect of data model changes.
Initially, we implement Compile to parse the instructions, find {{XXX}}, instructions, events, bindings, etc., and then initialize the view. But there is one thing left undone, which is that when the data changes, before updating the data, we have to subscribe to the data change, bind the update function, and then we need to add the subscriber Watcher. When the subscriber observes a change in the data, the Updater is triggered to update the view.
Of course, the premise of creating Watcher was to do data hijacking to listen for all properties, so the observer.js file was created. In the GET method, the Dep needs to be notified of the change, so we need to associate the dependency collection of the Dep and add the subscriber Watcher (which was created when Complie subscribed to the data change and bound the update function). At this point, the Dep subscriber has many watchers, and there should be as many watchers as there are properties.
So, let’s take a simple example to walk through the above flow chart:
If there is an A :1 in the original data and we update it to a:10, the set method will be triggered because of the data hijacking and listening for all attributes of our data. In the set method, the Dep subscriber will be notified of the change. The relevant Watcher is then notified to trigger the update function to update the view. These subscriber Watcher are already created when Complie subscribes to data changes and binds the update function.
Summary and q&A
Finally is finished writing this long article, the number of words also reached nearly 1W8. Through learning Vue MVVM source code, for Vue two-way data binding this piece of understanding is also more profound. Of course, the code written in this article is relatively simple, but also referred to the big guy’s blog and code, at the same time, there are also deficiencies and a small part of the function is not realized, compared to the source code, there are still a lot of optimization and reconfigurable, so also welcome friends to PR. Let’s implement MVVM.
Github: MVVM video learning: Vue source code parsing