I have introduced the Javascript framework of Hongmeng in the previous article, these days I finally compiled the JS warehouse through, during which I stepped on a lot of holes, but also contributed several PR to Hongmeng. Today we will analyze the JS framework in hongmeng system line by line.

All the code in this article is based on the current latest version of Hongmeng (version 677ED06, submission date 2020-09-10).

Hongmeng system uses JavaScript to develop GUI, which is similar to wechat small program and light application mode. In this MVVM pattern, V is actually carried by C++. The JavaScript code is just the ViewModel layer.

The Hongmeng JS framework is zero-dependent and only uses some NPM packages in the development packaging process. The packaged code is not dependent on any NPM package. Let’s take a look at the use of hongmeng JS framework written JS code exactly what looks like.

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

If I didn’t tell you it was hongmeng, you would even think it was vue or applets. If you use JS on its own (out of the 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
Copy the code

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

So let’s do this line by line.

There are four directories in the SRC directory with a total of eight files. One of them is a unit test. There is also a performance analysis. Remove the two index.js files, and there are four useful files in total. It is also the focus of this paper.

SRC ├ ─ ─ __test__ │ └ ─ ─ the index, the test. The js ├ ─ ─ the core │ └ ─ ─ index. The js ├ ─ ─ index. The js ├ ─ ─ the observer │ ├ ─ ─ index. The js │ ├ ─ ─ the observer, js │ ├─ ├─ ├─ ├─ ├─ ├─Copy the code

The first entry file, SRC /index.js, has only 2 lines of code:

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

It’s just a reexport.

Another similar file, SRC /observer/index.js, is also 2 lines of code:

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

The Observer and Subject implement an observer pattern. Subject is the subject being observed. An observer is an observer. The subject needs to be actively notified of any changes in the subject. That’s the response.

Both files use SRC /observer/utils.js, so let’s examine the utils file first. It is divided into three 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]; }};Copy the code

The first is to define a stack for storing observers, following the principle of last in first out, internal storage using stack array.

  • Into the stack,push, and arraypushThe same function puts an observer observer at the top of the stack.
  • The stack,pop, and arraypopThe delete () function removes the observer at the top of the stack and returns the deleted observer.
  • Take the top element of the stacktop, andpopThe operation is different,topThe top element is removed from the stack, but not removed.

The second part

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

Defines a string constant SYMBOL_OBSERVABLE. For convenience in the back.

A function canObserve is defined to determine whether a target can be observed. Only objects can be observed, so use typeof to determine the typeof the target. Wait, there’s something wrong. The function also returns true if target is null. If null is not observable, then it is a bug. (AT the time of writing THIS ARTICLE I have already mentioned a PR and asked if this behavior is expected).

The third part

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

There is nothing to explain here, except that the Object.defineProperty code is too long, so define a function to avoid code duplication.

SRC /observer/observer.js

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();
}
Copy the code

Constructor. Accept four parameters.

Context The context in which the current observer is located, of type ViewModel. When the third argument callback is called, this of the function is the context.

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

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

Meta metadata. The Observer does not care about meta metadata.

On the last line of the constructor, this._lastValue = this._get(). Let’s look at the _get function.

The second part

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

The ObserverStack is the stack that stores all the observers analyzed above. Pushes the current observer onto the stack and gets the current value via _getter. Combined with the constructor from the first part, this value is stored in the _lastValue attribute.

Once this process is complete, the observer is initialized.

The third part

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; }};Copy the code

This section implements the Dirty checking mechanism for data updates. Compare the updated value to the current value and, if different, execute the callback function. If the callback function is to render the UI, then on-demand rendering can be implemented. If the values are the same, then check to see if the new values can be observed and decide whether to execute the callback at all.

The fourth part

Observer.prototype.subscribe = function(subject, key) {
  const detach = subject.attach(key, this);
  if (typeofdetach ! = ='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()(); }};Copy the code

Subscribe and unsubscribe.

We talked a lot about the observer and the observed. There’s another term for the observer model, and it’s called subscribe/publish. This part of the code implements a subscription to the subject.

Subscribe by calling the attach method of the topic. If the subscription is successful, the subject-. attach method returns a function that will be unsubscribed when called. This return value must be saved in order to be able to unsubscribe in the future.

The implementation of Subject should have been guessed by many. The observer subscribes to the Subject, so all the subject needs to do is notify the observer when the data changes. How does the subject know that the data has changed? The same mechanism as VUe2 uses Object.defineProperty for property hijacking.

SRC /observer/subject.js

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]));
}
Copy the code

Constructor. There are basically no difficulties. Set _hijacking property to true, indicating that the object has been hijacked. Keys hijacks each property through traversal. If it is an array, hijackArray is called.

The second part

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;
};
Copy the code

The Subject constructor is not directly called externally, but is wrapped in a subject.of static method.

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

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

Otherwise, the constructor is called to initialize the Subject.

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

The third part

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); }; }};Copy the code

This method is very familiar, right, is above the Observer. The prototype. Call the subscribe. Is used by an observer to subscribe to a topic. The method is “how topics are subscribed to”.

The observer maintains the hash table _obsMap for this topic. The key of the hash table is the key to subscribe to. For example, one observer subscribes to a change in the name attribute, while another subscribes to a change in the age attribute. Moreover, property changes can be subscribed to by multiple observers at the same time, so the hash table stores values as an array, and each element of the data is an observer.

The fourth part

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

Observers subscribed to the property are notified when the property changes. Iterate over each observer and call the observer’s UPDATE method. As we mentioned above, dirty checking is done in this method.

The fifth part

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

Subject.prototype.notifyParent = function() {
  this._parent && this._parent.notify(this._key);
};
Copy the code

This section is used to deal with the problem of nested objects. An object like this: {user: {name: ‘JJC’}}.

The sixth part

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); }}); }Copy the code

This section shows how to use Object.defineProperty for attribute hijacking. When the property is set, set(value) is called, the new value is set, and the notify method of the subject is called. There is no checking, it is called as soon as the property is set, even if the new value of the property is the same as the old value. Notify notifies all observers.

The seventh part

Hijack the 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]
);
Copy the code

ObservedMethods defines the array functions that need to be hijacked. Uppercase is used for keys and lowercase is used for methods that need 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(); }}); }); }Copy the code

Unlike objects, arrayhijacking cannot use Object.defineProperty.

We need to hijack six array methods. They are header add, header delete, tail add, tail delete, replace/delete some items, array inversion.

Array hijacking is realized by rewriting the array method. One caveat here is that every element of the data has been observed, but not yet observed when new elements are added to the array. So the code also needs to determine if the current method is push, unshift, splice, then it needs to put the new element in the observer queue.

The other two files, unit tests and performance analysis, will not be analyzed here.