【requireJS源碼學習03】細究requireJS的加載流程


前言

這個星期折騰了一周,中間沒有什么時間學習,周末又干了些其它事情,這個時候正好有時間,我們一起來繼續學習requireJS吧

還是那句話,小釵覺得requireJS本身還是有點難度的,估計完全吸收這個月就過去了,等requireJS學習結束后,我們的學習流程可能就朝兩個方向走

① 單頁應用框架/UI庫整理

② UML文檔相關/重構思想相關(軟性素質)

然后以上的估計估計會持續3、4個月時間,希望學習下來自己能有不一樣的提高,成為一個合格的前端,於是我們繼續今天的內容吧

requireJS中的隊列

經過之前的學習,我們隊requireJS的大概結構以及工作有了一定認識,但是,我們對於其中一些細節點事實上還是不太清晰的,比如里面的隊列相關

requireJS中有幾種隊列,每種隊列是干神馬的,這些是我們需要挖掘的,而且也是真正理解requireJS實現原理的難點

首先,requireJS有兩個隊列:

① globalDefQueue / 全局

② defQueue / newContext 閉包

這個隊列事實上是一個數組,他們具體干了什么我們還不得而知,但是我下意識覺得他比較關鍵......

我們這里來簡單的理一理這兩個隊列

globalDefQueue 

這個是全局性的隊列,與之相關的第一個函數為takeGlobalQueue

takeGlobalQueue

/**
* Internal method to transfer globalQueue items to this context's
* defQueue.
*/
function takeGlobalQueue() {
  //Push all the globalDefQueue items into the context's defQueue
  if (globalDefQueue.length) {
    //Array splice in the values since the context code has a
    //local var ref to defQueue, so cannot just reassign the one
    //on context.
    apsp.apply(defQueue, [defQueue.length - 1, 0].concat(globalDefQueue));
    globalDefQueue = [];
  }
}

這個函數中涉及到了defQueue中的的操作,每一次有效操作后都會將全局隊列清空,其中有一個apsp方法這個是數組的splice方法

該函數主要用於將globalDefQueue中的數據導入defQueue,而globalDefQueue只會有可能在define函數出被壓入數據,具體原因還得往后看

所以這里的takeGlobalQueue其實就如注釋所說,將全局隊列中的項目轉入context defQueue中

define

第二個涉及globalDefQueue函數為define

/**
* The function that handles definitions of modules. Differs from
* require() in that a string for the module should be the first argument,
* and the function to execute after dependencies are loaded should
* return a value to define the module corresponding to the first argument's
* name.
*/
define = function (name, deps, callback) {
  var node, context;

  //Allow for anonymous modules
  if (typeof name !== 'string') {
    //Adjust args appropriately
    callback = deps;
    deps = name;
    name = null;
  }

  //This module may not have dependencies
  if (!isArray(deps)) {
    callback = deps;
    deps = null;
  }

  //If no name, and callback is a function, then figure out if it a
  //CommonJS thing with dependencies.
  if (!deps && isFunction(callback)) {
    deps = [];
    //Remove comments from the callback string,
    //look for require calls, and pull them into the dependencies,
    //but only if there are function args.
    if (callback.length) {
      callback
                  .toString()
                  .replace(commentRegExp, '')
                  .replace(cjsRequireRegExp, function (match, dep) {
                    deps.push(dep);
                  });

      //May be a CommonJS thing even without require calls, but still
      //could use exports, and module. Avoid doing exports and module
      //work though if it just needs require.
      //REQUIRES the function to expect the CommonJS variables in the
      //order listed below.
      deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps);
    }
  }

  //If in IE 6-8 and hit an anonymous define() call, do the interactive
  //work.
  if (useInteractive) {
    node = currentlyAddingScript || getInteractiveScript();
    if (node) {
      if (!name) {
        name = node.getAttribute('data-requiremodule');
      }
      context = contexts[node.getAttribute('data-requirecontext')];
    }
  }

  //Always save off evaluating the def call until the script onload handler.
  //This allows multiple modules to be in a file without prematurely
  //tracing dependencies, and allows for anonymous module support,
  //where the module name is not known until the script onload event
  //occurs. If no context, use the global queue, and get it processed
  //in the onscript load callback.
  (context ? context.defQueue : globalDefQueue).push([name, deps, callback]);
};
View Code

他會根據context是否初始化決定當前鍵值標識存於哪個隊列,據代碼看來,如果是標准瀏覽器應該都會先走globalDefQueue隊列

然后就沒有然后了,我們接下來再看看吧

defQueue 

首先defQueue處於newContext閉包環境中,按照之前的知識來看,newContext每次也只會執行一次,所以這個defQueue以后會被各個函數共享

操作defQueue的第一個函數為

intakeDefines

function intakeDefines() {
  var args;
  //Any defined modules in the global queue, intake them now.
  takeGlobalQueue();
  //Make sure any remaining defQueue items get properly processed.
  while (defQueue.length) {
    args = defQueue.shift();
    if (args[0] === null) {
      return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' + args[args.length - 1]));
    } else {
      //args are id, deps, factory. Should be normalized by the
      //define() function.
      callGetModule(args);
    }
  }
}

引入定義,第一件事情就是將globalDefQueue中的項目移入defQueue中,而后將其中的項目一個個取出並執行callGetModule方法,但是我這里好像都沒有效果,這塊先忽略之

第二個函數為completeLoad

completeLoad

/**
* Internal method used by environment adapters to complete a load event.
* A load event could be a script load or just a load pass from a synchronous
* load call.
* @param {String} moduleName the name of the module to potentially complete.
*/
completeLoad: function (moduleName) {
  var found, args, mod,
              shim = getOwn(config.shim, moduleName) || {},
              shExports = shim.exports;

  takeGlobalQueue();

  while (defQueue.length) {
    args = defQueue.shift();
    if (args[0] === null) {
      args[0] = moduleName;
      //If already found an anonymous module and bound it
      //to this name, then this is some other anon module
      //waiting for its completeLoad to fire.
      if (found) {
        break;
      }
      found = true;
    } else if (args[0] === moduleName) {
      //Found matching define call for this script!
      found = true;
    }

    callGetModule(args);
  }

  //Do this after the cycle of callGetModule in case the result
  //of those calls/init calls changes the registry.
  mod = getOwn(registry, moduleName);

  if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) {
    if (config.enforceDefine && (!shExports || !getGlobal(shExports))) {
      if (hasPathFallback(moduleName)) {
        return;
      } else {
        return onError(makeError('nodefine',
                                        'No define call for ' + moduleName,
                                        null,
                                        [moduleName]));
      }
    } else {
      //A script that does not call define(), so just simulate
      //the call for it.
      callGetModule([moduleName, (shim.deps || []), shim.exportsFn]);
    }
  }

  checkLoaded();
},
View Code

這個會將globalDefQueue中的隊列項搞到defQueue中,然后處理一下就調用callgetModule方法,其中參數是這樣的

 

callGetModule

function callGetModule(args) {
  //Skip modules already defined.
  if (!hasProp(defined, args[0])) {
    getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
  }
}

這個時候就會由全局registry中獲取當前的模塊了,然后執行他的init方法,這里會加載script標簽,將其依賴項載入,這里還會涉及到registry的操作,我們放到后面來學習

而completeLoad是在script標簽加載結束后調用的方法

/**
* callback for script loads, used to check status of loading.
*
* @param {Event} evt the event from the browser for the script
* that was loaded.
*/
onScriptLoad: function (evt) {
  //Using currentTarget instead of target for Firefox 2.0's sake. Not
  //all old browsers will be supported, but this one was easy enough
  //to support and still makes sense.
  if (evt.type === 'load' ||
            (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {
    //Reset interactive script so a script node is not held onto for
    //to long.
    interactiveScript = null;

    //Pull out the name of the module and the context.
    var data = getScriptData(evt);
    context.completeLoad(data.id);
  }
},

所以我們這里來重新整理下requireJS的執行流程(可能有誤)

① 引入requireJS標簽后,首先執行一些初始化操作

② 執行req({})初始化newContext,並且保存至contexts對象中

③ 執行req(cfg),將讀取的data-main屬性並且封裝為參數實例化模塊

④ 執行main.js中的邏輯,執行require時候,會一次加載name與say

⑤ 調用依賴時候會根據define進行設置將加載好的標簽引入鍵值對應關系,執行點是load事件

所以關鍵點再次回到了main.js加載之后做的事情

require方法

經過之前的學習,面對requireJS我們大概知道了以下事情

① require是先將依賴項加載結束,然后再執行后面的函數回調

首先第一個就是一個難點,因為require現在是采用script標簽的方式引入各個模塊,所以我們不能確定何時加載結束,所以這里存在一個復雜的判斷以及緩存

② 依賴列表以映射的方式保存對應的模塊,事實上返回的是一個執行后的代碼,返回可能是對象可能是函數,可能什么也沒有(不標准)

這個也是一塊比較煩的地方,意味着,每一個define模塊都會維護一個閉包,而且多數時候這個閉包是無法釋放的,所以真正大模塊的單頁應用有可能越用越卡

面對這一問題,一般采用將大項目分頻道的方式,以免首次加載過多的資源,防止內存占用過度問題

③ 加載模塊時候會創建script標簽,這里為其綁定了onload事件判斷是否加載結束,若是加載結束,會在原來的緩存模塊中找到對應模塊並且為其賦值,這里又是一個復雜的過程

require的整體理解之所以難,我覺得就是難在異步加載與循環依賴一塊,異步加載導致程序比較晦澀

所以我們再次進入程序看看,這一切是如何發生的,這里先以main.js為例

再說main模塊的加載

經過之前的學習,main模塊加載之前會經歷如下步驟

① require調用req({})初始化一個上下文環境(newContext)

② 解析頁面script標簽,碰到具有data-main屬性的標簽便停下,並且解析他形成第一個配置項調用req(cfg)

③ 內部調用統一入口requirejs,並取出上文實例化后的上下文環境(context),執行其require方法

④ 內部調用localRequire(makeRequire)方法,這里干了比較重要的事情實例化模塊

⑤ 模塊的實例化發生在localRequire中,這里的步驟比較關鍵

首先,這里會調用nextTick實際去創建加載各個模塊的操作,但是這里有一個settimeout就比較麻煩了,所有的操作會拋出主干流程之外

這樣做的意義我暫時不能了解,可能這段邏輯會異步加載script標簽,若是不拋到主干流程外會有問題吧,若是您知道請告知

nextTick使用 settimeout 的原因不明,待解決/經測試不加延時可能導致加載順序錯亂

我們這里干一件不合理的事情,將nexttick的延時給去掉試試整個邏輯

req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) {
  setTimeout(fn, 4);
} : function (fn) { fn(); };

req.nextTick = function (fn){ fn();};

如此,除了script加載一塊就不再具有異步的問題了,這里我們來從新理一理

深入req(cfg)

第一步調用req(cfg)

第二步處理參數並且調用require

可以看到,經過require.config(config)的處理,相關的參數已經放到了實例里面

第三步調用localRequire,並且進入nextTick流程,一個要注意的地方是這里的this指向的是context

第四步執行intakeDefines,將全局的依賴裝入,這里是沒有的

第五步實例化模塊(makeModuleMap),建立映射關系,最后會返回類似這樣的東西

第六步便將此映射關系傳入getModule建立相關模塊,然后傳入該映射關系對象建立模塊,Module類根據參數對象作簡單初始化便返回

第七步調用mod的init方法真正操作該模塊,這里會執行加載邏輯Module的init方法,最后會到context的load方法加載script標簽,值得注意的是加載結束后這里會綁定onScriptLoad方法

第八步加載成功后會調用context.completeLoad(data.id)方法

因為之前定義過該模塊了,這里只是將其取出(mod = getOwn(registry, moduleName))然后再調用其模塊init方法又會走一連串邏輯,最后再check一塊結束

if (this.map.isDefine && !this.ignore) {
  defined[id] = exports;

  if (req.onResourceLoad) {
    req.onResourceLoad(context, this.map, this.depMaps);
  }
}

因為每一個加載模塊都會定義一個事件,在其實際加載結束后會執行之

if (this.defined && !this.defineEmitted) {
  this.defineEmitted = true;
  this.emit('defined', this.exports);
  this.defineEmitComplete = true;
}

最后會調用checkLoaded檢查是否還有未加載的模塊,總之這步結束后基本上就main.js就加載結束了,這個由全局contexts中的defined對象可以看出

這里仍然有一塊比較難,因為在main.js加載結束前還未執行其load事件,其下一步的require流程又開始了

contexts._.defined
Object {}

這個時候全局的defined還沒有東西呢,所以他這里會有一個狀態機做判斷,否則最后不會只是main.js中的fn

require(['name', 'say'], function (name, say) {
  say(name);
});

他們判斷的方式就是不停的check,不停的check,直到加載成功結束

main.js中的require

因為main模塊並不具有模塊,所以其執行邏輯還是稍有不同的,我們現在將關注點放到main.js中的require相關邏輯

require(['name', 'say'], function (name, say) {
  say(name);
});

首次進入這個邏輯時候事實上main.js的onload事件並未執行,所以全局contexts._.defined對象依舊為空,這里進入了實際模塊的加載邏輯既有依賴項又有回調

PS:這里有一個比較有意思的做法就是將原來的nextTick的settimeout干掉這里的情況會有所不同

依舊進入context.require流程

 return context.require(deps, callback, errback);

期間會碰到main.js onload事件觸發,並導致

contexts._.defined => Object {main: undefined}

第二步便是這里的會創建一個模塊,這個與,而后調用其init方法,這里需要注意的是傳入了deps(name, say)依賴,所以這里的depMaps便不為空了

並且這里將當前回調傳給factory,並且將依賴的name與say模塊保存

 

this.factory = factory;
this.depMaps = depMaps && depMaps.slice(0);

進入enable流程,首先注冊當前對象之閉包(newContext)enableRegistry中

這里有一個操作是如果具有依賴關系,我們這里便依賴於say以及name會執行一個邏輯

//Enable each dependency
each(this.depMaps, bind(this, function (depMap, i) {
  var id, mod, handler;

  if (typeof depMap === 'string') {
    //Dependency needs to be converted to a depMap
    //and wired up to this module.
    depMap = makeModuleMap(depMap,
                                  (this.map.isDefine ? this.map : this.map.parentMap),
                                  false,
                                  !this.skipMap);
    this.depMaps[i] = depMap;

    handler = getOwn(handlers, depMap.id);

    if (handler) {
      this.depExports[i] = handler(this);
      return;
    }

    this.depCount += 1;

    on(depMap, 'defined', bind(this, function (depExports) {
      this.defineDep(i, depExports);
      this.check();
    }));

    if (this.errback) {
      on(depMap, 'error', bind(this, this.errback));
    }
  }

  id = depMap.id;
  mod = registry[id];

  //Skip special modules like 'require', 'exports', 'module'
  //Also, don't call enable if it is already enabled,
  //important in circular dependency cases.
  if (!hasProp(handlers, id) && mod && !mod.enabled) {
    context.enable(depMap, this);
  }
}));

循環的載入其依賴項,並形成模塊,這里都會搞進enableRegistry中,比如這段邏輯結束前后有所不同

事實上對應模塊初始化已經結束,進入了script待加載邏輯,只不過暫時卡到這里了......

然后這里會進入其check邏輯,由於這里defineDep等於2所以不會執行函數回調,而直接跳出,這里有一個關鍵便是我們的Registry未被清理

以上邏輯只是在main.js中require方法執行后所執行的邏輯,確切的說是這段代碼所執行的邏輯

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

然后會執行一個checkLoaded方法檢測enabledRegistry中未加載完成的模塊並且進行清理,這段邏輯比較關鍵

function checkLoaded() {
  var map, modId, err, usingPathFallback,
            waitInterval = config.waitSeconds * 1000,
  //It is possible to disable the wait interval by using waitSeconds of 0.
            expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(),
            noLoads = [],
            reqCalls = [],
            stillLoading = false,
            needCycleCheck = true;

  //Do not bother if this call was a result of a cycle break.
  if (inCheckLoaded) {
    return;
  }

  inCheckLoaded = true;

  //Figure out the state of all the modules.
  eachProp(enabledRegistry, function (mod) {
    map = mod.map;
    modId = map.id;

    //Skip things that are not enabled or in error state.
    if (!mod.enabled) {
      return;
    }

    if (!map.isDefine) {
      reqCalls.push(mod);
    }

    if (!mod.error) {
      //If the module should be executed, and it has not
      //been inited and time is up, remember it.
      if (!mod.inited && expired) {
        if (hasPathFallback(modId)) {
          usingPathFallback = true;
          stillLoading = true;
        } else {
          noLoads.push(modId);
          removeScript(modId);
        }
      } else if (!mod.inited && mod.fetched && map.isDefine) {
        stillLoading = true;
        if (!map.prefix) {
          //No reason to keep looking for unfinished
          //loading. If the only stillLoading is a
          //plugin resource though, keep going,
          //because it may be that a plugin resource
          //is waiting on a non-plugin cycle.
          return (needCycleCheck = false);
        }
      }
    }
  });

  if (expired && noLoads.length) {
    //If wait time expired, throw error of unloaded modules.
    err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads);
    err.contextName = context.contextName;
    return onError(err);
  }

  //Not expired, check for a cycle.
  if (needCycleCheck) {
    each(reqCalls, function (mod) {
      breakCycle(mod, {}, {});
    });
  }

  //If still waiting on loads, and the waiting load is something
  //other than a plugin resource, or there are still outstanding
  //scripts, then just try back later.
  if ((!expired || usingPathFallback) && stillLoading) {
    //Something is still waiting to load. Wait for it, but only
    //if a timeout is not already in effect.
    if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) {
      checkLoadedTimeoutId = setTimeout(function () {
        checkLoadedTimeoutId = 0;
        checkLoaded();
      }, 50);
    }
  }

  inCheckLoaded = false;
}

他首先會遍歷enableRegistry取出其中定義的模塊,並且將沒有加載成功的模塊標識注入noLoads數組,如果過期了這里就會報錯

如果上述沒問題還會做循環依賴的判斷,主要邏輯在breakCycle中,因為我們這里不存在循環依賴便跳出了,但還未結束

我們這里開始了遞歸檢測依賴是否載入

if ((!expired || usingPathFallback) && stillLoading) {
  //Something is still waiting to load. Wait for it, but only
  //if a timeout is not already in effect.
  if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) {
    checkLoadedTimeoutId = setTimeout(function () {
      checkLoadedTimeoutId = 0;
      checkLoaded();
    }, 50);
  }
}

如果模塊沒有載入,這里就會一直繼續,直到所有模塊加載結束,其判斷點又在各個define方法中,define方法會根據鍵值改變對應模塊的標識值

幾個關鍵判斷點為:

① checkLoadedTimeoutId

② inCheckLoaded

③ stillLoading

但是最終的判斷點事實上來源與mod的mod.inited/fetched/isDefine等屬性,所以我們這里需要來理一理

首次模塊執行init方法時會執行

 this.inited = true;

因為初始化時候動態的傳入了enabled為true所以首次會執行enable邏輯

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

if (options.enabled || this.enabled) {
  //Enable this module and dependencies.
  //Will call this.check()
  this.enable();
} else {
  this.check();
}

於是達成enabled為true的條件,這里並且會為該模塊的依賴執行enable操作,並且為其支持defined事件在加載結束后會觸發之

each(this.depMaps, bind(this, function (depMap, i) {
  var id, mod, handler;

  if (typeof depMap === 'string') {
    //Dependency needs to be converted to a depMap
    //and wired up to this module.
    depMap = makeModuleMap(depMap,
                                  (this.map.isDefine ? this.map : this.map.parentMap),
                                  false,
                                  !this.skipMap);
    this.depMaps[i] = depMap;

    handler = getOwn(handlers, depMap.id);

    if (handler) {
      this.depExports[i] = handler(this);
      return;
    }

    this.depCount += 1;

    on(depMap, 'defined', bind(this, function (depExports) {
      this.defineDep(i, depExports);
      this.check();
    }));

    if (this.errback) {
      on(depMap, 'error', bind(this, this.errback));
    }
  }

  id = depMap.id;
  mod = registry[id];

  //Skip special modules like 'require', 'exports', 'module'
  //Also, don't call enable if it is already enabled,
  //important in circular dependency cases.
  if (!hasProp(handlers, id) && mod && !mod.enabled) {
    context.enable(depMap, this);
  }
}));

這里的邏輯比較關鍵,而后才執行該模塊的check方法

PS:讀到這里,才大概對requireJS的邏輯有一定認識了

跳入check方法后便會將defining設置為true因為這里的依賴項未載入結束,所以這里的depCount為2,所以不會觸發

this.defined = true;

所以下面的遞歸settimeout會一直執行,直到成功或者超時,這里我們進入define相關流程

define方法

這里以say為例,在加載文件結束時候會觸發其define方法,這里主要向globalDefQueue中插入當前模塊的隊列,而這里上面做過介紹

而這里的關鍵會在script標簽執行onload事件時候將全局隊列的東西載入context.defQueue

而這個時候又會根據相關的映射id(由event參數獲取),實例化相關模塊(事實上是取得當前模塊,之前已經實例化),這個時候又會進入check邏輯,這個時候是say模塊的check邏輯

say無依賴,並且加載結束,這里會將當前模塊與其返回值做依賴,這里是一個函數,這里factory與exports的關系是

然后會將當前鍵值右Registry相關刪除,完了便會進入下面的邏輯,值得注意的是這里會觸發前面為say模塊注冊的defined事件

PS:這里一定要注意,這里的say模塊里面定義的家伙被執行了!!!

//注冊點
on(depMap, 'defined', bind(this, function (depExports) {
  this.defineDep(i, depExports);
  this.check();
}));

//觸發點
this.emit('defined', this.exports);

//關鍵點,用於消除依賴
this.defineDep(i, depExports);

所以,我們的整體邏輯基本出來了

結語

最后,我們來一次總結,對初次的requireJS學習畫下一個初步的句點

① requireJS會初始化一個默認上下文出來

req({}) => newContext

② 加載main.js,main.js與基本模塊不太一致,加載結束便會執行里面邏輯對主干流程沒有太大影響

③ 執行main.js中的require.config配置,最后調用require方法

④ 調用時候會將數組中的依賴項載入,並且實例化一個匿名模塊出來(mod)

因為主干(匿名)模塊依賴於say與name,所以會在enable中實例化兩個模塊並且將當前實例depCount設置為2

⑤ 各個依賴模塊也會執行加載操作,say以及name,若是有依賴關系會循環執行enable

⑥ 會執行主干模塊的check操作由於depCount為2便執行其它邏輯,這里為其注冊了defined事件

⑦ 執行checkLoaded方法,這里會開始遞歸的檢查模塊是否加載結束,一定要在主干模塊depCount為0 時候才會執行其回調,並且會傳入say與name返回值做參數

⑧ 當模塊加載結束后會觸發其onScriptLoad => completeLoad事件

⑨ 因為各個define模塊會想全局隊列壓入標識的值,並且會根據他獲取相關模塊並且執行其init事件

10 這個時候會執行模塊的實例化init方法,並且會檢測該模塊的依賴,say沒有依賴便繼續向下,將其factory方法執行回指exports(具有參數,參數是依賴項)

PS:其依賴項是在解除依賴時候注入的defineDep

11 最后所有依賴模塊加載時候,最后主干的depCount也就變成了0了,這個時候便會執行類似say的邏輯觸發回調

這里的關鍵就是,加載主干模塊時候會檢查器依賴項,並且為每一個依賴項注冊defined事件,其事件又會執行check方法

這也意味着,每一個依賴模塊檢查成功事實上都有可能執行主干流程的回調,其條件是主干的depCount為0,這塊就是整個requireJS的難點所在......

幾個關鍵便是

① require時候的模塊定義以及為其注冊事件

② 文件加載結束define將該模塊壓入全局隊列

③ script加載成功后觸發全局隊列的檢查

④ 各個子模塊加載結束,並且接觸主模塊依賴執,並且將自我返回值賦予行主模塊實例數組depExports

⑤ 當主模塊depCount為0 時候終於便可以觸發了,於是邏輯結束

最后,小釵渾渾噩噩的初步學習requireJS結束,感覺有點小難,等后面技術有所提高后便再學習吧


免責聲明!

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



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