Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

Hongmun's Javascript framework is interpreted as a line-by-line source code


May 31, 2021 Article blog


Table of contents


This article is from the public number: justjavac, author justjavac

I once introduced Hongmun's Javascript framework, these days finally compiled the JS warehouse through, during which I stepped on a lot of pits, but also contributed a few PRs to Hongmun.

Today we're going to analyze the JS framework in the Hongmun system line by line.

All of the code in this article is based on the current latest version of Hongmun (version 677ed06 with submission date 2020-09-10).

Hongmun's use of Javascript to develop GUI is a pattern similar to WeChat's small program and light application. I n this MVVM mode, V is actually borne by C. The Javascript code is just one of the ViewModel layers.

The Hongmun JS framework is zero-dependent and uses only some npm packages during development packaging. T he packaged code does not rely on any npm packages. Let's first look at what the JS code written using the Hon Mun JS framework looks like.

export default {
  data: {
    return { count: 1 };
  },
  increase() {
    ++this.count;
  },
  decrease() {
    --this.count;
  },
}

If I don't tell you it's Hongmun, you'll even think it's vue or a small program. If you take the JS out of use separately (out of the HungMun system), the code looks like this:

const vm = new ViewModel({
  data() {
    return { count: 1 };
  },
  increase() {
    ++this.count;
  },
  decrease() {
    --this.count;
  },
});
console.log(vm.count); // 1
vm.increase();
console.log(vm.count); // 2
vm.decrease();
console.log(vm.count); // 1

All JS code in the repository implements a responsive system that acts as ViewModel in the MVVM.

Let's analyze it line by line.

There are four directories in the src directory, for a total of 8 files. O ne of them is a unit test. T here is also 1 performance analysis. R emove the 2 index .js files, and the total number of useful files is 4. It is also the focus of this analysis.

src
├── __test__
│   └── index.test.js
├── core
│   └── index.js
├── index.js
├── observer
│   ├── index.js
│   ├── observer.js
│   ├── subject.js
│   └── utils.js
└── profiler
    └── index.js

The first is the entry file, src/index.js with only 2 lines of code:

import { ViewModel } from './core';
export default ViewModel;

It's actually a re-export. Another similar file is src/observer/index.js which is also 2 lines of code:

export { Observer } from './observer';
export { Subject } from './subject';

Observer and subject implement an observer pattern. S ubject is the subject, that is, the observer. O bserver is the observer. T he subject needs to be proactively notified when there are any changes. This is responsive.

Both files are used in src/observer/utils.js so let's analyze the utils files first. Divided into 3 parts.

The first part

export const ObserverStack = {
  stack: [],
  push(observer) {
    this.stack.push(observer);
  },
  pop() {
    return this.stack.pop();
  },
  top() {
    return this.stack[this.stack.length - 1];
  }
};

The first is to define a stack for the observer, following the principle of last-in, first-out, internal use of stack arrays for storage.

  • The in-stack operation push like the push function of the array, observer an observer observer at the top of the stack.
  • The out-of-stack operation pop like the pop function, removes the observer at the top of the stack and returns the deleted observer.
  • Stack top element top unlike pop operation, top is to take out the top element of the stack, but not delete.

Part II

export const SYMBOL_OBSERVABLE = '__ob__';
export const canObserve = target => typeof target === 'object';

A string constant SYMBOL_OBSERVABLE is SYMBOL_OBSERVABLE For the convenience of the back.

A function canObserve is defined, and whether the target can be observed. O nly objects can be observed, so typeof used to determine the type of target. W ait, there seems to be something wrong. I f target is null the function also returns true I f null is not observable, then this is a bug. (At the time of writing, I had mentioned a PR and asked if this behavior was expected.)

Part III

export const defineProp = (target, key, value) => {
  Object.defineProperty(target, key, { enumerable: false, value });
};

There's nothing to explain this, which is that Object.defineProperty code is too long to define a function to avoid code duplication. Let's analyze the observer src/observer/observer.js in 4 parts.

The first part

export function Observer(context, getter, callback, meta) {
  this._ctx = context;
  this._getter = getter;
  this._fn = callback;
  this._meta = meta;
  this._lastValue = this._get();
}

Constructor. Accept 4 parameters.

context current observer is in the afternoon and the type is ViewModel When the third argument callback is called, this the context of the function.

getter type is a function that gets the value of a property.

callback type is a callback function that executes when a value changes.

meta metadata. Observers do not pay attention meta metadata.

On the last line of the constructor, this._lastValue = this._get() Let's analyze _get function.

Part II

Observer.prototype._get = function() {
  try {
    ObserverStack.push(this);
    return this._getter.call(this._ctx);
  } finally {
    ObserverStack.pop();
  }
};

ObserverStack is the stack analyzed above to store all observers. P ut the current observer on the stack and get the current value _getter C ombined with the first part of the constructor, this value is stored _lastValue property. Once this process has been performed, the observer has been initialized.

Part III

Observer.prototype.update = function() {
  const lastValue = this._lastValue;
  const nextValue = this._get();
  const context = this._ctx;
  const meta = this._meta;
  if (nextValue !== lastValue || canObserve(nextValue)) {
    this._fn.call(context, nextValue, lastValue, meta);
    this._lastValue = nextValue;
  }
};

This section implements the Dirty Checking mechanism when data is updated. C ompare the updated value with the current value, and if it is different, execute the callback function. I f this callback function is a rendering UI, you can render on demand. If the values are the same, then check to see if the new values you set can be observed and decide whether or not to execute the callback function.

Part IV

Observer.prototype.subscribe = function(subject, key) {
  const detach = subject.attach(key, this);
  if (typeof detach !== 'function') {
    return;
  }
  if (!this._detaches) {
    this._detaches = [];
  }
  this._detaches.push(detach);
};
Observer.prototype.unsubscribe = function() {
  const detaches = this._detaches;
  if (!detaches) {
    return;
  }
  while (detaches.length) {
    detaches.pop()();
  }
};

Subscribe and unsubscribe.

We used to talk about observers and observers. T here's another way to say about the Observer pattern, called subscription/publish mode. This part of the code, on the other hand, implements a subscription to the subject.

The attach method is called first to subscribe. I f the subscription succeeds, subject.attach method returns a function that is unsubscribed when called. In order to be able to unsubscribe in the future, this return value must be saved.

The implementation of subject should have been guessed by many people. O bservers subscribe to subject, so all subject needs to do is notify the observer when the data changes. How does subject know that the data has changed, using Object.defineProperty for property hijacking, as with vue2.

Let's analyze the src/observer/subject.js in 7 parts.

The first part

export function Subject(target) {
  const subject = this;
  subject._hijacking = true;
  defineProp(target, SYMBOL_OBSERVABLE, subject);
  if (Array.isArray(target)) {
    hijackArray(target);
  }
  Object.keys(target).forEach(key => hijack(target, key, target[key]));
}

Constructor. T here's basically nothing hard about it. S et _hijacking property to true to indicate that the object has been hijacked. Object.keys hijacks each property throughout history. If it is an array, call hijackArray

Part II Two static methods.

Subject.of = function(target) {
  if (!target || !canObserve(target)) {
    return target;
  }
  if (target[SYMBOL_OBSERVABLE]) {
    return target[SYMBOL_OBSERVABLE];
  }
  return new Subject(target);
};
Subject.is = function(target) {
  return target && target._hijacking;
};

Subject's constructor is not called directly externally, but is encapsulated in Subject.of static method.

If the target cannot be observed, return the target directly.

If target[SYMBOL_OBSERVABLE] is not undefined the target has been initialized.

Otherwise, the constructor is called to initialize Subject.

Subject.is is used to determine whether the target has been hijacked.

Part III

Subject.prototype.attach = function(key, observer) {
  if (typeof key === 'undefined' || !observer) {
    return;
  }
  if (!this._obsMap) {
    this._obsMap = {};
  }
  if (!this._obsMap[key]) {
    this._obsMap[key] = [];
  }
  const observers = this._obsMap[key];
  if (observers.indexOf(observer) < 0) {
    observers.push(observer);
    return function() {
      observers.splice(observers.indexOf(observer), 1);
    };
  }
};

This method is familiar, yes, as called in Observer.prototype.subscribe above. T he role is that an observer uses to subscribe to a topic. This approach is "How the topic is subscribed".

Observers maintain a hash table _obsMap for this topic. T he key of the hash table is the key that needs to be subscribed to. F or example, one observer subscribes to changes name property, while another observer subscribes to changes in the age property. And property changes can be subscribed by multiple observers at the same time, so the value stored by the hash table is an array, and each element of the data is an observer.

Part IV

Subject.prototype.notify = function(key) {
  if (
    typeof key === 'undefined' ||
    !this._obsMap ||
    !this._obsMap[key]
  ) {
    return;
  }
  this._obsMap[key].forEach(observer => observer.update());
};

When a property changes, notify the observer who subscribes to the property. T raverse each observer and call the observer's update method. As we mentioned above, dirty checks are done within this method.

Part V

Subject.prototype.setParent = function(parent, key) {
  this._parent = parent;
  this._key = key;
};
Subject.prototype.notifyParent = function() {
  this._parent && this._parent.notify(this._key);
};

This part is used to deal with the problem of nested property. It's something like this: { user: { name: 'JJC' } }

Part 6

function hijack(target, key, cache) {
  const subject = target[SYMBOL_OBSERVABLE];
  Object.defineProperty(target, key, {
    enumerable: true,
    get() {
      const observer = ObserverStack.top();
      if (observer) {
        observer.subscribe(subject, key);
      }
      const subSubject = Subject.of(cache);
      if (Subject.is(subSubject)) {
        subSubject.setParent(subject, key);
      }
      return cache;
    },
    set(value) {
      cache = value;
      subject.notify(key);
    }
  });
}

This section shows how to use Object.defineProperty for property hijacking. W hen you set a property, set (value) is called, a new value is set, and then the notify method of subject is called. T here is no check here, and calls as soon as the property is set, even if the property has the same new value as the old value. Notify informs all observers.

Part 7 Hijack array method.

const ObservedMethods = {
  PUSH: 'push',
  POP: 'pop',
  UNSHIFT: 'unshift',
  SHIFT: 'shift',
  SPLICE: 'splice',
  REVERSE: 'reverse'
};
const OBSERVED_METHODS = Object.keys(ObservedMethods).map(
    key => ObservedMethods[key]
);

ObservedMethods the array functions that need to be hijacked. The first capital is used to do key, and the lowercase is the method that needs to be hijacked.

function hijackArray(target) {
  OBSERVED_METHODS.forEach(key => {
    const originalMethod = target[key];
    defineProp(target, key, function() {
      const args = Array.prototype.slice.call(arguments);
      originalMethod.apply(this, args);
      let inserted;
      if (ObservedMethods.PUSH === key || ObservedMethods.UNSHIFT === key) {
        inserted = args;
      } else if (ObservedMethods.SPLICE) {
        inserted = args.slice(2);
      }
      if (inserted && inserted.length) {
        inserted.forEach(Subject.of);
      }
      const subject = target[SYMBOL_OBSERVABLE];
      if (subject) {
        subject.notifyParent();
      }
    });
  });
}

The hijacking of arrays is different from objects, and Object.defineProperty cannot be used.

We need to hijack 6 array methods. These are head addition, head deletion, tail addition, tail deletion, replacement/deletion of a few items, array reversal.

The hijacking of the array is achieved by rewriting the array method. B ut here's a note that every element of the data has been observed, but when new elements are added to the array, they haven't been observed. Therefore, the code also needs to determine if the current method is push unshift splice then the new elements need to be placed in the observer queue.

The above is W3Cschool编程狮 about Hongmun's Javascript framework line-by-line source code interpretation of the relevant introduction, I hope to help you.