The students who have used VUE must all know that it is based on Object. DefineProperty implementation response, so how do you do it? Next, I will use less than 200 lines of code step by step to dismantle the response principle, to achieve a minimalist version of VUE defineProperty can refer to MDN
Intercepts the storage behavior of object properties
Create a new file my-viee.js, define a method defineReactive internally, and intercept the operation of Object properties with object.defineProperty inside the method
// my-vue.js
function defineReactive(obj, key) {
// A closure is formed where the key and value are not reclaimed and are resident in memory
// For the record, the dependency update dep.notify feature will take advantage of closures
let value = obj[key];
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get() {
console.log("get");
return value;
},
set(v) {
console.log("set");
if (v === value) {
return; } value = v; }}); }Copy the code
To correct errors in a timely manner, we can run a test every time we finish writing a method to verify that there is no problem with the code and execute the commandnode my-vue.js
See the effectSo far we’ve implemented intercepting properties, but only one property is handled, so we need an additional method to traverse the object and add intercepting behavior to each property
function observe(obj) {
if (typeofobj ! = ="object" || obj === null) {
return;
}
Object.keys(obj).forEach((key) = > {
defineReactive(obj, key);
});
}
Copy the code
Again, there is a defect, we only dealt with normal properties, we didn’t intercept the nested object case, so we need to rewrite defineReactive, and when we do the assignment we need to check whether it is an object, again observe
function defineReactive(obj, key) {
let value = obj[key];
// In this case, value may be an object, so you need to observe again
observe(value);
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get() {
console.log("get");
return value;
},
set(newVal) {
console.log("set");
if (newVal === value) {
return;
}
// In this case, value may be an object, so you need to observe againobserve(value); value = v; }}); }function observe(obj) {
if (typeofobj ! = ="object" || obj === null) {
return;
}
Object.keys(obj).forEach((key) = > {
defineReactive(obj, key);
});
}
const obj = {
test: "tttt".testObj: {
chid: "child",}}; observe(obj); obj.testObj.chid ="update child";
console.log(obj.testObj.chid);
Copy the code
At this point, all properties of the object have been intercepted. The only thing left is to render the updated page in some form as the data changes. Before you get done with that, take a look at vUE’s data-driven view philosophyLet’s start by introducing some of the concepts involved in the figure below
- New MVVM() framework constructors such as new Vue()
- Observer: Performs data responsiveness
Note: We have already done this step above
- Compile: Compile templates, initialize views, collect dependencies (update functions, watcher create)
- Watcher: Perform the update function (dom update)
- Dep: Manage multiple Watcher, batch update
Implementation of the new MYVUE() framework constructor
class MYVUE {
constructor(options) {
this.$options = options;
this.$data = options.data;
// Process the data into responses
observe(this.$data); }}Copy the code
Now that you’ve defined the constructor, you’ve got a prototype and you can initialize it just like you would with vue, create an HTML to actually experience the initialization process
<! DOCTYPEhtml>
<html lang="en">
<body>
<div id="app">
<p>Expressions:<span>{{counter}}</span></p>
<span>Customize the specified V-text:</span><span v-text="counter"></span>
</div>
<script src="./my-vue.js"></script>
<script>
const app = new MYVUE({
el: "#app".data: {
counter: 1,}});console.log(app.counter);
console.log(app.$data.counter);
</script>
</body>
</html>
Copy the code
In MYVUE, the user-initialized data is mounted in this.$data, so it is not directly accessible. To solve this problem, we need to introduce the concept of a proxy. App.$data.counter is returned when the implementation user accesses app.counter
class MYVUE {
constructor(options) {
this.$options = options;
this.$data = options.data;
observe(this.$data);
proxy(this)}}function proxy(vm) {
Object.keys(vm.$data).forEach((key) = > {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key];
},
set(v){ vm.$data[key] = v; }}); }); }Copy the code
After dealing with the agent issue, I went back to the above HTML file to check the effect, and found that I could print app.counter correctly
Compile implementation, initialize the view
This step is mainly based on the EL attribute passed by new MYVUE to parse HTML and compile the template syntax such as expression {{}} and custom instruction V-xxx into corresponding data. The main implementation method is as follows:
New Compile(el,vm)
class Compile {
constructor(el, vm) {
this.$vm = vm
this.$el = document.querySelector(el)
this.compile(this.$el)
}
compile($el) {
$el.childNodes.forEach((node) = > {
if (this.isElement(node)) {
NodeType === 1
// Compile the elements
// Handle custom instructions on elements
this.compileElement(node)
} else if (this.isExpression(node)) {
// If the expression {{XXX}} is regular, /\{\{{{(.*)\}\}/
this.compileExpression(node)
}
if (node.childNodes) {
this.compile(node)
}
})
}
// Compile the element to handle custom instructions
compileElement(node) {
Array.from(node.attributes).forEach((attr) = > {
const attrName = attr.name
const exp = attr.value
if (this.isDirective(attrName)) {
// Get the name of the directive. For example, the name of v-text is text
const dir = attrName.substring(2)
this[dir] && this[dir](node, exp)
}
})
}
isDirective(attrName) {
return attrName.startsWith('v-')}isElement(node) {
return node.nodeType === 1
}
isExpression(node) {
return node.nodeType === 3 && / \ {\ {(. *) \} \} /.test(node.textContent)
}
compileExpression(node) {
$1 is an attribute of RegExp, which refers to the first submatch (marked by parentheses) string that matches the regular expression
node.textContent = this.$vm[RegExp.$1]
}
text(node, exp) {
node.textContent = this.$vm[exp]
}
}
Copy the code
Going back to the HTML file to see what it looks like, it looks good so far, and it has initialized the rendered page correctly
Watcher
If you try to change the value in the data, you find that the page is not updated, only the page is rendered during initialization, because there is no dependency collection. The so-called dependency is that the key in the data is used by the template. If the key value changes, you need to notify the Watcher to update the view. Each time a key is referenced, a wather is added. This means that a key may have multiple Watcher, which are managed by the dependent Dep. It is easier to understand the vUE data-driven view principle provided above.
Conclusion is
One key for one Dep,
One Dep manages multiple reference dependencies for this key
Each time the key is referenced, a watcher is created
// Create a watcher every time the key is referenced in the page
class Watcher {
// updateFn: method to update the page when the key changes
constructor(vm, key, updateFn) {
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
}
// Dep notifishes watcher of the update
update() {
this.updateFn.call(this.vm, this.vm[this.key]); }}Copy the code
The Watcher class is written, the next step is to find where the key is referenced, create a Watcher instance, and the key is referenced mainly in Compile, so we switch the perspective back to Compile, In Compile we implement v-text and {{}} two places where data can be dynamically referenced, which is where key can be referenced, so we rewrite isExpression and text in the Compile class to create watcher instances in these two methods
Add the watcher instance method
New Compile(el,vm)
class Compile {
constructor(el, vm) {
console.log(el, vm);
this.$vm = vm;
this.$el = document.querySelector(el);
this.compile(this.$el);
}
compile($el) {
$el.childNodes.forEach((node) = > {
if (this.isElement(node)) {
NodeType === 1
// Compile the elements
// Handle custom instructions on elements
this.compileElement(node);
} else if (this.isExpression(node)) {
// If the expression {{XXX}} is regular, /\{\{{{(.*)\}\}/
this.compileExpression(node);
}
if (node.childNodes) {
this.compile(node); }}); }compileElement(node) {
Array.from(node.attributes).forEach((attr) = > {
const attrName = attr.name;
const exp = attr.value;
if (this.isDirective(attrName)) {
// Get the name of the directive. For example, the name of v-text is text
const dir = attrName.substring(2);
this[dir] && this[dir](node, exp); }}); }isDirective(attrName) {
return attrName.startsWith("v-");
}
isElement(node) {
return node.nodeType === 1;
}
isExpression(node) {
return node.nodeType === 3 && / \ {\ {(. *) \} \} /.test(node.textContent);
}
compileExpression(node) {
$1 is an attribute of RegExp, which refers to the first submatch (marked by parentheses) string that matches the regular expression
node.textContent = this.$vm[RegExp. $1];new Watcher(this.$vm, RegExp. $1,function (val) {
this.expressionUpdater(node, val);
});
}
expressionUpdater(node, value) {
node.textContent = value;
}
text(node, exp) {
// When initialization
node.textContent = this.$vm[exp];
// Execute watcher at update time
new Watcher(this.$vm, exp, function (val) {
this.textUpdater(node, val);
});
}
textUpdater(node, value){ node.textContent = value; }}Copy the code
After that, I don’t know if you noticed that there is a lot of duplicate code to add watcher to the compileExpression, text method, so we can further optimize the code to extract watcher as a common method update
update(node, exp, updaterPrefix) {
// updaterPrefix is equivalent to the directive name. Expression and text may be used here
const fn = this[updaterPrefix + "Updater"];
fn && fn(node, this.$vm[exp]);
new Watcher(this.$vm, exp, function (val) {
fn && fn(node, val);
});
}
Copy the code
The complete code for the Compile class after refactoring is:
New Compile(el,vm)
class Compile {
constructor(el, vm) {
console.log(el, vm);
this.$vm = vm;
this.$el = document.querySelector(el);
this.compile(this.$el);
}
compile($el) {
$el.childNodes.forEach((node) = > {
if (this.isElement(node)) {
NodeType === 1
// Compile the elements
// Handle custom instructions on elements
this.compileElement(node);
} else if (this.isExpression(node)) {
// If the expression {{XXX}} is regular, /\{\{{{(.*)\}\}/
this.compileExpression(node);
}
if (node.childNodes) {
this.compile(node); }}); }compileElement(node) {
Array.from(node.attributes).forEach((attr) = > {
const attrName = attr.name;
const exp = attr.value;
if (this.isDirective(attrName)) {
// Get the name of the directive. For example, the name of v-text is text
const dir = attrName.substring(2);
this[dir] && this[dir](node, exp); }}); }isDirective(attrName) {
return attrName.startsWith("v-");
}
isElement(node) {
return node.nodeType === 1;
}
isExpression(node) {
return node.nodeType === 3 && / \ {\ {(. *) \} \} /.test(node.textContent);
}
update(node, exp, updaterPrefix) {
// updaterPrefix is equivalent to the directive name. Expression and text may be used here
const fn = this[updaterPrefix + "Updater"];
fn && fn(node, this.$vm[exp]);
new Watcher(this.$vm, exp, function (val) {
fn && fn(node, val);
});
}
// Compile the expression
compileExpression(node) {
$1 is an attribute of RegExp, which refers to the first submatch (marked by parentheses) string that matches the regular expression
// node.textContent = this.$vm[RegExp.$1];
// new Watcher(this.$vm, RegExp.$1, function (val) {
// this.expressionUpdater(node, val);
// });
this.update(node, RegExp. $1,"expression");
}
expressionUpdater(node, value) {
node.textContent = value;
}
// Compile the V-text directive
text(node, exp) {
// // when initializing
// node.textContent = this.$vm[exp];
// // Watcher is executed during updates
// new Watcher(this.$vm, exp, function (val) {
// this.textUpdater(node, val);
// });
this.update(node, RegExp. $1,"text");
}
textUpdater(node, value){ node.textContent = value; }}Copy the code
After the watcher is created, it cannot be executed because the Dep is needed to notify the watcher to update the view.
Before writing Dep code, let’s take a look at what it would look like to update the view without Dep
Define a global variable, const Watchers = [], and then the constructor of the Watcher class adds the instance to Watchers, updating the view each time the data is updated
const watchers = [];
// Create a watcher every time the key is referenced in the page
class Watcher {
// updateFn: method to update the page when the key changes
constructor(vm, key, updateFn) {
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
watchers.push(this)}// Dep notifishes watcher of the update
update() {
this.updateFn.call(this.vm, this.vm[this.key]); }}Copy the code
When data is updated, the view is updated
function defineReactive(obj, key) {
let value = obj[key];
observe(value);
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get() {
return value;
},
set(v) {
if (v === value) {
return;
}
observe(value);
value = v;
// When data is updated, the view is directly updated without passing through Dep
watchers.forEach((item) = >item.update()); }}); }Copy the code
At this point, I added a timer to the HTML, and found that the page did refresh in real time. It seems that the update can be realized without notifting watcher directly, and no Dep is required
const app = new MYVUE({
el: "#app".data: {
counter: 1,}});setInterval(() = > {
console.log(app.counter++);
}, 1000);
Copy the code
The problem is that updating any value in the data will cause all of the pages to rely on watcher updates, which means that the entire page will be rerendered, resulting in a waste of performance. Vue introduced the concept of Dep to solve this problem. All watcher were collected and each key was corresponding to the Dep instance. In this way, every time a key of data was updated, only the Dep corresponding to the key needed to notify its own watcher to update
class Dep {
constructor() {
this.deps = [];
}
addDep(watcher) {
this.deps.push(watcher);
}
notify() {
this.deps.forEach((watcher) = >watcher.update()); }}Copy the code
Each key will have a corresponding Dep, so we will instantiate a key in defineReactive and generate a Dep. At this point, there is a new problem. During defineReactive, Watcher did not generate a Dep. In the case of deP with no Watcher to collect, Vue’s solution is to mount the instance as a static property on the DEP class during the watcher instantiation phase, and then read the key once again to trigger a get function. In the GET function, the Watcher instance is collected
Each time the key is referenced in the page, a watcher instance is created
class Watcher {
// updateFn: method to update the page when the key changes
constructor(vm, key, updateFn) {
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
// Mount the instance on the Dep class as a static property
Dep.watcher = this;
// Read a key to trigger a get function
this.vm[this.key];
Dep.watcher = null;
}
// Dep notifishes watcher of the update
update() {
this.updateFn.call(this.vm, this.vm[this.key]); }}Copy the code
Collect the Watcher and update the view as data changes
function defineReactive(obj, key) {
let value = obj[key];
const dep = new Dep();
observe(value);
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get() {
/ / collect watcher
Dep.watcher && dep.addDep(Dep.watcher);
return value;
},
set(v) {
console.log("set");
if (v === value) {
return;
}
observe(value);
value = v;
// When the data is updated, trigger all watcher corresponding to the key to update the pagedep.notify(); }}); }Copy the code
So far a minimalist Vue has been implemented! Call it a day! A: hello! Three o ‘clock a few, do eggs ah do, tea first!