preface
Vue's responsive principle is an old interview question, and most people will choose to recite the answer directly to deal with the interview. Once the interviewer continues to ask questions, he can't answer anything. Therefore, I hope to write code (about 300 lines) to implement a simple Vue MVVM framework by referring to Vue source code, so as to let us better understand Vue's responsive principle.
We'll pass
- Implement instruction parser Compile
- Implement data listener Observer
- Implement Watcher
To realize the responsive principle of MVVM of the whole Vue
The functions implemented are not perfect. If you are interested, you can add them yourself. This code is mainly to complete the whole process of the responsive principle, understand what Observer, Compile and Watcher are, what they are used for, and how to build a bridge of the whole MVVM architecture through these three, so as to realize the responsive principle of Vue.
Create a simple html as a test
<div id="app"> <div>{{person.name}} -- {{person.age}}</div> <div>{{person.fav}}</div> <ul> <li>1</li> <li>2</li> <li>3</li> </ul> <div v-html="htmlStr" v-bind:style="{backgroundColor:red}"></div> <div v-text="person.fav"></div> <!-- <input type="text" v-bind:value='msg'> --> <input type="text" v-model='msg'> <div>{{msg}}</div> <h2 v-text="tip" v-on:click="clickMe"></h2> </div>
Like Vue, create a Vue object in the script tag below
// To avoid conflicts with Vue, MVue is used as the class name const app = new MVue({ el: "#app", data: { person: { name: 'GHkmmm', age: '21', fav: 'programming' }, msg: 'understand Vue Bidirectional binding principle of', tip: 'Point me', htmlStr: 'hello world!' }, methods: { clickMe(){ console.log('click'); } } })
Create MVue class
Used to receive incoming parameters
class MVue{ constructor(options){ this.$options = options; this.$el = options.el; this.$data = options.data; if(this.$el){ // Implement an instruction parser new Compile(this.$el, this); } } }
Implement an instruction parser Compile
class Compile{ constructor(el, vm){ this.el = this.isElementNode(el) ? el : document.querySelector(el); this.vm = vm; /* When compiling the page content, it is impossible to change each object one by one because it needs to traverse each object But through the form of document fragments, modify them uniformly, and then use fragment to update the whole page */ // 1. Get the document fragment object and put it into memory to reduce the backflow and redrawing of the page const fragment = this.nodeFragment(this.el); // 2. Compilation template this.compile(fragment); // 3. Append child element to root element this.el.append(fragment); } }
Get document fragments
Traverse the child nodes and add them to the document fragment object
nodeFragment(node){ // Create document fragment object const f = document.createDocumentFragment(); let firstChild; //Traverse the child nodes under < div id = "app" > while(node.firstChild){ firstChild = node.firstChild; // Add the traversed node to the document fragment object f.append(firstChild); } // Return document fragment object return f; }
Compilation template
After the fragment is passed in, we need to traverse the nodes in the fragment to determine whether it is an element node or a text node. Because the processing methods of the two are different, we need to use different methods to compile
compile(fragment){ // Pass in the just obtained document fragment object fragment // 1. Get child nodes const childNodes = fragment.childNodes; // Traversing the childNodes array [...childNodes].forEach(child => { /* isElementNode(el){ return el.nodeType === 1; } */ if(this.isElementNode(child)){ // Element node // console.log('element node ', child); this.compileElement(child); }else{ // Non element node (text node...) // console.log('text node ', child); this.compileText(child); } // Judge whether there are any child nodes under the child node. If so, recursion is performed if(child.childNodes && child.childNodes.length){ this.compile(child); } }) }
Compile element node
compileElement(node){ // Get all attributes on the node const attributes = node.attributes; // Traversal property array [...attributes].forEach(attr => { // Using deconstruction assignment, the name attribute and value attribute in attr are extracted separately // The name and value here correspond to the attributes in attr and cannot be changed // name: v-text,v-model,v-bind.... // vaule: v-text,v-model.. Value followed const { name, value } = attr; /* Judge whether the name starts with 'v -'. If so, compile it. If not, do not process it isDirective(attrName){ return attrName.startsWith('v-'); } */ if(this.isDirective(name)){ // According to the '-' split name, the split method returns an array. The first parameter is not used, and the second parameter is assigned to directive // The directive here is different from the above. Just choose a name here const [,directive] = name.split('-'); // Because the instructions may be not only v-text, but also v-on:click, further segmentation is required const [dirName,eventName] = directive.split(':'); // Update data driven view // Call different methods in compleleutil according to different dirnames // dirName refers to text,html,bind,on complileUtil[dirName](node, value, this.vm, eventName) // Delete attributes on tags with instructions node.removeAttribute(name); } }) }
Compile text node
compileText(node){ // Get text content const content = node.textContent; if(/\{\{(.+?)\}\}/.test(content)){ complileUtil['text'](node,content,this.vm) } }
Compleleutil object
The processing methods of different instructions are implemented internally
const complileUtil = { getVal(expr, vm){ /* expr It could be msg or person Name, so you need to split expr again After segmentation, an array is obtained, such as ['person ','name']. After using the reduce method Return VM for the first time$ data['person'] Return VM for the second time$ data['person']['name'] Another example is MSG, which gets the array ['msg '], so it directly returns VM$ Data ['msg '] */ return expr.split('.').reduce((data, currentVal) => { // console.log(data); // console.log(currentVal); return data[currentVal]; }, vm.$data//Initial value) }, setVal(expr, vm, inputVal){ return expr.split('.').reduce((data, currentVal) => { data[currentVal] = inputVal; }, vm.$data) }, // Compile v-text instruction and mustache syntax of text node {{msg}} text(node, expr, vm){ let value; // Determine whether to start with '{{' if(expr.indexOf("{{")!==-1){ // The string in braces is extracted by matching double braces through regular expression value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => { // Pass in the getVal method in VM$ Find data in data return this.getVal(args[1], vm); }) }else{ value = this.getVal(expr, vm); } // update the view this.updater.textUpdater(node, value); }, // Compiling v-html instructions html(node, expr, vm){ const value = this.getVal(expr, vm); this.updater.htmlUpdater(node, value); }, // Compiling v-model instructions model(node, expr, vm){ const value = this.getVal(expr, vm); // View = > data = > View // Listen for input events node.addEventListener('input', (e)=>{ this.setVal(expr, vm, e.target.value); }) this.updater.modelUpdater(node, value); }, // Compiling v-on instructions on(node, expr, vm, eventName){ let fn = vm.$options.methods && vm.$options.methods[expr]; node.addEventListener(eventName, fn.bind(vm), false) }, // Compiling v-model instructions bind(node, expr, vm, attrName){ ...//Here you can do it yourself }, //Updated function updater: { textUpdater(node, value){ node.textContent = value; }, htmlUpdater(node, value){ node.innerHTML = value; }, modelUpdater(node, value){ node.value = value; }, bindUpdater(node, attrName, value){ node[attrName] = value } } }
Implement a data listener Observer
Create Observer class
effect
Use object Define property to set getter s and setter s for all properties in data
class Observer{ constructor(data){ this.observer(data); } observer(data){ if(data && typeof data === 'object'){ // Traversal data object Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]); }) } } defineReactive(data, key, value){ // Recursive traversal this.observer(value); const dep = new Dep(); // Listen and hijack all properties // defineProperty passes in data and key to add corresponding getter and setter to data[key] Object.defineProperty(data, key, { enumerable: true, configurable: false, //Describes whether the attribute is configured and whether it can be deleted get(){ // This function is called when the property is accessed // initialization // When the subscription data changes, add observers to Dep Dep.target && dep.addSub(Dep.target); return value; }, // This function is called when the property value is modified // The arrow function is used here to make this point to the upper layer instead of Object set: (newVal) => { this.observer(newVal) if(newVal !== value){ value = newVal; } //Tell Dep to notify changes dep.notify(); } }); } }
Implemented in MVue class
class MVue{ constructor(options){ this.$options = options; this.$el = options.el; this.$data = options.data; if(this.$el){ // Implement a data observer new Observer(this.$data); // Implement an instruction parser new Compile(this.$el, this); } } }
Implement dependency collector Dep
Create Dep class
effect
- Collect observers
- If the hijacked data in the Observer changes, Dep will be notified to notify the corresponding Observer
class Dep{ constructor() { this.subs = []; } // Collect observers addSub(watcher){ this.subs.push(watcher); } // Notify observers of updates notify(){ this.subs.forEach(w => w.update()) } }
Implement a Watcher to update the view
Create Watcher class
class Watcher{ constructor(vm, expr, callback){ this.vm = vm; this.expr = expr; this.callback = callback; this.oldVal = this.getOldVal() } getOldVal(){ Dep.target = this; const oldVal = complileUtil.getVal(this.expr, this.vm) Dep.target = null; return oldVal; } update(){ const newVal = complileUtil.getVal(this.expr, this.vm); if(newVal !== this.oldVal){ this.callback(newVal); } } }
Implement Watcher in the method of each processing instruction of compleleutil
new Watcher(vm, expr, callback)
const complileUtil = { getContentVal(expr, vm){ return expr.replace(/\{\{(.+?)\}\}/g, (...args) => { return this.getVal(args[1], vm) }) }, text(node, expr, vm){ let value; if(expr.indexOf("{{")!==-1){ value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => { new Watcher(vm, args[1], ()=>{ this.updater.textUpdater(node, this.getContentVal(expr,vm)); }) return this.getVal(args[1], vm); }) }else{ value = this.getVal(expr, vm); } this.updater.textUpdater(node, value); }, html(node, expr, vm){ const value = this.getVal(expr, vm); // Bind the observer. If the data changes in the future, the callback here will be triggered to update new Watcher(vm, expr, (newVal)=>{ this.updater.htmlUpdater(node, newVal); }) this.updater.htmlUpdater(node, value); }, ... }
Exhibition
At this point, we have realized the MVVM response of the whole Vue and the two-way binding of data
summary
vue adopts data hijacking and publisher subscriber mode
Object.defineProperty() hijacks the setter s and getter s of various properties. When the data changes, it publishes a message to the dependent collector Dep to notify the observer and make the corresponding callback function to update the view.
As the binding entry, MVVM integrates Observer,Compile and Watcher, monitors model data changes through Observer, parses and compiles template instructions through Compile, and finally uses Watcher to build
Communication bridge between observer and compile to achieve data change = > View update; Interactive view change = > bidirectional binding effect of data model change
If there are any questions in the article, you are welcome to point out
Reference tutorial: Reference tutorial