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:

  1. Declarative rendering {{message}}
  2. Conditional render V-if
  3. List render V-for
  4. Event processing V-ON
  5. Component rendering < Component >

Frame features:

  1. Build with typescript+ Webpack
  2. The framework manipulates the real DOM directly without using the VDOM
  3. Only part of vue’s functionality is implemented, because it’s a bit of a brick to try to do it all.
  4. 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…