MVVM = M data Model (Model) + VM ViewModel (View) + V View layer (View)
For more conceptual knowledge, please go to: Vue MVVM Understanding and Implementation
MVVM process analysis
As shown in the figure above: In Vue MVVM design, we mainly for Compile (template compilation), Observer (data hijacking), Watcher (data monitoring) and Dep (publish subscription) several parts to achieve.
Steps to implement bidirectional data binding:
1. Compile an instruction parser, scan and parse the instructions of each element node, replace data according to the instruction template, and bind the corresponding update function
2. Implement a data listener Observer, which can listen to all the attributes of the data object. If there is any change, it can get the latest value and notify the subscriber (Dep).
3. Implement a Watcher that acts as a bridge between the Observer and Compile, subscribing to and receiving notification of each property change, and executing the corresponding callback function (publish) of the instruction binding to update the view
4.MVVM entry function, integration of the above three
Simple flow chart:
Reference article:
- Vue data hijacking
- Observer vs. publish subscribe
MVVM principle implementation case
Do your homework
Note: the following comments are new code, until the end of the article, some of the written code I will not stick, look at the structure to know where to write
Note: Don’t delete any of the code I haven’t attached, just focus on the code I’ve added and commented
Create a new index.html file to import the MVVM we wrote, which can be used to test the implementation principle of creating a new vuemvm. js file to write MVVM
Index.html code:
<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>MVVM</title> </head> <body> <div id="app"> <input type="text" v-model='school.name'> <div>{{school.name}}</div> <div>{{school.age}}</div> <ul> <li>1</li> <li>2</li> </ul> </div> <! -- < script SRC = "https://cdn.bootcss.com/vue/2.6.10/vue.common.dev.js" > < / script > -- > < script SRC = "VueMVVM. Js" > < / script > <script> let vm = new Vue({ el: "#app", data: { school: { name: "HuangHuai", age: 10 } }, computed: {} }) </script> </body> </html>Copy the code
Change the data, the view responds
In Vue, only one constructor named Vue is exposed. When used, a new Vue instance is passed, and then an options parameter is passed. The type is an object, including the scope el of the current Vue instance, the data bound to the template, and so on.
Constructor (el,vm){this.el = this.iselementNode (el)? el:document.querySelector(el) console.log(this.el); } isElementNode(node){return node.nodeType === 1; }} // To render a web page, form a DOM tree with two types of nodes: Class Vue{// As long as new Vue, Constructor (options){// Attach el and data to the MVVM instance. this.$data = options.data; $el {// If (this.$el){// If (this.$el,this)}}Copy the code
We can print out our index.html template:
Save the child nodes in the template to the document fragment (which creates a space in memory for HTML code) :
class Compiler{ constructor(el,vm){ this.el = this.isElementNode(el) ? El: document querySelector (el) / / pass node2fragment the template, Let fragment = this.node2fragment(this.el) // re-upload the replaced data to the page this.el.appendChild(fragment)} // define a method, Node2fragment (node){// Create a fragment to store our HTML template, note: They are detailed for storage of the let fragments = document. CreateDocumentFragment () / / the first child let firstChild; While (firstChild = node.firstChild){fragment.appendChild(firstChild)} // Return template return fragment; } isElementNode(node){ return node.nodeType === 1; }}Copy the code
Compile
Compile the template to determine whether it is an element node or a text node:
constructor(el,vm){ this.el = this.isElementNode(el) ? El: document querySelector (el) let fragments = this. Node2fragment (enclosing el) / / replace (compiled template) running the compiled using data to this.com (fragments) this.el.appendChild(fragment) } compile(node){ // console.log(node) // [input, div, div, Ul] // childNodes does not contain li. It only gets childNodes, not childNodes. // console.log(node.childnodes); NodeList(9) [text, input, text, div, text, div, text, ul, text] // node.childNodes a bunch of nodes: Let childNodes = node.childNodes; [...childNodes]. ForEach (child=>{// Determine the element node, If (this.iselementNode (child)){console.log(child+' is an element node '); if(this.iselementNode (child)){console.log(child+' is an element node '); }else{console.log(child+' is a text node '); }})}Copy the code
If it is an element node, find out if it is an instruction:
constructor(el,vm){ this.el = this.isElementNode(el) ? el:document.querySelector(el) let fragment = this.node2fragment(this.el) this.compile(fragment) This.el.appendchild (fragment)} isDirective(attrName){// As long as the prefix is v-, Return attrname.startswith ('v-')} // compileElement(node){let attributes = node.attributes; // Console. log(Attributes); // NamedNodeMap {0: type, 1: v-model, type: type, V-model: V-model, length: 2} // Convert pseudo-arrays to real arrays [...attributes]. //type="text" v-model="school.name" let {name,value} = attr; // console.log(name,value); If (this.isdirective (name)){console.log(name+" is a directive ") //v-model is a directive console.log(node); <input type="text" V-model ="school.name">}})} // Compile text node compileText(node){} compile(node){let childNodes = node.childNodes; //childNodes is a pseudo-array [...childNodes]. ForEach (child=>{if(this.iselementNode (child)){ This.pileelement (child); this.pileElement (child); This.piletext (child)}}else{this.piletext (child)}}Copy the code
Find the text node:
// Compile the text node compileText(node){// console.log(node); // Get all the text nodes let Content = node.textContent; // console.log(content); Let reg = /\{\{(.+?) \} \} /; {{}} reg.test(content) //// If content satisfies the re we wrote, return ture, Otherwise false if(reg.test(content)){// find the text node console.log(content) //{{school.name}} {{school.age}}}} compile(node){let childNodes = node.childNodes; [...childNodes]. ForEach (child=>{// child indicates each node. Call compileElement if(this.iselementNode (child)){this.pileElement (child); // If there are other nodes inside the child, This.compile (child)}else{// otherwise call compileText this.compile(child)}})}Copy the code
On the outside (on a par with class Compiler{} and class Vue{}) define an object that holds the different processing methods for different instructions:
// Write an object to CompilerUtil = {model(){console.log(' handle v-model directives '); }, text(){console.log(' handle v-text instructions '); }, HTML (){console.log(' handle v-html instructions '); }},Copy the code
We then use it in the method that compiles the element node:
compileElement(node){ let attributes = node.attributes; [...attributes].forEach(attr=>{ let {name,value} = attr; if(this.isDirective(name)){ // console.log(name); // let [,directive] = name.split('-'); // Let [,directive] = name.split('-'); // console.log(directive); // model // Fetch different directives fetch erutil [directive](); }})}Copy the code
Render data to element nodes:
Constructor (el,vm){this.vm = vm this.el = this.iselementNode (el)? el:document.querySelector(el) let fragment = this.node2fragment(this.el) this.compile(fragment) this.el.appendChild(fragment) } /* ... */ compileElement(node){ let attributes = node.attributes; Let {name,value:expr} = attr; let {name,value:expr} = attr; if(this.isDirective(name)){ let [,directive] = name.split('-'); CompilerUtil[directive](node,expr,this.vm); }})} /*... */ CompilerUtil = { getVal(vm,expr){ console.log(expr.split('.')); //["school", "name"] // console.log(vm); Return expr.split(".").reduce((data,current)=>{return data[current] },vm.$data); }, model(node,expr,vm){// node is an element node with instructions expr is an expression vm is a vue object // console.log(' handle V-model instructions '); // console.log(node); //<input type="text" v-model="school.name"> // console.log(expr); //school.name console.log(vm); Node.value = XXXX let value = this.getVal(vm,expr) // console.log(value); //HuangHuai // Display this value in the input box on the template let fn = this.updater['modelUpdater'] // Call fn method fn(node,value)}, Text (){console.log(' handle v-text instructions '); }, HTML (){console.log(' handle v-html instructions '); }, // Update data updater:{modelUpdater(node,value){node.value = value}, htmlUpdater(){},}Copy the code
This will render the data from the HTML template to the view
Render the contents of the text node (i.e. {{}}) :
compileText(node){ let content = node.textContent; let reg = /\{\{(.+?) \} \} /; reg.test(content) if(reg.test(content)){ // console.log(content); // {{school.name}} {{school.age}} // console.log(node); //"{{school.name}}" node is text node CompilerUtil['text'](node,content,this.vm)}} /*... */ CompilerUtil = { getVal(vm,expr){ return expr.split(".").reduce((data,current)=>{ return data[current] },vm.$data); }, model(node,expr,vm){ let value = this.getVal(vm,expr) let fn = this.updater['modelUpdater'] fn(node,value) }, Text (node,expr,vm){// console.log(' handle v-text instructions '); // console.log(node); //"{{school.name}}" // console.log(expr); //{{school.name}} // console.log(vm); Replace let content = expr. Replace (/\{\{(.+?)) \}\}/g,(... args)=>{ // console.log(args); / / / "{{school. Age}}", "school. The age," 0, "{{school. Age}}"] / / / / get data console. The log (enclosing getVal (vm, args [1])); //HuangHuai 10 return this.getVal(vm,args[1])}) let fn = this.updater['textUpdater'] fn(node,content)}, HTML (){console.log(' handle v-html instructions '); }, updater:{ modelUpdater(node,value){ node.value = value }, TextContent = value}, htmlUpdater(){}},}Copy the code
This allows the data to be rendered to the page instead of {{}}
Observer
However, at this point, the data is not yet responsive, and we need to make the data responsive, using data hijacking:
// New class Observer{constructor(data){// console.log(data) // school: {name: "HuangHuai", age: 100} this.observer(data)} // Make an object observer(data){// If data exists and the type is object if(data && typeof data) == 'object'){ // console.log(data); // School: {name: "HuangHuai", age: 10} //for in loop a JS object for(let key in data){// console.log(key); //school // console.log(data[key]) //{name: "HuangHuai", age: This.defindreactive (data,key,data[key])}}} defindReactive(obj,key,value){ This.observer (value) // If the data is an Object, you also need to change the data in the Object to object.defineProperty (obj,key,{// When you get school, Get get(){// console.log('get... '); Return value}, // Set set (newVal)=>{// If (newVal! = value){ // console.log("set..." ) this.observer(newVal) value = newVal } } }) } } class Vue{ constructor(options){ this.$el = options.el; this.$data = options.data; If (this.$el){new Observer(this.$data); Console. log(this.$data) new Compiler(this.$el,this)}}} console.log(this.$data) new Compiler(this.Copy the code
So if you look at that, you have get and you have set, and then the data becomes responsive
Dep, Watcher
It is not enough to make the data responsive, we need to change the data, let it automatically render to the page, then we need to use publish and subscribe, (here is a little changed, only paste the modified code, see the comment, add the comment is the new or changed).
/ / publish-subscribe observer contains the publish-subscribe pattern in the observer pattern / / publish-subscribe publish and subscribe / / is no connection between the observer (observer and the observed) contained in the observed observer / / store the observer Dep class Dep { constructor(){ this.subs = []; AddSub (watcher){this.subs.push(watcher) {this.subs.push(watcher) {this.subs.push(watcher) {this.subs.push(watcher) {this.subs.push(watcher) {this.subs.push(watcher) Constructor (vm,expr,cb){notify(){this.subs.foreach (watcher=>watcher.update())}} This. Vm = vm this.expr = expr this.cb = cb This.oldvalue = this.get()} get(){dep.target = this; Let value = compilerutil.getVal (this.vm,this.expr) // Dep.target = null; Return value} // When the status changes, Update (){let newVal = compilerUtil.getVal (this.vm,this.expr) if(newVal! == this.oldValue){ this.cb(newVal) } } } class Observer{ constructor(data){ this.observer(data) } observer(data){ if(data && typeof data == 'object'){ for(let key in data){ this.defindReactive(data,key,data[key]) } } } DefindReactive (obj,key,value){this.observer(value) let dep = new dep () object.defineProperty (obj,key,{get(){ // Use dep.target && dep.subs.push(dep.target) return value}, set (newVal)=>{if(newVal! = value){this.observer(newVal) value = newVal // Execute update method dep.notify()}}})}} /*... */ CompilerUtil = { getVal(vm,expr){ return expr.split(".").reduce((data,current)=>{ return data[current] },vm.$data); }, model(node,expr,vm){let fn = this.updater['modelUpdater'] New Watcher(vm,expr,(newVal)=>{fn(node,newVal)}) let value = this.getVal(vm,expr) fn(node,value)}, GetContentValue (vm,expr){return expr. Replace (/\{\{(.+?)) \}\}/g,(... args)=>{ return this.getVal(vm,args[1]) }) }, text(node,expr,vm){ let fn = this.updater['textUpdater'] let content = expr.replace(/\{\{(.+?) \}\}/g,(... The args) = > {/ / add a new observer Watcher (vm, args [1], () = > {fn (node, enclosing getContentValue (vm, expr)); }) return this.getVal(vm,args[1])}) fn(node,content)}, HTML (){console.log(' handle v-html instructions '); }, updater:{ modelUpdater(node,value){ node.value = value }, textUpdater(node,value){ node.textContent = value }, htmlUpdater(){} }, }Copy the code
In this case, whenever we change the data, the view changes accordingly
The view changes, the data changes
Now that we’ve only modified the data, the view responds to the corresponding content, but when we modify the view, the corresponding data doesn’t change,
We need to set the data to CompilerUtil:
CompilerUtil = { getVal(vm,expr){ return expr.split(".").reduce((data,current)=>{ return data[current] },vm.$data); }, // set data setVal(vm,expr,value){expr.split('.').reduce((data,current,index,arr)=>{ Arr = array ["school", "name"] // Arr = array ["school", "name"] // console.log(data,current,index,arr); if(index == arr.length-1){ // console.log(current); //name return data[current] = value } return data[current] },vm.$data) }, model(node,expr,vm){ let fn = this.updater['modelUpdater'] new Watcher(vm,expr,(newVal)=>{ fn(node,newVal) }) Node.addeventlistener ('input',(e)=>{let value = e.target.value // e.targe. value can get the contents of the input box // Call setVal, SetVal (vm,expr,value)}) let value = this.getVal(vm,expr) fn(node,value)}, getContentValue(vm,expr){ return expr.replace(/\{\{(.+?) \}\}/g,(... args)=>{ return this.getVal(vm,args[1]) }) }, text(node,expr,vm){ let fn = this.updater['textUpdater'] let content = expr.replace(/\{\{(.+?) \}\}/g,(... args)=>{ new Watcher(vm,args[1],()=>{ fn(node,this.getContentValue(vm,expr)); }) return this.getVal(vm,args[1])}) fn(node,content)}, HTML (){console.log(' handle v-html instructions '); }, updater:{ modelUpdater(node,value){ node.value = value }, textUpdater(node,value){ node.textContent = value }, htmlUpdater(){} }, }Copy the code
This is where we implement our two-way data binding
supplement
The agent (Proxy)
If we use official Settings, they are set with proxies. For example, when changing data, we must enter: $data.school.name=” XXX “; $data.school.name=” XXX “; $data.school.name=” XXX “;
Use official:
Use your own :(error reporting directly)
At this point, we also want to implement the official method, using the VM proxy $data (very simple) :
class Vue{ constructor(options){ this.$el = options.el; this.$data = options.data; $data this. ProxyVm ($data) new Compiler(this.$el,this)}} // Make vm proxy Data proxyVm(data){for(let key in data){// {school:{name:HuangHuai,age:10}} // school-- [object Object]-----[object Object] // console.log(key+"---"+data[key]+"-----"+data) Object.defineProperty(this,key,{ // vm.school get(){ return data[key] } }) } } }Copy the code
So you can do the official way
Computed attributes (computed)
Finally, one more function is to calculate attributes, which can be used as data:
Modify the HTML template code
<div id="app"> <input type="text" v-model='school.name'> <div>{{school.name}}</div> <div>{{school.age}}</div> <! - using computational properties - > {{getNewName}} < ul > < li > 1 < / li > < li > 2 < / li > < / ul > < / div > / / computation attribute computed: { getNewName(){ return this.school.name + "666"; }}Copy the code
The default is undefined, so change our code:
class Vue{ constructor(options){ this.$el = options.el; this.$data = options.data; // mount let computed = options.computed if(this.$el){new Observer(this.$data) Because there must be a lot for(let key in computed){// console.log(key) // getNewName object.defineProperty (this.$data,key,{get:)=>{ return computed[key].call(this); } }) } this.proxyVm(this.$data) new Compiler(this.$el,this) } } proxyVm(data){ for(let key in data){ Object.defineProperty(this,key,{ get(){ return data[key] } }) } } }Copy the code
Ok, so here we can use computed properties as data
The source code
Am IAccess to the source code