Foreword
The Vue responsive principle consists of the following three parts:
- Data hijacking: Vue intercepts each attribute in data through the Object.defineProperty() method. When the attribute value changes, it will trigger the setter method to notify the dependency update.
- Publish-subscribe model: Vue uses the publish-subscribe model to implement responsive updates of data. When the data changes, the dependency will be notified to update.
- Dependency collection: When Vue renders a component, it will collect the data used in the template and associate the data used in the component with the corresponding Watcher object. When the data changes, it will notify the related Watcher object to update .
Implementation process
The principle of handwritten Vue responsiveness can be divided into the following steps:
- Implement the Observer class: use the Object.defineProperty() method to intercept each property in the data. When the property value changes, the setter method will be triggered to notify the dependency update.
- Implement the Dep class: used to manage Watcher objects, including adding and deleting Watcher objects, and notifying Watcher objects to update.
- Implement the Watcher class: used to establish the connection between the view and the data. When the data changes, the Watcher object will be notified to update.
- Implement the Compile class: used to parse the template instructions, render the data corresponding to the instructions into the view, and establish the connection between the view and the data.
- Implement the Vue class: integrate the Observer, Watcher, and Compile classes to implement Vue’s responsive update mechanism.
In general, the principle of handwritten Vue responsiveness is mainly composed of Observer, Dep, Watcher, Compile, and Vue. Among them, Observer is used to intercept data changes, Dep is used to manage Watcher objects, Watcher is used to establish the connection between views and data, Compile is used to parse template instructions, and Vue integrates these classes to implement Vue’s responsive update mechanism.
Create a Dep class
We use recursion to traverse all properties in the data object, and define each property using the Object.defineProperty() method. In the defineReactive() method, we also created a Dep class, Dep class is used to manage all subscribers (Watcher) and notify them of updates.
class Dep {<!-- --> constructor() {<!-- --> this.subs = []; // Array to store dependencies } // add dependencies addSub(sub) {<!-- --> if (sub & amp; & amp; sub. update) {<!-- --> this.subs.push(sub); } } // Notify dependency update notify() {<!-- --> this.subs.forEach(sub => {<!-- --> sub. update(); }); } } Dep.target = null; // Static attribute target, used to save the current Watcher object
The above code defines a class called Dep
, which has the following methods:
constructor()
: Constructor, initialize the subscriber arraysubs
to an empty array.addSub(sub)
: The method of adding a subscriber, adding the incoming subscriber objectsub
to thesubs
array.notify()
: A method to notify all subscribers of updates, traverse thesubs
array, and call itsupdate()
method for each subscriber.target
: Define a global variabletarget
to store the current subscriber object.
In Vue, each responsive data (such as an attribute in data) corresponds to a Dep
object. When this property is read, the current subscriber object will be stored in Dep.target
, and then in the getter method of the property, Dep.target
will be added to In the subscriber array of the Dep
object of the current attribute; when the value of the attribute is modified, notify()
Dep object of the attribute will be called > method that notifies all subscribers of the update.
Create a Watcher class
Next, we need to create a Watcher class whose main function is to trigger the update operation of the view when the data changes. In the Watcher class, we first need to save the callback function required to update the view, and add the Watcher instance to the data subscription list. When the data changes, we traverse the subscription list and call the callback function in turn to update the view.
// Create a Watcher class to manage dependencies and view updates class Watcher {<!-- --> constructor(vm, key, cb) {<!-- --> this.vm = vm; this.key = key; this.cb = cb; // Specify the current Watcher instance as Dep.target Dep. target = this; // Get the value of the data, trigger the get method of the data, and add the current Watcher instance to Dep this. oldValue = vm[key]; Dep. target = null; } // update view update() {<!-- --> const newValue = this. vm[this. key]; if (this. oldValue === newValue) {<!-- --> return; } this.cb(newValue); this. oldValue = newValue; } }
Create an Observer class
Observer class: This class is used to monitor and respond to data, and mainly implements two methods: walk and defineReactive. The walk method traverses all properties in the object, and calls the defineReactive method for each property to perform responsive processing; the defineReactive method uses Object.defineProperty to add getters and setters to each property. When the property is accessed or modified, the corresponding dependency update will be triggered.
class Observer {<!-- --> constructor(data) {<!-- --> this. walk(data); } // Recursively traverse the data object, adding getters and setters for each property walk(data) {<!-- --> Object.keys(data).forEach(key => {<!-- --> this.defineReactive(data, key, data[key]); }); } defineReactive(obj, key, val) {<!-- --> const dep = new Dep(); // create a dependency collector Object.defineProperty(obj, key, {<!-- --> enumerable: true, // enumerable configurable: true, // configurable get() {<!-- --> // add dependencies if (Dep. target) {<!-- --> dep. depend(); } return val; }, set(newVal) {<!-- --> if (val === newVal) {<!-- --> return; } val = newVal; // Trigger dependency update dep. notify(); } }); } }
Create a Compile class
Compile class code. In the Compile class, we first need to traverse the nodes in the template and process them according to their type. For ordinary nodes, we will process their text content, and for nodes containing instructions, we will create Watcher instances and add them to the subscription list.
class Compile { constructor(el, vm) { this.el = document.querySelector(el); // Get the root node this.vm = vm; // save the Vue instance this.compile(this.el); // Compile the template } compile(el) { const childNodes = el.childNodes; // Get the list of child nodes of the root node Array.from(childNodes).forEach(node => { if (node.nodeType === 1) { // element node this. compileElement(node); } else if (this.isInterpolation(node)) { // text node and contains interpolation syntax this. compileText(node); } // Recursively compile child nodes if (node.childNodes & amp; & amp; node.childNodes.length > 0) { this. compile(node); } }); } compileElement(node) { const attrs = node.attributes; // Get the attribute list of the element node Array.from(attrs).forEach(attr => { const attrName = attr.name; const exp = attr. value; if (attrName.startsWith("v-")) { // match command const dir = attrName.substring(2); // Get the instruction name this[dir] & amp; & amp; this[dir](node, exp); // call the corresponding instruction function } }); } compileText(node) { const exp = node.textContent; // Get the expression in the interpolation syntax node.textContent = this.getVMValue(exp); // replace the interpolation syntax with the value of the expression } isInterpolation(node) { return node.nodeType === 3 & amp; & amp; /\{\{(.*)\}\}/.test(node.textContent); // Text node with interpolation syntax } getVMValue(exp) { let value = this.vm; exp.split(".").forEach(key => { value = value[key]; }); return value; } // v-model directive model(node, exp) { this.bind(node, exp, "model"); node.addEventListener("input", e => { const newValue = e. target. value; this.setVMValue(exp, newValue); }); } // v-bind directive bind(node, exp, dir) { const updaterFn = this[dir + "Updater"]; updaterFn & amp; & amp; updaterFn(node, this.getVMValue(exp)); new Watcher(this. vm, exp, value => { updaterFn & amp; & amp; updaterFn(node, value); }); } // model directive updates the view modelUpdater(node, value) { node.value = value; } // v-text directive text(node, exp) { this.bind(node, exp, "text"); } // The text command updates the view textUpdater(node, value) { node.textContent = value; } setVMValue(exp, value) { let vm = this.vm; const keys = exp. split("."); keys.forEach((key, index) => { if (index < keys. length - 1) { vm = vm[key]; } else { vm[key] = value; } }); } }
The instantiation of the Compile class needs to pass in two parameters: el and vm. Among them, el is the selector of the root node, and vm is the Vue instance.
The Compile class mainly implements the following functions:
- Traverse the root node and its child nodes, and compile each element node and text node containing interpolation syntax.
- For element nodes, traverse its attribute list, match instructions and call corresponding instruction functions for processing.
- For text nodes containing interpolation syntax, replace with the value of the expression.
- Implemented handling of v-model, v-bind and v-text directives.
- The processing of responsive data is realized, the data is monitored through Watcher, and the view is automatically updated when the data changes.
Create a complete Vue instance
Create a Vue class that combines the Observer, Watcher and Compile classes to create a complete Vue instance.
class Vue {<!-- --> constructor(options) {<!-- --> this. $options = options; this. $data = options. data; this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el; // Proxy the properties of the Vue instance to the $data object this._proxyData(this. $data); // Create an Observer instance to monitor data changes new Observer(this. $data); // Create a Compile instance and parse template directives new Compile(this. $el, this); } //Use the _proxyData() method to proxy the data to the Vue instance, and then you can access the data through this.key in the Vue instance _proxyData(data) {<!-- --> Object.keys(data).forEach((key) => {<!-- --> Object.defineProperty(this, key, {<!-- --> enumerable: true, configurable: true, get() {<!-- --> return data[key]; }, set(newValue) {<!-- --> if (newValue === data[key]) {<!-- --> return; } data[key] = newValue; }, }); }); } }
So far, we have completed the process of handwriting code to simulate Vue2.0’s responsive data implementation. Through this process, we can gain a deep understanding of the responsive data principle of Vue2.0, so as to better apply Vue2.0 to develop applications.
The other source code series of vue2.0 will continue to be updated in the future, including the source code of vue3.0 that is currently being studied, and will be updated later. If you like it, pay attention.
In-depth vue2.0 source code series: handwritten code to simulate the responsive data implementation of Vue2.0
In-depth vue2.0 source code series: the realization principle of handwritten code simulating Vue2.0 to realize virtual DOM
In-depth vue2.0 source code series: the realization of life cycle
In-depth vue2.0 source code series: the implementation principle and implementation method of instructions
In-depth vue2.0 source code series: the realization principle of template compilation
In-depth vue2.0 source code series: the realization and application of event mechanism
In-depth vue2.0 source code series: from the perspective of source code to see the realization of MVVM architecture pattern
In-depth vue2.0 source code series: implementation of dependency tracking and computed properties