The code implementation comes from Everest open class MVVM principle. For the record, write the code a few times to deepen your understanding of MVVM.
1. Concept of MVVM
Model-view-viewmodel is implemented by data hijacking + publish/subscribe mode.
MVVM is a design idea. Model represents the data Model in which the business logic for data modification and manipulation can be defined. The VIEW represents the UI component and is responsible for transforming the data model into the UI presentation. It makes data binding declarations, directive declarations, and event binding declarations. ; The viewModel is an object that synchronizes the View and the Model. In the MVVM framework, there is no direct relationship between the View and the Model; they interact through the viewModel. MVVM does not require manual manipulation of the DOM; it only needs to focus on business logic. The difference between MVVM and MVC is that MVVM is data-driven while MVC is DOM-driven. The advantage of MVVM is that it does not need to operate a large number of DOM, and does not need to pay attention to the relationship between model and view, while MVC needs to manually update the view when the model changes. A large number of DOM operations degrade page rendering performance, slow down the loading speed, and affect user experience.
2. Advantages of MVVM
- 1. Low coupling there is no direct relationship between view and model, and two-way data binding is completed through viewModel.
- 2. Reusability Components are reusable. You can put some data logic into a viewModel and have many views reuse it.
- Independent developers focus on the viewModel, designers focus on the View.
- Testability ViewModels exist to help developers write better test code.
3. Disadvantages of MVVM
- 1. Bugs are difficult to debug, because the data is bi-directional bound, so the problem may be in the View or the Model. It is difficult to locate the original bug, and the code in the View cannot be debugged, which makes it difficult to locate bugs.
- 2. Models in a large module can be very large, and keeping them in memory for a long time can affect performance.
- 3. For large graphical applications, the more view states, the more expensive the viewModel will be to build and maintain.
4. Bidirectional binding of MVVM
At the heart of MVVM are data hijacking, data brokering, data compilation, and the “publish/subscribe pattern.”
Data hijacking – adding get and set hook functions to an object property.
- 1, look at the Object, add object.defineProperty to the Object
- Vue does not add a get or set hook function to a nonexistent property.
- 3. Depth response. Loop recursively through the properties of data, adding get and set hook functions to the properties.
- Each time a new object is assigned (i.e. the set hook function is called), the new object is data hijacked (defineProperty).
// Use the set and get hook functions to hijack data
function defineReactive(data){
Object.keys(data).forEach(key= >{
const dep=new Dep();
let val=data[key];
this.observe(val);// Deep listening
Object.defineProperty(data,key,{
get(){
// Add a subscriber watcher (add a subscriber for each data attribute so that you can listen for changes in the data attribute in real time)
Dep.target&&dep.addSub(Dep.target);
// Return the initial value
return val;
},set(newVal){
if(val! ==newVal){ val=newVal;// Notify subscriber of data change (release)
dep.notify();
returnnewVal; }}})})}Copy the code
2. Data broker
Mount data from data, Methods,compted to the VM instance. Instead of having to go through mVVM._data.b every time we get data, we can go directly to MVVM.b.a.
class MVVM{
constructor(options){
this.$options=options;
this.$data=options.data;
this.$el=options.el;
this.$computed=options.computed;
this.$methods=options.methods;
// Hijack data to listen for changes
new Observer(this.$data);
// Mount data to the VM instance
this._proxy(this.$data);
// Mount the method to the VM as well
this._proxy(this.$methods);
// Mount the data attributes to the VM instance
Object.keys(this.$computed).forEach(key= >{
Object.defineProperty(this,key,{
get(){
return this.$computed[key].call(this);// Enter the VM into computed}})})// Compile the data
new Compile(this.$el,this)};// Private method for data hijacking
_proxy(data){
Object.keys(data).forEach(key= >{
Object.defineProperty(this,key,{
get(){
return data[key]
}
})
})
}
}
Copy the code
3. Data compilation
Replace {{}}, V-model, V-HTML,v-on with data.
class Compile{
constructor(el,vm){
this.el=this.isElementNode(el)? el:document.querySelector(el);
this.vm=vm;
let fragment=this.nodeToFragment(this.el);
// Compile the node
this.compile(fragment);
// Add the compiled code to the page
this.el.appendChild(fragment);
};
// The core compilation method
compile(node){
const childNodes=node.childNodes;
[...childNodes].forEach(child= >{
if(this.isElementNode(child)){
this.compileElementNode(child);
// If it is an element node, it must be compiled recursively
this.compile(child);
}else{
this.compileTextNode(child); }})};// Compile the element nodes
compileElementNode(node){
const attrs=node.attributes;
[...attrs].forEach(attr= >{
//attr is an object
let {name,value:expr}=attr;
if(this.isDirective(name)){
// Only the case of V-HTML and V-model is considered
let [,directive]=name.split("-");
// Consider the v-on:click case
let [directiveName,eventName]=directive.split(":");
// Call different instructions to compile
CompileUtil[directiveName](node,this.vm,expr,eventName); }})};// Compile the text node
compileTextNode(node){
const textContent=node.textContent;
if(/ \ {\ {(. +?) \} \} /.test(textContent)){
CompileUtil["text"](node,this.vm,textContent)
}
};
// Convert element nodes to document fragments
nodeToFragment(node){
// Cache the element nodes and replace them after compiling
let fragment=document.createDocumentFragment();
let firstChild;
while(firstChild=node.firstChild){
fragment.appendChild(firstChild);
}
return fragment;
};
// Check whether it is an element node
isElementNode(node){
return node.nodeType===1;
};
// Check whether it is an instruction
isDirective(attr){
return attr.includes("v-"); }}// The object that holds the compiled method
CompileUtil={
// Get the value from the property in data, triggering the observer's GET hook
getVal(vm,expr){
const data= expr.split(".").reduce((initData,curProp) = >{
// Triggers the observer's get hook
return initData[curProp];
},vm)
return data;
},
// Triggers the observer's set hook
setVal(vm,expr,value){
expr.split(".").reduce((initData,curProp,index,arr) = >{
if(index===arr.length- 1){
initData[curProp]=value;
return;
}
return initData[curProp];
},vm)
},
getContentValue(vm,expr){
const data= expr.replace(/ \ {\ {(. +?) \}\}/g, (... args)=>{return this.getVal(vm,args[1]);
});
return data;
},
model(node,vm,expr){
const value=this.getVal(vm,expr);
const fn=this.updater["modelUpdater"];
fn(node,value);
// Listen to the input event of the input, implement data response
node.addEventListener('input',e=>{
const value=e.target.value;
this.setVal(vm,expr,value);
})
// Watch the data (expr) change and add watcher to the subscriber queue
new Watcher(vm,expr,newVal=>{
fn(node,newVal);
});
},
text(node,vm,expr){
const fn=this.updater["textUpdater"];
// Replace person. James in {{person.name}} with James
const content=expr.replace(/ \ {\ {(. +?) \}\}/g, (... args)=>{// Observe the change of data
new Watcher(vm,args[1], () = > {// this.getContentValue(vm,expr) gets the compiled value of textContent
fn(node,this.getContentValue(vm,expr))
})
return this.getVal(vm,args[1]);
})
fn(node,content);
},
html(node,vm,expr){
const value=this.getVal(vm,expr);
const fn=this.updater["htmlUpdater"];
fn(node,value);
new Watcher(vm,expr,newVal=>{
// Replace the data again after the change
fn(node,newVal);
})
},
on(node,vm,expr,eventName){
node.addEventListener(eventName,e=>{
// Call to pass the VM instance (this) to the methodvm[expr].call(vm,e); })},updater:{ modelUpdater(node,value){ node.value=value }, htmlUpdater(node,value){ node.innerHTML=value; }, textUpdater(node,value){ node.textContent=value; }}}Copy the code
4. Publish and subscribe
Publish subscribe is primarily based on an array relationship, with subscribe being put into the function (that is, adding subscribers to the subscription queue) and publish being performed by the function in the array (informing subscribers to perform the corresponding action when the data changes). Messages are published and subscribed in the observer’s data binding — subscribed when the get hook function is called (subscribed when the data is compiled via new Watcher()), and published when the set hook function is called.
// The message manager (publisher) notifying subscribers to take action when the data changes
class Dep{
constructor() {this.subs=[];
};
/ / subscribe
addSub(watcher){
this.subs.push(watcher);
};
/ / release
notify(){
this.subs.forEach(watcher= >watcher.update()); }}// A subscriber is an observer of changes in data
class Watcher{
constructor(vm,expr,cb){
this.vm=vm;
this.expr=expr;
this.cb=cb;
this.oldValue=this.get();
};
get(){
Dep.target=this;
const value=CompileUtil.getVal(this.vm,this.expr);
Dep.target=null;
return value;
};
update(){
const newVal=CompileUtil.getVal(this.vm,this.expr);
if(this.oldValue! ==newVal){this.cb(newVal); }}}/ / observer
class Observer{
constructor(data){
this.observe(data);
};
// Make the data responsive
observe(data){
if(data&&typeof data==="object") {this.defineReactive(data)
}
};
defineReactive(data){
Object.keys(data).forEach(key= >{
const dep=new Dep();
let val=data[key];
this.observe(val);// Deep listening
Object.defineProperty(data,key,{
get(){
// Add a subscriber watcher (add a subscriber for each data attribute so that you can listen for changes in the data attribute in real time)
Dep.target&&dep.addSub(Dep.target);
// Return the initial value
return val;
},set(newVal){
if(val! ==newVal){ val=newVal;// Notify subscriber of data change (release)
dep.notify();
returnnewVal; }}})})})Copy the code