Eloquent Javascript第二版。第10章:帮助实施AMD

时间:2017-10-31 03:20:35

标签: javascript

我是一个尝试学习javascript的新手。在雄辩的javascript的第10章中,讨论了AMD的实现,并展示了define()的机制。首先定义了一个函数getModule,它返回一个描述模块状态的对象;在getModule中使用了未明确显示的backgroundReadFile函数。然后写出define函数。

我可以请求帮助理解这段代码。例如,如果我有一个需要两个模块的函数,即define([mod1,mod2],function(mod1,mod2)...),那么在getModule和define函数中发生了一步一步的过程是什么?谢谢。

以下是可在其在线图书https://eloquentjavascript.net/index.html中看到的代码:

//the getModule function
var defineCache = Object.create(null);
var currentMod = null;

function getModule(name) {
  if (name in defineCache)
    return defineCache[name];

  var module = {exports: null,
                loaded: false,
                onLoad: []};
  defineCache[name] = module;
  backgroundReadFile(name, function(code) {
    currentMod = module;
    new Function("", code)();
  });
  return module;
}

// the define function
function define(depNames, moduleFunction) {
  var myMod = currentMod;
  var deps = depNames.map(getModule);

  deps.forEach(function(mod) {
    if (!mod.loaded)
      mod.onLoad.push(whenDepsLoaded);
  });

  function whenDepsLoaded() {
    if (!deps.every(function(m) { return m.loaded; }))
      return;

    var args = deps.map(function(m) { return m.exports; });
    var exports = moduleFunction.apply(null, args);
    if (myMod) {
      myMod.exports = exports;
      myMod.loaded = true;
      myMod.onLoad.forEach(function(f) { f(); });
    }
  }
  whenDepsLoaded();
}

1 个答案:

答案 0 :(得分:1)

我在代码中添加了一堆注释。 我希望这有用!

// module cache used in getModule
var defineCache = Object.create(null);
// Object.create(null) gives an empty object like {} but even *emptier*
// in that it doesn't inherit properties from a prototype
// which makes it better for using as a key-value map,
// although there's also a built-in Map type that could be used.

// This variable is actually shared between the two functions.
// (see explanation further on)
var currentMod = null;

// the getModule function
function getModule(name) {
  // If a module has already loaded *or has started* being loaded,
  // it should be in the cache and we can just return it.
  if (name in defineCache)
    return defineCache[name];

  var module = {exports: null,
                loaded: false,
                onLoad: []};

  // Put the module in the cache.
  defineCache[name] = module;

  // Asynchronously load file contents with a function such as
  // fs.readFile() in Node.js, or fetch() in the browser
  // (which both have different APIs; readFile would need
  // an err parameter in the callback, and fetch uses Promises)
  readFileAsync(name, function(code) {

    // We're going to execute the code, and the code should include a define() call,
    // but `define` will need access to the module object
    // corresponding to the code that's calling define().
    // So we set `currentMod` to the module object,
    // and share this state between the two functions.
    currentMod = module;

    // Then evaluate the the loaded code.
    new Function("", code)();
    // Creating a Function and executing it is like using eval()
    // but with less stuff considered 'in scope'.
    // The new function only has access to global scope,
    // which includes the `define` function,
    // but not other variables it doesn't need,
    // and doesn't need to be able to mess with!
    // eval() is more like dynamically including code in-line; it's messy.

    // I'm not sure why "" is passed there. According to the docs for Function,
    // any arguments before the last would be names of parameters of the function, 
    // which it's not using any of.
    // I suspect it would also work with just new Function(code)().

    // I think maybe there should be a `currentMod = undefined;` here
    // in case there are multiple top-level define() calls so it doesn't
    // overwrite the exports of a (sub-level) module from the first top-level define()
    // when doing the second top-level define()
  });

  // Return the module object even before the file is loaded.
  return module;
}

// the define function
function define(depNames, moduleFunction) {
  // If this was called from a module loaded from a file,
  // currentMod should be set by getModule just previously.
  // Otherwise, if this was called outside of a module (i.e. a main script),
  // currentMod should be undefined.
  var myMod = currentMod;

  // Call getModule for each dependency.
  // The naming seems a little inconsistent - shouldn't it be `var modules`?
  // I guess either works, but anyway deps is an array of the module objects.
  // This will also kick off reading the files for each of the modules,
  // but execution will continue on below before they're loaded. 
  var deps = depNames.map(getModule);

  // We want to know when all the dependencies are loaded,
  // so we need to know when any dependency loads.
  // Register callbacks to each dependency's onLoad event
  // (by adding it to an array of callbacks).
  deps.forEach(function(mod) {
    if (!mod.loaded)
      mod.onLoad.push(checkForWhenDepsLoaded);
  });
  // Check immediately in case there are no deps or they've already loaded.
  checkForWhenDepsLoaded();
  // (I moved this up here so both places that handle
  // calling checkForWhenDepsLoaded are next to each other.)
  // (They could also both be at the end.)


  // This was called whenDepsLoaded but that's not a great name
  // because it's not called *when* the deps are loaded,
  // it's called to check if they're loaded and then do some stuff if so.
  function checkForWhenDepsLoaded() {
    // Continue only if all the dependencies are marked as loaded.
    if (!deps.every(function(m) { return m.loaded; }))
      return;
    // Now from here, all deps should be loaded.
    // We should only get here once!
    // That's important when doing callbacks;
    // you need to know whether there's a possibility of calling back multiple times.
    // In some cases you do want to call back multiple times,
    // like when iterating over a collection (forEach and map etc.)
    // but then you still need to be careful not to call back multiple times *per item*.
    // You can try to look at code paths to make sure it shouldn't happen,
    // but you can also test the assumption!
    // You could make a flag variable like "calledBack"
    // with `var calledBack = false;` (at the top of `define` if doing it here),
    // set it to true when calling the callback, and before that
    // log a message in case it does actually get there a second time,
    // and if so return so the callback isn't called again.

    // Time to call back!
    // the caller of define() won't really care about the module *objects*,
    // just the exports of said modules,
    // so collect only the `exports` of each module in an array.
    var args = deps.map(function(m) { return m.exports; });
    // Then call back with them as arguments.
    // apply() lets you call a function with an array of values as arguments
    // essentially calling moduleFunction(args[0], args[1], ...)
    var exports = moduleFunction.apply(null, args);

    if (myMod) {
      myMod.exports = exports;
      // Mark as loaded and execute all onLoad handlers.
      myMod.loaded = true;
      myMod.onLoad.forEach(function(f) { f(); });
    }
  }
}