Как стать автором
Обновить

Сборка мусора после выгрузки модуля из родительского приложения

Время на прочтение 7 мин
Количество просмотров 891
Во-первых, все написаное ниже относится к клиентской части web-приложений, созданных на JavaScript.
Во-вторых, это относится к web-приложениям, использующим модульную архитектуру для расширения своей функциональности (подгрузка/выгрузка модулей).

Родительское приложение — приложение в котором запускаются модули, запускаемый модуль знает о родительском приложении.
Модуль — совокупность скриптов и ресурсов реализующих определенную функциональность, призваную расширить возможность web-приложения. Код модуля может вызывать внутренние функции web-приложения и как следствие менять внутреннее состояние этого приложения.

Задача: При выгрузке модуля из web-приложения делать откат тех изменений, которые произвел модуль, вызывая внутри своего кода функции web-приложения.

Пример: Картографическое приложение загружает модуль, показывающий пользовательский слой методами этого приложения, после выгрузки модуля необходимо корректно удалить пользовательский слой. Для этого картографическое приложение имеет внутренний метод корректного удаления пользовательского слоя и после выгрузки модуля он должен быть вызван.

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

В процессе придумывания как это сделать было сгенерировано 4 решения. Два из которых использовали свойство caller из объекта Function, но после того как было изучено вот это www.opera.com/docs/specs/opera9/js/ecma эти решения можно было смело отправить в утиль, к сожалению… Хотя тут еще спорный вопрос конечно, то что нет этого свойства в Opera решает определенные вопросы безопасности JavaScript кода, этот вопрос я поднимал в посте Модификация кода ядра платформы подгружаемыми апплетами (модулями) и был предложен хак моего подхода именно через свойство caller habrahabr.ru/blogs/javascript/28660/#comment_756953. В Опере конечно такой хак не пройдет, но и отсутствие этого свойства не дает осуществить некоторые красивые решения в частности то, о чем пойдет речь в этом топике. И конечно ставя безопасность исполнения JavaScript на первое место, свойство caller могут со временем убрать совсем (хотя некоторые неосознающие личности не понимают что, как и почему происходит в мире технологий, а просто минусуют не пытаясь дойти до сути). Поэтому пришлось искать еще одно решение.

Далее поверхностно представлены сущности задачи, акцент на суть проблемы.

Примерный класс модуля:

function Module(fCode) {
   if(fCode) this.CreateInstance(fCode);
   return this;
}

Module.prototype = new Object;

Module.prototype.oInstance = null;

Module.prototype.CreateInstance = function(fCode) { this.oInstance = fCode; }
Module.prototype.GetInstance = function() { return this.oInstance; }
Module.prototype.Run = function() {

   if(this.oInstance) this.oInstance.apply(this.oInstance, arguments);
}

Module.prototype.Unload = function() { /* Здесь будет код очистки */ }

Конструктор Module принимает исполняемый код, в данном случае в виде готовой функции, но можно сделать чтобы код приходил в виде текста.
Метод Run запускает код и передает в него необходимые параметры.
Метод Unload занимается корректным удалением объектов созданных в модуле.

На базе класса Module создадим наше родительское web-приложение:

function BaseModule() {}
BaseModule.prototype = new Module(function(oParentInstance) {

   function MyClass() { this.aObjects = []; }
   MyClass.prototype.aObjects = null;

   MyClass.prototype.CreateObject = function(sParam) {
      var oObject = new Object;
      oObject.sParam = sParam;
      this.aObjects.push(oObject);
   }

   MyClass.prototype.RemoveObject = function(oObject) {
      alert(«Release object: „ + oObject.sParam);

      for(var i=0; i<this.aObjects.length; i++) {
         if(this.aObjects[i] == oObject) {
            this.aObjects.splice(i, 1);
            break;
         }
      }
      delete oObject;
   }

   var oMyClass = new MyClass;

   this.oMyClass = oMyClass;

});

Приложение содержит внутри себя класс MyClass предоставляющий два метода для корректного создания и удаления объектов (в примере показны просто абстрактные объекты). После запуска приложение создает экземпляр класса и делает его публичным: var oMyClass = new MyClass; this.oMyClass = oMyClass;
Теперь другие модули смогут получить свойство oMyClass и вызывать методы CreateObject и RemoveObject.

Опишем подгружаемый модуль:

function ExternModule1() {}
ExternModule1.prototype = new Module(function(oParentInstance) {
   var oMyClass = oParentInstance.oMyClass;
   var oObject = oMyClass.CreateObject(“Hello world!»);
});

После загрузки и начала выполнения модуль обращается к пространству имен родительского приложения, получает свойство oMyClass и вызывает метод CreateObject.

И в целом:

// Запускаем наше приложение.
var oBaseModule = new BaseModule;
oBaseModule.Run({});

// Запускаем модуль передавая объект Function с кодом приложения (логически связываем родительское приложение и запускаемый модуль).
var oExternModule1 = new ExternModule1();
oExternModule1.Run(oBaseModule.GetInstance());

// Выгружаем модуль.
oExternModule1.Unload();


В контексте решаемой задачи необходимо чтобы после выгрузки модуля автоматически удалились объекты, созданные в модуле через методы родительского приложения. В примере выше создается объект через метод CreateObject, и после выгрузки, если разработчик модуля сам не произвел удаление посредством метода RemoveObject за него это должен сделать сам модуль.

Было решено для тех объектов, которым требуется автоматическое удаление с вызовом специального метода удаления, ввести функцию регистрации объекта причем регистрацию для того контекста в котором этот объект был создан. В примере о объекте oObject должен знать Модуль в котором этот объект был создан. После выгрузки модуль сам проходит по списку тех объектов что были в нем созданы и вызывает метод Release (в методе уже указано как корректно удалить самого себя).

Расширим нативный объект Object указав методы регистрации и удаления объектов:

function GBind(oContext, fFunctor) {
   return function () {
        return fFunctor.apply(oContext, arguments);
   }
}

Object.prototype.aHeap = null;
Object.prototype.Release = function() {}

Object.prototype.RegisterObject = function(oContext, oObject, fRelease) {
   oObject.Release = GBind(oContext, fRelease);
   if(!this.aHeap) this.aHeap = [];
   this.aHeap.push(oObject);
}

Object.prototype.ReleaseObjects = function() {
   if(!this.aHeap) return;
   for(var i=0; i<this.aHeap.length; i++)
      if(this.aHeap[i].Release)
         this.aHeap[i].Release(this.aHeap[i]);
}


Доработаем метод CreateObject с учетом регистрации созданного объекта:

   MyClass.prototype.CreateObject = function(sParam) {
      var oObject = new Object;
      oObject.sParam = sParam;
      this.aObjects.push(oObject);

      this.RegisterObject(this, oObject, function(oObject) { this.RemoveObject(oObject); });

   }

где в методе RegisterObject указывается где удалять (контекст в котором будет вызван метод удаления), что удалять (удаляемый объект) и как удалять (метод для удаления).

Есть еще одна важная делать, RegisterObject все равно вызывается в контексте родительского web-приложения, а чтобы модуль смог зарегистрировать объект в своем контексте необходимо вызывать RegisterObject в контексте модуля. До этого у меня было решение на базе caller. Чтобы определить контекст вызова, была функция, которая рекурсивно пробегала по свойству caller до точки вызова (модуль откуда произведен вызов), получала контекст модуля и уже через него вызывался RegisterObject. Без свойства caller эту задачу решить можно по другому, но не так красиво. Так как модуль знает какое приложение для него родительское, он может подменить контекст вызова метода RegisterObject на свой контекст (задача решается через замыкание). Подмена делается предварительно до запуска кода модуля. Сразу добавим в метод Unload удаление зарегистрированных объектов. И финальный класс модуля:

function Module(fCode) {
   if(fCode) this.CreateInstance(fCode);
   return this;
}

Module.prototype = new Object;

Module.prototype.oInstance = null;

Module.prototype.CreateInstance = function(fCode) { this.oInstance = fCode; }
Module.prototype.GetInstance = function() { return this.oInstance; }
Module.prototype.Run = function() {

   var oParentInstance = arguments[0].oMyClass;
   var oCloneInstance = {};
   for(vProp in oParentInstance) oCloneInstance[vProp] = oParentInstance[vProp];
   oCloneInstance.RegisterObject = GBind(this.oInstance, Object.prototype.RegisterObject);
   arguments[0].oMyClass = oCloneInstance;

   if(this.oInstance) this.oInstance.apply(this.oInstance, arguments);
}

Module.prototype.Unload = function() { this.oInstance.ReleaseObjects(); }


Пример для изучения с двумя модулями: www.okarta.ru/hyper/modulerelease.html

На базе статей: Динамическая подгрузка модулей на JavaScript, Планировщки задач на JavaScript, Модификация кода ядра платформы подгружаемыми модулями и настоящей статьи сделан следующий пример практического применения: www.okarta.ru/hyper/base.html

P.S. Опять же дана идея реализации, есть различные ньюансы, но если давать реализацию с их учетом, то статья (так же как и код) разрастется очень сильно, а понять суть будет очень сложно.
Теги:
Хабы:
+7
Комментарии 15
Комментарии Комментарии 15

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн