Построчный анализ JavaScript-фреймворка системы Hongmeng

открытый источник JavaScript

я представил в предыдущей статьеФреймворк Hongmeng для Javascript, За последние несколько дней склад JS наконец-то был скомпилирован и сдан.За этот период я ​​наступил на множество ям и внес несколько PR в Hongmeng. Сегодня мы построчно разберем JS-фреймворк в системе Hongmeng.

Все коды в этой статье основаны на текущей последней версии Hongmeng (версия677ed06, дата подачи 2020-09-10).

Использование JavaScript для разработки графического интерфейса в системе Hongmeng — это режим, аналогичный апплету WeChat и легкому приложению. В этом шаблоне MVVM V фактически выполняется C++. Код JavaScript — это всего лишь слой ViewModel.

Фреймворк Hongmeng JS не имеет зависимостей и использует только некоторые пакеты npm в процессе разработки и упаковки. Упакованный код не зависит ни от какого пакета npm. Давайте сначала посмотрим, как выглядит код JS, написанный с использованием фреймворка Hongmeng JS.

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

Если я вам не скажу, что это Hongmeng, вы даже подумаете, что это vue или апплет. Если вы используете только JS (вне системы Hongmeng), код выглядит следующим образом:

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

Весь код JS в репозитории реализуетОтзывчивая система, который действует как ViewModel в MVVM.

Ниже мы разбираем построчно.

Всего в каталоге src 4 каталога с 8 файлами. 1 из них — модульный тест. Существует также 1 анализ производительности. Удалите еще 2 файла index.js, всего полезных файлов 4. Это также является предметом данного анализа.

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

Первый — это входной файл, src/index.js, содержащий всего 2 строки кода:

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

По сути, это реэкспорт.

Еще один похожий файл — src/observer/index.js, тоже 2 строчки кода:

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

наблюдатель и субъект реализуют шаблон наблюдателя. субъект есть субъект, то есть наблюдаемое. наблюдатель есть наблюдатель. Когда есть какие-либо изменения в предмете, наблюдатель должен быть активно уведомлен. Это отзывчивость.

Оба этих файла используют src/observer/utils.js, поэтому давайте сначала проанализируем файл utils. Делится на 3 части.

первая часть

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

Во-первых, определить стек для хранения наблюдателей, следуя принципу «последний вошел, первый ушел и используется внутри».stackмассив для хранения.

  • push-операцияpush, и массивpushПодобно функциям, поместите наблюдателя-наблюдателя на вершину стека.
  • всплывающая операцияpop, и массивpopКак и функция, она удаляет наблюдателя из вершины стека и возвращает удаленного наблюдателя.
  • получить верхний элемент стекаtoppopоперация отличается,topБерет верхний элемент стека, но не удаляет его.

Вторая часть

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

определяет строковую константуSYMBOL_OBSERVABLE. Для удобства позже.

определяет функциюcanObserve, можно ли наблюдать за целью. Можно наблюдать только объекты, поэтому используйтеtypeofдля определения типа цели. Подождите, кажется, что-то не так. еслиtargetзаnull, функция также возвращаетtrue. еслиnullнезаметно, то это ошибка. (На момент написания этой статьи я уже подал PR и спросил, является ли такое поведение желаемым).

третья часть

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

Этому нет никакого объяснения, это простоObject.definePropertyКод слишком длинный, определите функцию, чтобы избежать дублирования кода.

Далее разберем обозреватель src/observer/observer.js, который разбит на 4 части.

первая часть

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

Конструктор. Принимает 4 параметра.

contextКонтекст, в котором находится текущий наблюдатель, типаViewModel. Когда вызывается обратный вызов третьего параметра, функцияthisЭто оноcontext.

getterТип — это функция, которая получает значение свойства.

callbackТип — это функция, функция обратного вызова, которая выполняется при изменении значения.

metaметаданные. Наблюдатель (наблюдатель) не обращает вниманияmetaметаданные.

В последней строке конструктораthis._lastValue = this._get(). Давайте проанализируем_getфункция.

Вторая часть

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

ObserverStackИменно стек используется для хранения всех проанализированных выше наблюдателей. Поместите текущего наблюдателя в стек и передайте_getterПолучить текущее значение. В сочетании с конструктором первой части это значение сохраняется в_lastValueв свойствах.

После выполнения этого процесса наблюдатель был инициализирован.

третья часть

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

Эта часть реализует механизм грязной проверки при обновлении данных. Сравните обновленное значение с текущим значением и, если они отличаются, выполните функцию обратного вызова. Если эта функция обратного вызова предназначена для визуализации пользовательского интерфейса, то может быть достигнута визуализация по требованию. Если значение то же самое, то проверьте, можно ли наблюдать новый набор значений, а затем решите, выполнять ли функцию обратного вызова или нет.

четвертая часть

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

Подписка и отписка.

Мы часто говорим о наблюдателе и наблюдаемом ранее. На самом деле есть еще один способ назвать шаблон наблюдателя, называемый шаблоном подписки/публикации. Эта часть кода реализует подписку на тему.

сначала назовите темуattachспособ подписки. Если подписка прошла успешно,subject.attachМетод возвращает функцию, которая при вызове отписывается. Это возвращаемое значение необходимо сохранить, чтобы иметь возможность отписаться в будущем.

О реализации темы должны были догадаться многие. Наблюдатель подписывается на субъект, после чего все, что нужно сделать субъекту, — это уведомить наблюдателя об изменении данных. Как субъект узнает, что данные изменились?Механизм тот же, что и vue2, с использованиемObject.definePropertyЗаймитесь угоном имущества.

Разберем обозреватель src/observer/subject.js, который разбит на 7 частей.

первая часть

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

Конструктор. В принципе ничего сложного. настраивать_hijackingсобственностьtrue, используется для обозначения того, что объект был захвачен.Object.keysУгоните каждую собственность, пройдя через нее. Если это массив, вызовитеhijackArray.

Вторая часть

Два статических метода.

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 не вызывается напрямую извне, а инкапсулируется вSubject.ofв статических методах.

Если цель не видна, возвращайтесь прямо к цели.

еслиtarget[SYMBOL_OBSERVABLE]нетundefined, указывая, что цель была инициализирована.

В противном случае вызовите конструктор для инициализации субъекта.

Subject.isОн используется для определения того, была ли цель захвачена.

третья часть

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

Этот метод очень знаком, да, это вышеObserver.prototype.subscribeпозвонил в. Роль заключается в том, что наблюдатель используется для подписки на тему. А этот способ "как подписывается тема".

Наблюдатели поддерживают хеш-таблицу этой темы_obsMap. Ключ хеш-таблицы — это ключ, на который нужно подписаться. Например, наблюдатель подписывается наnameсвойство изменяется, пока другой наблюдатель подписывается наageизменения свойств. И изменения свойств также могут быть подписаны несколькими наблюдателями одновременно, поэтому значение, хранящееся в хеш-таблице, представляет собой массив, а каждый элемент данных является наблюдателем.

четвертая часть

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

Наблюдатели, подписанные на это свойство, уведомляются об изменении свойства. Перебрать каждого наблюдателя и вызвать наблюдателяupdateметод. Мы также упоминали выше, что в этом методе выполняется грязная проверка.

пятая часть

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

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

Эта часть используется для решения проблемы вложенных объектов. что-то вроде этого:{ user: { name: 'JJC' } }.

шестая часть

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

В этом разделе показано, как использоватьObject.definePropertyСовершить захват имущества. Когда свойство установлено, вызывается set(value), устанавливается новое значение, а затем вызывается метод уведомления субъекта. Здесь проверка не выполняется, она вызывается всякий раз, когда устанавливается свойство, даже если новое значение свойства совпадает со старым значением. notify уведомит всех наблюдателей.

Часть 7

Методы захвата массива.

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Определяет функцию массива, которую необходимо перехватить. В качестве ключа используется верхний регистр, а нижний регистр — это метод, который необходимо взломать.

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

Захват массивов отличается от захвата объектов и не может быть использованObject.defineProperty.

Нам нужно захватить 6 методов массива. Это добавление головы, удаление головы, добавление хвоста, удаление хвоста, замена/удаление определенных элементов и инверсия массива.

Перехват массива достигается путем переопределения метода массива. Но здесь есть одна оговорка, каждый элемент данных наблюдается, но когда в массив добавляются новые элементы, эти элементы еще не наблюдаются. Следовательно, код также должен оценивать текущий метод, если онpush,unshift,splice, то новый элемент необходимо поместить в очередь наблюдателя.

Два других файла — модульное тестирование и анализ производительности, которые здесь анализироваться не будут.