require(dependencies, callback)
function. The first argument is the list of modules to load, and the second is the function that will be called when the load is completed, with the modules in the arguments. Using Promise to write it is a snap: function require(deps, factory) { return Promise.all(deps.map(function(dependency) { if(!modules[dependency]) { modules[dependency] = loadScript(dependency); } return modules[dependency]; }).then(function(modules) { return factory.apply(null, modules); }); }
function loadScript(name) { return new Promise(function(resolve, reject) { var el = document.createElement("script"); el.onload = resolve; el.onerror = reject; el.async = true; el.src = './' + name + '.js'; document.getElementsByTagName('body')[0].appendChild(el); }); }
resolve
and reject
. These are functions for reporting results. If successful, call resolve
, and on error, reject
.Promise.all
, which will collect several promises into one, and its result in case of success will be an array of results of loading all the necessary modules. With the help of these two simple functions, you can already get a minimally working prototype.define()
function is designed for this in the AMD standard. For example, the registration of module A looks like this: define('A', function() { return 'module A'; });
define
function. At the last step, we simply waited for the successful loading of the script and did not check its contents. Now we will wait for a call to define
. And at the time of the call, we will find our module and mark it as loaded.promise
field in which the Promise is written, the result of which we specify. Deferred are easily made from Promise prescription from Stackoverflow .require
, we will create a deferred object for each module and save it to the cache (pendingModules).define
will be able to get it from there and call resolve to mark it as loaded and save it. function define(name, factory) { var module = factory(); if(pendingModules[name]) { pendingModules[name].resolve(module); delete pendingModules[name]; } else { modules[name] = module; } }
function loadScript(name) { var deferred = defer(), el = document.createElement("script"); pendingModules[name] = deferred; el.onerror = deferred.reject; el.async = true; el.src = './' + name + '.js'; document.getElementsByTagName('body')[0].appendChild(el); return deferred.promise; }
define
function, another argument is provided - dependencies
, which can be between name and factory.factory()
there will now be require
: define(name, deps, factory) { ... - var module = factory(); + var module = require(deps, factory); ... }
module
variable will not be a module, but the promise of a module. When we pass it to resolve, the promise of the source module will not be fulfilled, but will now wait for the dependencies to load. This is quite a convenient feature of Promise, when our asynchronous process stretches into several stages, we can resolve to transfer the Promise from the next stage, and the internal logic will recognize this and switch to waiting for the result from the new Promise.require()
to load module dependencies, but this function is a fixed set of arguments, written in the standard, it must be followed. Let's create our own internal function _require(deps, factory, path)
to which we will be able to transmit information about the module load history, and in the public API we will make its call: function require(deps, factory) { return _require(deps, factory, []); }
_require()
will now have the same boot logic, plus history tracking. function loadScript(name, path) { var deferred = defer(); + deferred.path = path.concat(name); ... } function _require(deps, factory, path) { return Promise.all(deps.map(function (dependency) { + if(path.indexOf(dependency) > -1) { + return Promise.reject(new Error('Circular dependency: '+path.concat(dependency).join(' -> '))); + } ... }
define
and transferred to _require
if you need to load more modules. I note that we add a new module to the history via .concat()
, instead of .push()
, because we need an independent copy of the history in order not to spoil the history to other modules that were loaded to us. And instead of the usual throw new Error()
we return Promise.reject (). This means that the promise did not come true, and an error handler will be called, just as it does when an error occurs during the loading of the script, only the message indicates another reason - a cycle in dependencies..then()
we pass two functions. The first one is already transmitted and called, if everything is good, the second one will be called up if something goes wrong.errback
, as in the original require.js function _require(deps, factory, errback, path) { ... })).then(function (modules) { return factory.apply(null, modules); + }, function(reason) { + if(typeof errback === 'function') { + errback(reason); + } else { + console.error(reason); + } + return Promise.reject(reason); }); }
Promise.reject
.document.currentScript
property is not supported by all browsers, so we will have to define the current script in a different way. We will make the loading of the modules consistent, which means we will expect only one module at a time. Using Promise, you can easily get the implementation of a FIFO queue: var lastTask = Promise.resolve(), currentContext; function invokeLater(context, fn) { lastTask = lastTask.then(function() { currentContext = context; return fn(); }).then(function() { currentContext = null; }); }
pendingModules
list, but one pendingModule, and the rest will be waiting. function loadScript(name, path) { var deferred = defer(); deferred.name = name; deferred.path = path.concat(name); invokeLater(deferred, function() { return new Promise(function(resolve, reject) { // }); }); return deferred.promise; }
define(function() { return 'module-content'; });
define
, it does not mean that it needs to be initialized immediately. Maybe no one has asked him yet and he may not need it. Therefore, it can be saved along with information about its dependencies and called only when it really comes in handy. It will also be useful if the modules can be declared in any order, and their dependencies will be dealt with at the very end, during application initialization.predefines
, and save modules in it, if nobody asked them. function define(name, deps, factory) { ... } else { - modules[name] = _require(deps, factory, null, []); + predefines[name] = [deps, factory, null]; } }
require
we will first check the predefines
for the modules we are interested in. function _require(deps, factory, errback, path) { ... + var newPath = path.concat(dependency); + if(predefines[dependency]) { + modules[dependency] = _require.apply(null, predefines[dependency].concat([newPath])) + } else if (!modules[dependency]) { modules[dependency] = loadScript(dependency, newPath); } ... }
define('A', ['B'], function() {}); define('B', function() {}); require(['A'], function() {});
require
. Also now it does not matter the order of their announcement.Source: https://habr.com/ru/post/252783/
All Articles