【requireJS源碼學習02】data-main加載的實現


前言

經過昨天的學習,我們大概了解到了requireJS的主要結構,這里先大概的回顧一下

首先從總體結構來說,require這里分為三塊:

① newContext之前變量聲明或者一些工具函數

② newContext大家伙

③ 解析script標簽抽出data-main,並提供幾個對外接口

從流程上講,大概發生了這些事情:

① script標簽引入requireJS后,便會初始化一些變量以及函數,並不干實際的事情

② 在主干結構第三步之前會使用req({})調用(並且只會調用一次)newContext方法由此會維護一個newContext的閉包環境,之后的很多變量全在其中

③ 取出script標簽中的data-main參數,做第一次簡單參數處理(這里有兩個全局變量contexts以及cfg比較重要)

在此調用req(cfg),這里便會進行實際的操作了

所以,關鍵還是第二次調用req(cfg),這里由涉及到很多細節的地方了,這里我們便可以關注requirejs方法的細節了:

① 這次調用首先會取出當前上下文環境,然后調用config方法設置屬性,最后調用context.require進行實際操作

context = getOwn(contexts, contextName);
if (!context) {
  context = contexts[contextName] = req.s.newContext(contextName);
}
if (config) {
  context.configure(config);
}
return context.require(deps, callback, errback);

② 屬性配置結束后便執行localRequire方法,這里會慢慢加載模塊了,然后使用

requireMod = getModule(makeModuleMap(null, relMap));

真正加載模塊,具體里面真的干了什么,這就是我們今天要學習的內容了,於是我們繼續吧

requireJS如何加載data-main文件

這里我們先干一件事情,搞清楚requireJS是如何將script標簽中data-main對應的文件加載出來的,這一步搞懂了后,才能知道其它文件如何加載

首先,根據前面的學習,我們知道了

req(cfg)
=>
context.require(config)
=>
context.makeRequire()
=>
localRequire()

而localRequire干了很多事情,我們這里暫時是不關注的,於是主要注意力一到了nextTick上面,於是最終進入了這里的核心getModule

getModule

function getModule(depMap) {
  var id = depMap.id, mod = getOwn(registry, id);
  if (!mod) {
    mod = registry[id] = new context.Module(depMap);
  }
  return mod;
}

我們前面說了registry里面存儲着已經加載好了的模塊,而一個模塊加載后便不會再加載了,這里的唯一標識是makeModuleMap處理的,我們暫時不予關注

這里沒有便會創建,而這里的創建module過程便是我們需要了解的主干流程

mod = registry[id] = new context.Module(depMap);

context.Module

我們這里實例化了一個mod作為返回,並且將之存入了registry對象中,初始化過程中並未做什么特別的事情,但是該模塊具備了大量實用方法

然后,將該mod返回給了我們的臨時變量requireMod,並且調用了其初始化方法

requireMod.init(deps, callback, errback, {
  enabled: true
});

requireMod.init

這里開始了我們實際的模塊初始化邏輯了

 

① 如果該模塊已經初始化便不再執行下面邏輯

② 然后他會將依賴映射取出,這里為了保存原始數組的完整性還做了其它操作

//Do a copy of the dependency array, so that
//source inputs are not modified. For example
//"shim" deps are passed in here directly, and
//doing a direct modification of the depMaps array
//would affect that config.
this.depMaps = depMaps && depMaps.slice(0);

最后執行了module的enable方法,enable最后會調用this.check

requireMod.check

調用enable方法時,首先會存一個類似於registry的對象enabledRegistry,這個應該與依賴項什么的相關,比如backbone依賴於underscore,這里就應該先加載underscore

但是這里具體干了什么,我們先放一下,先跑主干流程,我們這里直接進入check

PS:這個enable很關鍵,我們后面一點詳細來看看

進入check流程后,馬上又跳至了this.fetch

這里首先記錄了當前上下文環境的開始時間(此context是所有mod共享的)

context.startTime = (new Date()).getTime();

然后回調用this.load方法,這個方法有可能要加載標簽了,這里也是有點東西的,我們稍候再說

然后控制器又回到了context的load手里

//Delegates to req.load. Broken out as a separate function to
//allow overriding in the optimizer.
load: function (id, url) {
  req.load(context, id, url);
},

req.load = function (context, moduleName, url) {
  var config = (context && context.config) || {},
          node;
  if (isBrowser) {
    //In the browser so use a script tag
    node = req.createNode(config, moduleName, url);

    node.setAttribute('data-requirecontext', context.contextName);
    node.setAttribute('data-requiremodule', moduleName);

    if (node.attachEvent &&
                  !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
                  !isOpera) {
      useInteractive = true;
      node.attachEvent('onreadystatechange', context.onScriptLoad);
    } else {
      node.addEventListener('load', context.onScriptLoad, false);
      node.addEventListener('error', context.onScriptError, false);
    }
    node.src = url;
    currentlyAddingScript = node;
    if (baseElement) {
      head.insertBefore(node, baseElement);
    } else {
      head.appendChild(node);
    }
    currentlyAddingScript = null;

    return node;
  } else if (isWebWorker) {
    try {
      importScripts(url);
      context.completeLoad(moduleName);
    } catch (e) {
      context.onError(makeError('importscripts',
                              'importScripts failed for ' +
                                  moduleName + ' at ' + url,
                              e,
                              [moduleName]));
    }
  }
};

這里做了很多兼容性處理,我們直接抽取主干邏輯即可,這里是重頭戲了:

① 使用createNode創建script標簽

req.createNode = function (config, moduleName, url) {
  var node = config.xhtml ?
              document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
              document.createElement('script');
  node.type = config.scriptType || 'text/javascript';
  node.charset = 'utf-8';
  node.async = true;
  return node;
};

這里創建標簽后給其注入了一些自定義屬性,並且綁定了一個事件(這里各個瀏覽器可能不同,我們關注標准的)

node.addEventListener('load', context.onScriptLoad, false);
onScriptLoad: function (evt) {
  if (evt.type === 'load' || (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {
    interactiveScript = null;
    var data = getScriptData(evt);
    context.completeLoad(data.id);
  }
},

然后將該節點插入head中

head.appendChild(node);

於是頭部多出了這么一塊東西

此段js加載結束后馬上執行調用onScriptLoad方法,但是,我們寫在main.js中的邏輯會被調用,該函數暫時看來是執行一些資源清理工作

並且會將該節點存儲起來

PS:注意啦,這里可能會有一定時序性問題,整個這塊邏輯我們還需要整理

看到這里,我們基本明白了,我們第一個文件,main.js是如何加載出來的了

一個簡單的例子

為了幫助理解,我們這里以一個簡單的例子做說明,再看看源碼的實現

HTML

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
</head>
<body>
  <script src="require.js" type="text/javascript" data-main="main.js"></script>
</body>
</html>

main.js

require.config({
  baseUrl: '',
  paths: {
    'nameDep': 'js/nameDep',
    'say': 'js/say',
    'name': 'js/name'
  },
  shim: {
    'name': {
      deps: ['nameDep']
    }
  }
});
require(['name', 'say'], function (name, say) {
  say(name);
});

name/say

//name
define([''], function () { return '測試'; });

//say define([],
function () { return function (name) { console.log(name); }; });

由上面的邏輯來看,當main.js加載結束后於是就該進行下一個邏輯的處理,我們這里看看他是如何處理的呢,這個時候第一個入口便是require.config了

require.config

這個時候會傳一些有意義的參數進來了

整個config依舊在做參數設置,這個這個操作實際的意義是為newContext中的config賦值,並在localRequire中使用

接下來順理成章的再次進入了模塊實例化的步驟

 requireMod = getModule(makeModuleMap(null, relMap));

最后進入上面提到的初始化環節,但是內部會有不一樣的細節處理(我們暫時不予關注,留着下次學習)這里每一次加載會清除前面的registry

config的設置並不會觸發script標簽的加載,所以其實際加載還是下面的require,這塊的邏輯又有點小復雜了......

PS:這里設置斷點調試時序上好像有點問題,所以這塊暫時就不處理了,應該與其維護的隊列有關,留待下次解決吧

結語

通過這兩天的學習,我們大概了解了requireJS的一些東西,但是還是那句話,小釵感覺requireJS讀起來還是有點小難,不是那么好吸收

關於他的學習,還需要慢慢來哦,感覺里面水有點深......


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM