Martial arts, do not bite off more than you can chew, Bo son is not fine as a recruit fresh eat all over the day, programming is the same, source code is the practice of internal forces. Here we based on the analysis and understanding of vUE source code to achieve a custom MVVM framework: KVue.
Achieve goals:
1. Data hijacking: defineProperty. 2. Dependency collection: Dep && Watcher. 3, compilation: interpolation binding {{name}}, instruction binding (V-text), two-way binding (V-model), event handling (@click), HTML parsing.
The figure below Outlines the details of the various parts of the MVVM implementation.
There are three main sections: reactive principles > dependency collection and tracing > compiling complie. Dependency collection is related to reactive and compilation principles, so we can implement an MVVM framework in two parts: reactive principles and compilation implementation.
Response principle
Update views as data is modified through responsiveness. The responseful principle of vue.js relies on Object.defineProperty. Vue listens for changes in data by setting setter/getter methods for Object properties, and relies on getter methods for collection. Each setter method is an observer. Notify the subscriber to update the view when the data changes.
The data was hijacked
Implementation process: Implement a KVue constructor that accepts options and data properties. Iterating through the properties in options.data, using the Object. defineProperty property to do data hijacking. Note that we execute a proxy for the property values as we walk through the data properties. This allows us to delegate the properties on data to the VM instance.
class KVue { constructor(options) { this.$options = options; this.$data = options.data; observe(this.$data); }}Copy the code
Implement a data observer Observe()
observe(value) { if(! value || typeof value ! Keys (value).foreach (key => {this.definereactive (value, key, Value [key]) // proxyData attribute to vue instance this.proxydata (key)})} defineReactive(obj, key, val){this.observe(val); Const dep = new dep (); Object.defineProperty(obj, key, { get: function(){ return val; }, set: function(newVal) { if(val === newVal){ return } val = newVal; }} proxyData(key) {// Execute a proxy proxy. This allows us to delegate the properties on data to the VM instance. Object.defineProperty(this, key, { get(){ return this.$data[key]; }, set(newVal){ this.$data[key] = newVal } }) }Copy the code
This implements data hijacking of the data property in options, through the getter to read the property, and through the setter to set the property value. In the previous example, we simply read the value in. In set, we set the new value.
Publish and subscribe model
In the previous step we just did data hijacking. To achieve the effect of data responsiveness, we need to do some things in getters and setters. We need to implement a dependency collector Dep() and a data listener Watcher().
Depend on the collection
Dependency collections also become publishers.
Data hijacking has been implemented for the date attribute of Option above, and getter events will be triggered naturally when initializing the read value. Therefore, as long as we perform render at the beginning, all the data in the rendered dependent data will be collected by getter in the SUBs of Dep. Setters only fire the subs function of the Dep when modifying the data in the data.
We will implement a simple dependency collection class. The deP maintains an array of dePS, addDep is used to add data dependencies, and notify function notifies dependency updates in the array.
class Dep { constructor() { this.deps = []; } addDep(dep) { this.deps.push(dep) } notify() { this.deps.forEach(dep => { dep.update() }); }}Copy the code
Data listener
Data listeners are also called subscribers.
When relying on collection, addSub will be added to sub. When modifying data in data, notify the DEP object will be triggered to notify all Watcher objects to modify the corresponding view.
class Watcher { constructor(vm, key, cb) { this.vm = vm; this.key = key; this.cb = cb; Dep. Target = this; Dep = this; // Trigger the getter to add the dependency this.vm[this.key]; Dep.target = null} update() {this.cb.call(this.vm, this.vm[this.key]); this.cb.call(this.vm, this.vm[this.key]); }}Copy the code
Implements a class that allows data to be observed
Start relying on collections
Once we have implemented the publish subscriber pattern, we can apply the dependency collector and data listener to data hijacking and start relying on collection.
defineReactive(obj, key, val){ this.observe(val); Const dep = new dep (); Object.defineProperty(obj, key, { get: */ dep.target && dep.adddep (dep.target) return val; function(){/* dep.target &&dep.target return val; }, set: function(newVal){ if(val === newVal){ return } val = newVal; /* Only functions in addSub trigger */ dep.notify(); }})}Copy the code
The first part detects the target achievement
Through the above code we have achieved the most basic part of the MVVM framework, data responsiveness. You can run the code above and simulate the watcher creation process to test your target results. Write an HTML file, introduce a KVue script, define an instance of kVue, pass in the option option, and modify attribute values
<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, Initial - scale = 1.0 "> < title > Document < / title > < / head > < body > < div id =" app "> < p id =" name "> <! -- {{test}} --> </p> </div> <script src="kvue.js"></script> <script> const app = new KVue({ data: { test: 'I am test', foo: { bar: 'bar' } } }) app.$data.test = 'hello, vue'; app.$data.foo.bar = 'oh, my bar'; </script> </body> </html>Copy the code
Simulate the process of relying on collection and data listening in the creation of watcher in the KVue constructor;
constructor(options) { this.$options = options; this.$data = options.data; this.observe(this.$data); // Simulate the creation of watcher; new Watcher(this, 'test', (val)=>{ console.log(val) }); this.$data.test; new Watcher(this.foo, 'bar', (val)=>{ console.log(val) }); this.$data.foo.bar // new Compile(options.el, this); // created executes // if(options.created){// options.created. Call (this); / /}}Copy the code
When you open the test HTML file in the console, you can see the console output “Hello, vue”, indicating that the dep.notify function written in the setter is triggered, the Watcher update function is called, and the Watcher callback function is triggered. So the property values are printed in watcher’s callback.
Compiler implementation
Achieve goals:
- Interpolation binding: {{name}},
- Instruction binding: K-text,
- Two-way binding: K-model,
- HTML parsing: K-HTML.
- Event handling: @click,
Compile constructor
New the compile. Js files, for instance rendered host node, node traversal host, through the document. The createDocumentFragment () method will be the node of the Dom node to document fragments. Because the document fragment exists in memory, not in the DOM tree, inserting the document fragment does not cause page backflow. Therefore, using document fragments generally results in better performance. The compiled function is then performed on the transformed document fragment: all the self nodes of the root node are iterated, and the element and interpolation nodes are treated separately. Note that if the child nodes still contain children, the compile function needs to be implemented recursively.
If you are not familiar with the DOM Node, please refer to the MDN section on Node to understand the compilation process.
class Compile { constructor(el, vm){ this.$el = document.querySelector(el); This.$vm = vm; Fragment this.$fragment = this.node2fragment (this.$el); This.compile (this.$fragment); // Append the compiled HTML result to el this.$el.appendChild(this.$fragment); }} // It is more efficient to iterate over the snippet of the host element. node2Fragment(el) { const frag = document.createDocumentFragment(); // Move all child elements in el to let child; while(child = el.firstChild){ frag.appendChild(child) } return frag; } // Compile (el){const childNodes = el.childnodes; // array. from(childNodes).foreach (node => {if(this.isElement(node)){console.log(' compile element '+ nodeName)} else If (this.isinterpolation (node)){// Interpolate text onsole.log(' compile text '+node.textContent)} // Recursive child if(Node.childNodes && Node.childnodes. length>0){this.compile(node)}}} isElement(node) {return node.nodeType === 1; } isInterpolation(node) {return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent); }}Copy the code
Realize interpolation and replacement processing
Firstly, a simple interpolation and replacement of interpolation nodes is implemented. The processing text branch in the compile function executes the procedure compileText.
. Array.from(childNodes).foreach (node => {if(this.isElement(node)){// Handle element console.log(' compile element '+ node.nodename)} else If (this.isinterpolation (node)){// Interpolate text console.log(' compile text '+node.textContent) this.piletext (node); }}... CompileText (node) {/* Catch the grouping in the regular expression match 1 */ let groupName = RegExp.$1 /* Set the node's textContent property to the property value in data, $data[groupName]; $data[groupName]; }Copy the code
Create a new test function for the compile process, compile-test.html
<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, Initial - scale = 1.0 "> < title > Document < / title > < / head > < body > < div id =" app "> < p > {{name}} < / p > < p k - text =" name "> < / p > < p > {{age}} < / p > < p > {{doubleAge}} < / p > < input type = "text" k - model = "name" > < button @ click = "changeName" > haha < / button > < div k-html="html"></div> </div> <script src="kvue.js"></script> <script src="compile.js"></script> <script> const app = new KVue({ el: "#app", data: { name: 'I am test', age: '10', doubleAge: '20', html: '< button > this is a button < / button >'}, created () {the console. The log (' started '); SetTimeout (() => {this.name = 'w I am testing '; }, 1500)}, methods: {changeName() {this.name = 'event trigger '; this.age = 1; } }, }) </script> </body> </html>Copy the code
To test the compilation process, we can add the Created declaration cycle to the constructor of KVue and adjust KVue as follows: implement compiler instantiation and execute the Created declaration cycle function. kvue.js
constructor(options) { this.$options = options; this.$data = options.data; this.observe(this.$data); new Compile(options.el, this); // created executes if(options.created){options.created. Call (this); }}Copy the code
Running the file, we find that the interpolation part is successfully parsed, but the value does not change on the page in response to changes. This is because interpolation properties are not added to dependencies during compilation. All compilers dealing with node property values need to combine compiler with data listeners and add dependency collection.
Dependency collection in the compiler
Because this step is required for other types of nodes, we extract this functionality as a public function update, passing in custom variables as function arguments.
compileText(node) { this.update(node, this.$vm, RegExp.$1, 'text') } update(node, vm, exp, Const updaterFn = this[dir+'Updater']; // Initialize updaterFn && updaterFn(node, vm[exp]); New Watcher(vm, exp, function(value){updaterFn && updaterFn(node, value); }) } textUpdater(node, value) { node.textContent = value }Copy the code
Running the compile-test.html file, we found that the three interpolation operators on the page were properly resolved and the property values modified during the Created declaration cycle were successfully displayed on the page. At this point, we have implemented a compiler that is responsive enough to handle interpolation operators.
Implementing instruction binding
The key to realize the instruction binding is to correctly process the instruction on the branch of the processing element of the compilation function and determine whether it starts with K -, for example, k-text. If dir matches text, then determine whether the this.text function exists and execute the text function. The public update function update() is called within the text function, which triggers ${dir}Updater() when dependent on updates and sets the node textContent property.
. If (this.isElement(node)){// Handle elements // find k-, @, :, const node.attributes = node.attributes; Array.from(nodeAttrs).forEach(attr => { const attrName = attr.name; const exp = attr.value; If (this.isdirective (attrName)) {// k-text const dir = attrname.substring (2); This [dir] && this[dir](node, this.$vm, exp); }})}... isDirective(attr) { return attr.indexOf('k-') === 0; } text(node, vm, exp){ this.update(node, vm, exp, 'text') } textUpdater(node, value) { node.textContent = value }Copy the code
Two-way binding
Bidirectional binding is generally implemented by k-Model. You already matched ‘model’ through dir in the previous code, so you just need to supplement the model function and modelUpdater function.
// Bind model(node, vm, exp){this.update(node, vm, exp, 'model'); // View response to model node.addeventListener ('input', e => {vm[exp] = e.target.value; }); } modelUpdater(node, value){ node.value = value; }Copy the code
HTML parsing
In the same way, with v-HTML instructions, you just add
html(node, vm, exp){
this.update(node, vm, exp, 'html');
}
htmlUpdater(node, value){
node.innerHTML = value;
}
Copy the code
event
Determine the instructions between the processes on the element molecule of the compile function. Dir matches the event type, such as: click
. If (this.isevent (attrName)){// Handle the event const dir = attrname.substring (1); this.eventHandler(node, this.$vm, exp, dir) } ... EventHandler (node, vm, exp, dir) {let fn = vm.$options.methods && vm. if(dir && fn) { node.addEventListener(dir, fn.bind(vm)); // Bind event listener}}Copy the code
At this point, a basic syntax MVVN framework KVue based on VUE has been realized, which can realize syntax parsing similar to VUE instructions, as well as two-way binding of real-time data view. Support interpolation binding, instruction binding, bidirectional binding HTML parsing, event processing and other syntax features.
conclusion
The above process may be helpful to understand in the following brain map.
Project source code:
Gitee address: kvue: Understand vue source code, from 0 to 1 to achieve their own MVVM framework if you feel helpful can give a star oh!