I believe that as long as you go to interview vUE, will be asked about vUE two-way data binding, if you say MVVM is a view model model view, as long as the data changes the view will also be updated! Then you are not far away from being passed!
Video has been recorded, address (www.bilibili.com/video/BV1qJ…)
Several ways to achieve bidirectional binding
At present, several mainstream MVC (VM) frameworks have implemented one-way data binding, and the two-way data binding I understand is nothing more than to add change(input) events to input elements (input, Textare, etc.) on the basis of one-way binding to dynamically modify the model and view, without much depth. So don’t worry too much about the unidirectional or bidirectional bindings that are implemented.
There are several ways to implement data binding:
Publish-subscriber mode (backbone.js)
Dirty value checking (Angular.js)
Data hijacking (vue.js)
Set (‘property’, value) is used to update the data. The default value is the value (‘property’, value). The default value is the value (‘property’)
This is now too low. We would rather update the data using the vm.property = value method and automatically update the view, so there are two ways to do this
Dirty value check: Angular. Js uses dirty value detection to determine whether or not to update the view by comparing data changes. The simplest way to detect changes is to periodically poll data with setInterval(). Something like this:
- DOM events, such as the user entering text, clicking a button, etc. ( ng-click )
- XHR response event ($HTTP)
- Browser Location change event ($Location)
- The Timer event (interval )
- performapply()
Data hijacking: Vue.js hijacks the setters and getters of each property through object.defineProperty () in the mode of data hijacking combined with publist-subscriber mode, publishes messages to subscribers when data changes and triggers the corresponding listening callback.
Principle of MVVM
The core method of Vue response principle is to hijack attributes through object.defineProperty () to achieve the purpose of monitoring data changes. Undoubtedly, this method is one of the most important and basic contents in this paper
In order to achieve MVVM bidirectional binding, you must implement the following:
- 1. Implement a data listener Observer, which can monitor all properties of data objects and get the latest value if there is a change and notify subscribers
- 2. Implement a 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
- 3. Implement a Watcher that acts as a bridge between the Observer and Compile, subscribing to and receiving notification of each attribute change, executing the corresponding callback function for the instruction binding, and updating the view
- 4, MVVM entry function, integration of the above three
Let’s look at the previous vue features
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<h2>{{obj.name}}--{{obj.age}}</h2>
<h2>{{obj.age}}</h2>
<h3 v-text='obj.name'></h3>
<h4 v-text='msg'></h4>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-html='htmlStr'></div>
<div v-html='obj.fav'></div>
<input type="text" v-model='msg'>
<img v-bind:src="imgSrc" v-bind:alt="altTitle">
<button v-on:click='handlerClick'>Button 1</button>
<button v-on:click='handlerClick2'>Button 2</button>
<button @click='handlerClick2'>Button 3</button>
</div>
<script src="./vue.js"></script>
<script>
let vm = new MVue({
el: '#app'.data: {
obj: {
name: 'Little Horse'.age: 19.fav:'< h4 > front-end Vue < / h4 >'
},
msg: 'How MVVM is implemented'.htmlStr:"<h3>hello MVVM</h3>".imgSrc:'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1568782284688&di=8635d17d550631caabfeb4306b5d76fa&i mgtype=0&src=http%3A%2F%2Fh.hiphotos.baidu.com%2Fimage%2Fpic%2Fitem%2Fb3b7d0a20cf431ad7427dfad4136acaf2fdd98a9.jpg'.altTitle:'eyes'.isActive:'true'
},
methods: {
handlerClick() {
alert(1);
console.log(this);
},
handlerClick2(){
console.log(this);
alert(2)}}})</script>
</body>
</html>
Copy the code
Implement the instruction parser Compile
Implement a directive parser Compile, scan and parse the instructions of each element node, replace data according to the directive template, bind the corresponding update function, add subscribers who listen to data, and once the data changes, receive notification and update the view, as shown in the figure:
Initialize the
New MVue. Js
class MVue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
// Save the options parameter, which will be used later to process the data
this.$options = options;
// Compile the template if the root element exists
if (this.$el) {
// 1. Compile an instruction parser
new Compile(this.$el, this)}}}class Compile{
constructor(el,vm) {
// Determine if the el parameter is an element node, if it is directly assigned, and if it is not, get the assignment
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
}
isElementNode(node){
// Check whether it is an element node
return node.nodeType === 1}}Copy the code
So the outside world can do this
let vm = new Vue({
el:'#app'
})
//or
let vm = new Vue({
el:document.getElementById('app')})Copy the code
Optimize compilation using document fragmentation
<h2>{{obj.name}}--{{obj.age}}</h2>
<h2>{{obj.age}}</h2>
<h3 v-text='obj.name'></h3>
<h4 v-text='msg'></h4>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-html='htmlStr'></div>
<div v-html='obj.fav'></div>
<input type="text" v-model='msg'>
<img v-bind:src="imgSrc" v-bind:alt="altTitle">
<button v-on:click='handlerClick'>Button 1</button>
<button v-on:click='handlerClick2'>Button 2</button>
<button @click='handlerClick2'>Button 3</button>
Copy the code
Next, find the value of the child element, such as obj.name,obj.age,obj.fav, find obj, find fav, get the value in the data and replace it
However, we have to think of a problem here. Every time we find a data replacement, we have to re-render it, which may cause backflow and redrawing of the page. Therefore, the best way is to put the above elements in memory and replace them after the operation is completed in memory.
class Compile {
constructor(el, vm) {
// Determine if the el parameter is an element node, if it is directly assigned, and if it is not, get the assignment
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// Because every time a match is made, the page will backflow and redraw, affecting the performance of the page
// So you need to create document fragments for caching, reducing page reflux and redrawing
// 1. Obtain the document fragmentation object
const fragment = this.node2Fragment(this.el);
// console.log(fragment);
// 2. Compile the template
// 3. Add all the contents of the child elements to the root element
this.el.appendChild(fragment);
}
node2Fragment(el) {
const fragment = document.createDocumentFragment();
let firstChild;
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild);
}
return fragment
}
isElementNode(el) {
return el.nodeType === 1; }}Copy the code
At this time, you will find that the page has not changed any before, but through the Fragment processing, optimize the page rendering performance
Compiling templates
// The class to compile the data
class Compile {
constructor(el, vm) {
// Determine if the el parameter is an element node, if it is directly assigned, and if it is not, get the assignment
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// Because every time a match is made, the page will backflow and redraw, affecting the performance of the page
// So you need to create document fragments for caching, reducing page reflux and redrawing
// 1. Obtain the document fragmentation object
const fragment = this.node2Fragment(this.el);
// console.log(fragment);
// 2. Compile the template
this.compile(fragment)
// 3. Add all the contents of the child elements to the root element
this.el.appendChild(fragment);
}
compile(fragment) {
// 1. Obtain the child node
const childNodes = fragment.childNodes;
// 2. Iterate over the child nodes
[...childNodes].forEach(child= > {
// 3. Handle different types of child nodes
if (this.isElementNode(child)) {
// is an element node
// Compile the element nodes
// console.log(' I am the element node ',child);
this.compileElement(child);
} else {
// console.log(' I am a text node ',child);
this.compileText(child);
// All that is left is the text node
// Compile the text node
}
// 4. Remember to iterate over the child elements recursively
if (child.childNodes && child.childNodes.length) {
this.compile(child); }})}// Compile the text method
compileText(node) {
console.log('Compile text')
}
node2Fragment(el) {
const fragment = document.createDocumentFragment();
// console.log(el.firstChild);
let firstChild;
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild);
}
return fragment
}
isElementNode(el) {
return el.nodeType === 1; }}Copy the code
Next, render according to the type of the different child elements
Compile the element
compileElement(node) {
// Get all properties of this node
const attributes = node.attributes;
// Iterate over the attributes
[...attributes].forEach(attr= > {
const { name, value } = attr; //v-text v-model v-on:click @click
// See if the current name is a directive
if (this.isDirective(name)) {
// Perform an operation on v-text
const [, directive] = name.split(The '-'); //text model html
// v-bind:src
const [dirName, eventName] = directive.split(':'); // Handle v-ON :click
// Update the data
compileUtil[dirName] && compileUtil[dirName](node, value, this.vm, eventName);
// Remove attributes from the current element
node.removeAttribute('v-' + directive);
}else if(this.isEventName(name)){
// Handles the event in this case @click
let [,eventName] = name.split(The '@');
compileUtil['on'](node, value, this.vm, eventName)
}
})
}
// If @click is the name of the event
isEventName(attrName){
return attrName.startsWith(The '@')}// Check whether it is an instruction
isDirective(attrName) {
return attrName.startsWith('v-')}Copy the code
Compile the text
// Compile the text method
compileText(node) {
const content = node.textContent;
// Match the contents of {{XXX}}
if (/ \ {\ {(. +?) \} \} /.test(content)) {
// Process text nodes
compileUtil['text'](node, content, this.vm)
}
}
Copy the code
You’ll also find out about compileUtil what the hell is that? The actual compilation operation I’m going to put into this object, and I’m going to do different things depending on the instructions. For example, V-text is for text, V-HTML is for HTML elements, and V-Model is for form data…..
So we initialize the view with the updater function in the current object compileUtil
Process elements/Process text/Process events….
const compileUtil = {
// The method to get the value
getVal(expr, vm) {
return expr.split('. ').reduce((data, currentVal) = > {
return data[currentVal]
}, vm.$data)
},
getAttrs(expr,vm){
},
text(node, expr, vm) { //expr might be {{object.name}}--{{object.age}}
let val;
if (expr.indexOf('{{')! = =- 1) {
//
val = expr.replace(/ \ {\ {(. +?) \}\}/g, (... args) => {return this.getVal(args[1], vm); })}else{ V -text='obj. Name 'v-text=' MSG'
val = this.getVal(expr,vm);
}
this.updater.textUpdater(node, val);
},
html(node, expr, vm) {
// HTML processing is very simple. You can just call the update function
let val = this.getVal(expr,vm);
this.updater.htmlUpdater(node,val);
},
model(node, expr, vm) {
const val = this.getVal(expr,vm);
this.updater.modelUpdater(node,val);
},
// Handle the event
on(node, expr, vm, eventName) {
// Get the event function
let fn = vm.$options.methods && vm.$options.methods[expr];
// Add the event because we don't need to worry about this pointing when using vue, because the source code internally handles this pointing for us
node.addEventListener(eventName,fn.bind(vm),false);
},
// Bind properties The simple properties are already handled with the class name and the style of binding is a little bit more complicated because the corresponding value could be an object or an array and you can try to write it according to your ability
bind(node,expr,vm,attrName){
let attrVal = this.getVal(expr,vm);
this.updater.attrUpdater(node,attrName,attrVal);
},
updater: { attrUpdater(node, attrName, attrVal){ node.setAttribute(attrName,attrVal); }, modelUpdater(node,value){ node.value = value; }, textUpdater(node, value) { node.textContent = value; }, htmlUpdater(node,value){ node.innerHTML = value; }}}Copy the code
This is done: we implement a compiler that parses instructions and initializes views with an updater
Implement a data listener Observer
Let’s do it we know that we can use obeject.defineProperty () to listen for property changes so we recurse through the data object that needs to observe, including the properties of the child property object, You put a setter and a getter on it so that if you assign a value to this object, it triggers the setter, so you can listen for changes. The relevant code could look like this:
//test.js
let data = {name: 'kindeng'};
observe(data);
data.name = 'dmq'; Kindeng --> DMQ --> DMQ
function observe(data) {
if(! data ||typeofdata ! = ='object') {
return;
}
// Fetch all attributes to iterate
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
});
};
function defineReactive(data, key, val) {
observe(val); // Listen for child attributes
Object.defineProperty(data, key, {
enumerable: true./ / can be enumerated
configurable: false.// No more define
get: function() {
return val;
},
set: function(newVal) {
console.log('Hahaha, I'm listening for a change.', val, '- >', newVal); val = newVal; }}); }Copy the code
Looking at the diagram, what we are going to implement next is a data listener Observer that listens for all properties of the data object, notifishes the dependent collection object (Dep) of changes, and notifishes the subscriber (Watcher) to update the view
// Create a data listener to hijack and listen for all data changes
class Observer{
constructor(data) {
this.observe(data);
}
observe(data){
// Hijack and listen if the current data is an object
if(data && typeof data === 'object') {// Iterate over the properties of the object to listen
Object.keys(data).forEach(key= >{
this.defineReactive(data,key,data[key]);
})
}
}
defineReactive(obj,key,value){
// Loop recursively to look at all levels of data
this.observe(value);// So obj can also be observed
Object.defineProperty(obj,key,{
get(){
return value;
},
set:(newVal) = >{
if(newVal ! == value){// If the object is directly modified by the outside world, the new value is observed again
this.observe(newVal);
value = newVal;
// Notify changesdep.notify(); }}}Copy the code
So that we can monitor the change of each data, then after listening to change is how to notify the subscriber, so we need to implement a message subscriber, is very simple, maintain an array, used to gather the subscriber, trigger notify data changes, then call the subscriber the update method, code after improvement is this:
Create a Dep
- Add a subscriber
- Define the method of advice
class Dep{
constructor() {
this.subs = []
}
// Add subscribers
addSub(watcher){
this.subs.push(watcher);
}
// Notify changes
notify(){
// The observer has an update method to update the view
this.subs.forEach(w= >w.update()); }}Copy the code
Although we have created the Observer, the Dep(subscriber), then the question arises, who are the subscribers? How do I add a subscriber to a subscriber?
Const dep = new dep (); Is defined within defineReactive, so to add subscribers via dep, you have to do it inside the closure, so we can do it inside getOldVal:
Implement a Watcher
It acts as a bridge between the Observer and Compile, subscribes to and receives notification of each attribute change, executes the corresponding callback function for the instruction binding, and updates the view
As long as the thing is done:
1. Add yourself to the property subscriber (DEP) when it instantiates itself
2. You must have an update() method of your own
3. When dep.notify() is notified of property changes, you can call your update() method and trigger the callback bound in Compile, then you are done.
//Watcher.js
class Watcher{
constructor(vm,expr,cb) {
// Watch for changes in the new and old values and update the view if there are changes
this.vm = vm;
this.expr = expr;
this.cb = cb;
// Save the old value first
this.oldVal = this.getOldVal();
}
getOldVal(){
Dep.target = this;
let oldVal = compileUtil.getVal(this.expr,this.vm);
Dep.target = null;
return oldVal;
}
update(){
// Dep will notify the observer to update the view when the data changes during the update operation
let newVal = compileUtil.getVal(this.expr, this.vm);
if(newVal ! = =this.oldVal){
this.cb(newVal); }}}//Observer.js
defineReactive(obj,key,value){
// Loop recursively to look at all levels of data
this.observe(value);// So obj can also be observed
const dep = new Dep();
Object.defineProperty(obj,key,{
get(){
// Subscribe to the data change and add observers to the Dep
Dep.target && dep.addSub(Dep.target);
return value;
},
/ /... omit})}Copy the code
When we modify some data, the data has changed, but the view has not been updated
When should we add the binding watcher
That is, we bind the update function to the watcher to update the view when the subscribed data changes
Modify the
// Compile the template tool class
const compileUtil = {
// The method to get the value
getVal(expr, vm) {
return expr.split('. ').reduce((data, currentVal) = > {
return data[currentVal]
}, vm.$data)
},
/ / set the value
setVal(vm,expr,val){
return expr.split('. ').reduce((data, currentVal, index, arr) = > {
return data[currentVal] = val
}, vm.$data)
},
// Get new values for {{a}}--{{b}}
getContentVal(expr, vm) {
return expr.replace(/ \ {\ {(. +?) \}\}/g, (... args) => {return this.getVal(args[1], vm);
})
},
text(node, expr, vm) { //expr might be {{object.name}}--{{object.age}}
let val;
if (expr.indexOf('{{')! = =- 1) {
//
val = expr.replace(/ \ {\ {(. +?) \}\}/g, (... args) => {// Bind watcher to update the view
new Watcher(vm,args[1], () = > {this.updater.textUpdater(node,this.getContentVal(expr, vm));
})
return this.getVal(args[1], vm); })}else{ V -text='obj. Name 'v-text=' MSG'
val = this.getVal(expr,vm);
}
this.updater.textUpdater(node, val);
},
html(node, expr, vm) {
// HTML processing is very simple. You can just call the update function
let val = this.getVal(expr,vm);
// Bind watcher when the subscription data changes, thus updating the function
new Watcher(vm,expr,(newVal)=>{
this.updater.htmlUpdater(node, newVal);
})
this.updater.htmlUpdater(node,val);
},
model(node, expr, vm) {
const val = this.getVal(expr,vm);
// Bind the update function to update views when data changes
// Data ==> View
new Watcher(vm, expr, (newVal) => {
this.updater.modelUpdater(node, newVal);
})
View ==> Data
node.addEventListener('input',(e)=>{
/ / set the value
this.setVal(vm,expr,e.target.value);
},false);
this.updater.modelUpdater(node,val);
},
// Handle the event
on(node, expr, vm, eventName) {
// Get the event function
let fn = vm.$options.methods && vm.$options.methods[expr];
// Add the event because we don't need to worry about this pointing when using vue, because the source code internally handles this pointing for us
node.addEventListener(eventName,fn.bind(vm),false);
},
// Bind properties The simple properties are already handled with the class name and the style of binding is a little bit more complicated because the corresponding value could be an object or an array and you can try to write it according to your ability
bind(node,expr,vm,attrName){
let attrVal = this.getVal(expr,vm);
this.updater.attrUpdater(node,attrName,attrVal);
},
updater: { attrUpdater(node, attrName, attrVal){ node.setAttribute(attrName,attrVal); }, modelUpdater(node,value){ node.value = value; }, textUpdater(node, value) { node.textContent = value; }, htmlUpdater(node,value){ node.innerHTML = value; }}}Copy the code
The proxy agent
When using vue, we can usually get data directly from vm. MSG, because the vue source code internally does a layer of proxy. In other words, the value operation on the DATA acquisition vm is represented on the VM.$data
class Vue {
constructor(options) {
this.$data = options.data;
this.$el = options.el;
this.$options = options
// Compile the template if the root element exists
if (this.$el) {
// 1. Implement a data listener Observe
// Can listen to all attributes of the data object, if there is a change to get the latest value and notify subscribers
// object.definerProperty ()
new Observer(this.$data);
$data = $data = $data = $data
this.proxyData(this.$data);
// 2. Implement an instruction parser, Compile
new Compiler(this.$el, this); }}// Act as an agent
proxyData(data){
for (const key in data) {
Object.defineProperty(this,key,{
get(){
returndata[key]; }, set(newVal){ data[key] = newVal; }}}Copy the code
The interview questions
Explain your understanding of vUE’s MVVM responsive principle
Vue.js hijacks the setters and getters of each property through object.defineProperty () in the mode of data hijacking combined with publist-subscriber mode, publishes messages to subscribers when data changes and triggers the corresponding listening callback.
As the entry point of data binding, MVVM integrates Observer, Compile and Watcher. It listens to its own model data changes through Observer and parses the compilation template instructions through Compile. Finally, use Watcher 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
With that picture above, it’s hard not to get hired