Writing in the front
Is there any use in implementing another MVVM framework after 1202? In fact,… Indeed… Baa use. You have written a good but difficult volume over the mature framework of VUE, React, etc., but personally, to implement a simple version of vUE will deepen your understanding of the principle of source code, so it seems to be a bit meaningful.
Final effect:
Functions:
- Declarative rendering {{message}}
- Conditional render V-if
- List render V-for
- Event processing V-ON
- Component rendering < Component >
Frame features:
- Build with typescript+ Webpack
- The framework manipulates the real DOM directly without using the VDOM
- Only part of vue’s functionality is implemented, because it’s a bit of a brick to try to do it all.
- Unlike the flaws of vue2. X, arrays within Moush-Vue can be updated by subscript indexing
On the lack of VDOM: This is the difference between vu1.x and 2.x, but the lack of VDOM doesn’t affect your understanding of the vue principle at all, because understanding the principle of 1.x, 2.x simply adds vDOM and the associated diff algorithm on top of the original.
Presentation:
template:
<div id="app">
<div class="ageContent">
<p>
{{name}}的年龄是{{age}}
</p>
<ul>
<li v-for="item in arr">
{{item}}
</li>
</ul>
<button v-on:click="addFunc">addFunc</button>
<coma v-if="isShow"></coma>
</div>
Copy the code
typescript:
const app = new moushVue({
el: "#app".data: function () {
return {
age: 1.name: "Xiao Ming".isShow: true.arr: [1.2.3.4.5.6.7.8.9.10.11]}; },methods: {addFunc:function(){
this.arr[0] + +},switchIsShow:function(){
this.isShow=!this.isShow
}
},
components: {
coma: {
template: ` < h1 class = "com" v - bind: test = "appName" > {{appName}} own local component attributes: {{appAttr}} < / h1 > `.data: function () {
return {
appName: "moush".appAttr:"attr"}; }},}});Copy the code
Clicking the addFunc button will ++ in the list ·1 and the browser view will update accordingly
Project address: github.com/moushicheng… It’s not too much to fool a star
Main process
class moushVue implements VM{
$options: any;
$data: any;
$el: HTMLElement;
$parentVm: VM;
$childrenVm: VM[];
$methods:any
$oldNode:any;
constructor(options: OPTIONS) {
this.$options = options;
this.init();
this.mount();
this.observe();
}
protected init() {
new init(this);
}
protected mount() {
this.$options.beforeMount.call(this);
this.$el =
typeof this.$options.el == "string"
? document.querySelector(this.$options.el)
: this.$options.el;
this.$options.mounted.call(this);
}
protected observe() {
new Observer('$data'.this.$data,this); // Make the internal data observable
new Complier(this); // Analyze the internal nodes of el and generate corresponding watcher}}Copy the code
Without worrying about the details of what the function does, let’s take a general view of what the constructor does internally: this.init(); Initialize some data
this.mount(); Attach this.observe() to the user’s el; It’s easy to make data observable and compile, but the key is to understand observe
new Observer('$data',this.$data,this); New Complier(this); // Analyze the internal nodes of el and generate corresponding watcherCopy the code
What did you do
Response principle
Vue2. X uses the Object.defineProperty API to monitor data, but our framework doesn’t do that because it doesn’t monitor data that way
arr[0]=1;
Copy the code
Array index changes. In our framework, we use the ES6 Proxy Proxy object to monitor object changes. Now you can try copying this code to the browser console and doing some simple debugging to experience Proxy
const obj={a:1}
const proxy = new Proxy(obj, {
get(obj, property) {
console.log('@get:'+property)
return obj[property];
},
set(obj, property, value) {
console.log('@set:'+property+value)
obj[property] = value;
return true; }});/ / debugging
proxy.a=2 //@set:a2
proxy.a //@get:a
Copy the code
So how do you monitor the data option in vUE? That’s of course the same thing with Proxy, when you’re updating data, you just notify the dependency in Proxy GET that it’s updating so what’s a dependency? This is not easy to explain, but generally refers to HTML that is directly related to data such as:
<div>
{{message}}
</div>
new moushVue({
...
data: {message:"Hello,world"}... })Copy the code
The update method will directly operate on the DOM update view. The update method will have different operations for different templates (different callback cb). This is related to the Complier compilation in our main process. It creates different types of CB based on HTML to service data updates:
class Watcher {
vm:VM
cb:Function;
getter:any;
value:any;
constructor (vm,initVal,expOrFn,cb) {
this.vm = vm;
this.cb = cb;
if(isType(expOrFn,'String'))this.getter = parsePath(expOrFn)
else if(isType(expOrFn,'Function'))this.getter=expOrFn
this.value = this.get() // Collect dependencies
this.value=initVal
}
get () {
window.target = this;
let value = this.getter(this.vm.$data)
window.target = undefined;
return value
}
update () {
const oldValue = this.value
// this.value = this.get() // Do not fire getters when updating otherwise dependencies will be collected
this.value = this.getter(this.vm.$data)
this.cb.call(this.vm, this.value, oldValue)
}
}
Copy the code
We’ll talk about GET, dependency collection, shortly. Ok, so let’s go back to data monitoring
Observer
The Observer is a detector that deeply recursively monitors all data within the data option.
class Observer{
$value: any;
$parent: any;
$key:string
dep: any;
constructor(key,value, parent) {
this.$key=key;
this.$value = value;
this.$parent = parent;
this.dep = new Dep();
def(value, "__ob__".this); // this.__ob__=value
this.walk(value);
this.detect(value, parent);
}
private walk(obj: Object | Array<any>) {
for (const [key, val] of Object.entries(obj)) {
if (typeof val == "object") {
// Determine both arrays and objects
newObserverNext(key,val, obj); }}}private detect(val: any, parent: any) {
const dep = this.dep
const key=this.$key
const proxy = new Proxy(val, {
get(obj, property) {
if(! obj.hasOwnProperty(property)) {return;
}
dep.depend(property);
return obj[property];
},
set(obj, property, value) {
obj[property] = value;
dep.notify(property);
if(parent.__ob__)parent.__ob__.dep.notify(key)
return true; }}); parent[this.findKey(parent, val)] = proxy;
}
// Find the key that points to the value of the object and its value.
FindKey (obj,1) => findKey(obj,1) =
private findKey(obj, value, compare = (a, b) => a === b) {
return Object.keys(obj).find((k) = >compare(obj[k], value)); }}Copy the code
Every object needs to have a DEP for data monitoring. What is a DEP? Because a data may correspond to multiple dependencies, it is necessary to make a unified management of all dependencies corresponding to the data, and this unified management is realized by DEP. The main process of the Observer, initializing, creating the DEP, and then walking (recursively analyzing whether there are nested objects inside the object, and monitoring the nested objects if there are.
walk(obj: Object | Array<any>) {
for (const [key, val] of Object.entries(obj)) {
if (typeof val == "object") {
// Determine both arrays and objects
newObserverNext(key,val, obj); }}}Copy the code
Next, detect
private detect(val: any, parent: any) {
const dep = this.dep
const key=this.$key
const proxy = new Proxy(val, {
get(obj, property) {
if(! obj.hasOwnProperty(property)) {return;
}
dep.depend(property);
return obj[property];
},
set(obj, property, value) {
obj[property] = value;
dep.notify(property);
if(parent.__ob__)parent.__ob__.dep.notify(key)
return true; }}); parent[this.findKey(parent, val)] = proxy;
}
Copy the code
Depend update dep. Notify the dependency update dep. Notify the dependency update deP. Notify the dependency update deP. The last
parent[this.findKey(parent, val)] = proxy;
Copy the code
Change the parent object that references our new detection object to a proxy. This makes the entire object reactive
In summary, the purpose of the Observer is to deeply recurse all the data inside the object and perform detection, notifying the agent to update the DEP when the internal data is updated, and notifying the agent to make the DEP dependent collection when the internal data is acquired.
dep
Let’s take a look at DEP, because the source code is very simple, so directly on the source code
class depNext {
subs: Map<string.Array<Watcher>>;
constructor() {
this.subs = new Map(a); }addSub(prop, target) {
const sub = this.subs.get(prop);
if(! sub) {this.subs.set(prop, [target]);
return;
}
sub.push(target);
}
// Add a dependency
depend(prop) {
if (window.target) {
this.addSub(prop, window.target); // Don't be surprised if window.target is covered later}}// Notify all dependencies of updates
notify(prop) {
const watchers = this.subs.get(prop);
if(! watchers)return;
for (let i = 0, l = watchers.length; i < l; i++) { watchers[i].update(); }}}Copy the code
Note that subs is a Map object that maps all the data in the object, and each mapped data corresponds to a dependency array. This is the above mentioned data may correspond to multiple dependencies
Depend on the collection
Dependency collection simply means collecting dependencies while fetching data. Post the source code in the Observer again
const proxy = new Proxy(val, {
get(obj, property) {
if(! obj.hasOwnProperty(property)) {return;
}
dep.depend(property);
return obj[property];
},
set(.){...}
});
Copy the code
This. AddSub (prop, window.target); this.addSub(prop, window.target);
Window. target is actually an instance of Watcher. When creating a watcher, the watcher will assign itself to the global window.target and fetch data from it. Collect this Watcher.
Get () {window.target = this;
let value = this.getter(this.vm.$data)
window.target = undefined;
return value
}
Copy the code
Getter is created by parsePath in the constructor. ParsePath takes the value represented by a string path of the form ‘data.a.b.c’ from the actual data object, thus completing the dependency collection
/** * Parse simple path. * Extract the value represented by a string path of the form 'data.a.b.c' from the real data object * for example: * data = {a:{b:{c:2}}} * parsePath('a.b.c')(data) // 2 */
export function parsePath(path) {
const bailRE = /[^\w.$]/;
const segments = path.split(".");
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if(! obj)return;
if (bailRE.test(segments[i])) {
//this.arr[0] this[arr[0]]
const match = segments[i].match(/(\w+)\[(.+)\]/);
obj = obj[match[1]];
obj = obj[match[2]].continue;
}
obj = obj[segments[i]];
}
return obj;
};
}
Copy the code
conclusion
The Observer deeply recurses the data inside the data option to make it responsive. The DEP inside the Observer instance is responsible for managing dependencies. When data is fetched, the DEP collects dependencies, and when data is updated, the DEP notifes dependencies of updates. The dependency is watcher, which updates the specific view by calling cb (the callback function) on it. The CB calls to the Watcher update view are determined at the time of the Complier build, which will be explained in a future article.
To learn more about the above process, you can click, directly see the project source github.com/moushicheng…