Touch hands to teach you to implement a simple vue responsive principle

Posted by ir4z0r on Thu, 03 Feb 2022 06:36:40 +0100

Write in front

In 1202, what's the use of implementing another mvvm framework? Actually exactly.... It's no use. No matter how well you write, it is difficult to roll over the mature vue,react and other frameworks. However, for individuals, implementing a simple version of vue by yourself will certainly deepen your understanding of the principle of the source code. It still makes sense.

Final effect:

Realize the following functions:

  1. Declarative rendering {{message}}
  2. Conditional rendering v-if
  3. List rendering v-for
  4. Event handling v-on
  5. Component rendering < component > < / component >
    Frame features:
  6. Build with typescript+webpack
  7. The framework operates directly on the real dom without using vdom
  8. Only part of the functions of vue are realized, because it is a little difficult to realize all the details.
  9. Unlike vue2 X, the array in moush Vue can update the view through subscript index

No about vdom:
This is also vue1 X and 2 The difference of X, but without vdom, it doesn't affect your understanding of Vue principle at all, because you understand 1 Principle of X, 2 X is nothing more than adding vdom and related diff algorithm on the original basis.

demonstration:

template:

  <div id="app">
    <div class="ageContent">
      <p>
        {{name}}What is your age{{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>

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">Local component{{appName}}Self attribute:{{appAttr}}</h1>`,
        data: function () {
          return {
            appName: "moush",
            appAttr:"attr",
          };
        },
      },
    },
  });

Click the add function button, and the browser view will be updated accordingly

Address of the project:
https://github.com/moushicheng/moush-vue
Isn't it too much to cheat star

Mainstream 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 internal data observable
    new Complier(this); //Analyze el internal nodes and generate corresponding watcher s
  }
}

Let's not care about the details of the function. Let's have a general understanding. Let's see that there are three things done inside the constructor:
this.init(); Initialize some data

this.mount(); Mount the el sent by the user
this.observe(); Make the data observable and perform compilation
The first two are relatively simple. Our core is to understand the language in observe

  new Observer('$data',this.$data,this); //Make internal data observable
  new Complier(this); //Analyze el internal nodes and generate corresponding watcher s

What the hell did you do

Responsive principle

To monitor the data changes inside an Object (both Array and Object are called objects here), we have to use some methods, vue2 X uses Object The api defineproperty is used to monitor data, but this is not the case in our framework, because it cannot be monitored

 arr[0]=1;

Changes caused by array subscript index.
In our framework, we use the Proxy object of es6 to monitor the changes of objects
Next, you can try to copy this code to the browser console, and then conduct 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

So what should I do to monitor the data option in vue? Of course, the same applies to Proxy. When updating data, just notify the dependent update in Proxy get
So what is dependence? This is not easy to explain. Generally speaking, it is the HTML template directly related to the data
For example:

<div>
{{message}}
</div>

new moushVue({
...
data:{
  message:"Hello,world"
}
...
})

To be clear, the Watcher object built internally is to notify the Watcher to execute its update method. The update method will directly operate the dom update view and have different operations on different templates (different callback cb), which is related to the Complier compilation in our main process, It will create different types of CBS according to HTML templates to serve 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() //Collection dependency
      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 trigger the getter when updating, otherwise the dependency will be collected
      this.value = this.getter(this.vm.$data)
      this.cb.call(this.vm, this.value, oldValue)
    }
  }

About get, dependency collection, we'll talk about it soon.
OK, so let's go back to data monitoring

Observer

observer is a detector. It will deeply recursively monitor all data inside the option data.

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); //Equivalent to 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") {
        //Judge arrays and objects at the same time
        new ObserverNext(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 pointing to the value through the object and a value in the object
  //For example, obj A = 1, findkey (obj, 1) = > return a
  private findKey(obj, value, compare = (a, b) => a === b) {
    return Object.keys(obj).find((k) => compare(obj[k], value));
  }
}

Data monitoring of each object requires a dep. what is a dep? Because one data may correspond to multiple dependencies, all dependencies corresponding to the data must be managed in a unified way, which is realized by Dep.
The main process of observer is to initialize, create dep, and then walk (recursively analyze whether there are nested objects inside the object. If so, the nested objects will also be monitored.

walk(obj: Object | Array<any>) {
    for (const [key, val] of Object.entries(obj)) {
      if (typeof val == "object") {
        //Judge arrays and objects at the same time
        new ObserverNext(key,val, obj);
      }
    }
  }

Then 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;
  }

First proxy val (the object passed in), and then collect the dependency dep.depend in get
Notify the dependency update dep.notify in the set. When updating, a layer of penetration notification will be made at the same time, and the parent object will also be updated.
last

parent[this.findKey(parent, val)] = proxy;

We change the newly detected object referenced in the parent object into an agent. This makes the entire object responsive

To sum up, the function of Observer is to deeply recursively analyze all the data inside the object and detect it. When the internal data is updated, the Observer will notify the agent to update the DEP, and when the internal data is obtained, the Observer will notify the agent to collect the dep dependency.

dep

Next, let's analyze dep, because the source code is very simple, so go directly to the source code

 class depNext {
  subs: Map<string, Array<Watcher>>;
  constructor() {
    this.subs = new Map();
  }

  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 Target will talk later
    }
  }
  // Notify all dependent 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();
    }
  }
}

Note that sub is a Map object, which will Map all the data in the object, and each mapped data corresponds to a dependency array. This is why one data mentioned above may correspond to multiple dependencies

Dependency collection

Dependency collection is simply to collect dependencies when obtaining data
Paste the source code in Observer again

const proxy = new Proxy(val, {
      get(obj, property) {
        if (!obj.hasOwnProperty(property)) {
          return;
        }
        dep.depend(property);
        return obj[property];
      },
      set(...){...}
    });

During data acquisition, see? dep.depend collects dependencies, and then adds the corresponding dependencies to the dependency array in the dependency collector dep
This is how it is collected in dep
this.addSub(prop, window.target);

window.target is actually an instance of the watcher. When creating the watcher, the Watcher will assign itself to the global window Target, and then get the data, and the data Proxy will dep.depend to collect the watcher.

watcher Dependency collection in
    get () {
      window.target = this;
      let value = this.getter(this.vm.$data)
      window.target = undefined;
      return value
    }

this.getter is created by parsePath in the constructor. parsePath will put a shape such as' data a. The value represented by the string path of B.C 'is taken from the real data object, which completes the dependency collection

  /**
   * Parse simple path.
   * Put a shape like 'data a. The value represented by the string path of B.C 'is taken 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;
  };
}

summary

Observer deeply recursively analyzes the data inside the option data to make it responsive.
dep inside the observer instance is responsible for unified management of dependencies,
dep will collect dependencies when acquiring data, and notify dependency updates when updating data
The dependency is the watcher, which is responsible for updating the view by calling the cb (callback function) on it.
The cb called by the watcher to update the view is determined when the Complier compiles and builds, which will be explained in subsequent articles.

If you want to know more about the above process, you can click to see the project source code directly
https://github.com/moushicheng/moush-vue

file

#Touch hands to teach you to realize a simple vue (1) responsive principle
#Touch hands to teach you to implement a simple vue (2) start writing observer
#Touch hands to teach you to implement a simple vue (3) start writing dep and watcher

Topics: Vue