前言
前幾天看了下webpack打包出來的js,豁然開朗覺得實現一個模組化工具穩穩的,真開始寫的時候才發現too young。
基本目標
// 定義模組apple:
define('apple',['orange'],function(orange){
return orange
})
// 定義模組orange:
define('orange',[],function(){
return {
name:'orange',
color:'white',
size:'small',
}
})
// 使用定義好的模組:
var a = require(['apple','jquery'],function(apple,$){
console.log(apple)
console.log($('<div>123</div>'))
})
===>輸出
{
name:'orange',
color:'white',
size:'small',
},
n.fn.init [div]
複製程式碼
實現流程
- 首先定義模組名稱與對應的路徑,也就是實現require.config功能,簡化版如下:
let paths = {
apple:'./apple.js',
orange:'./orange.js',
jquery:'https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js',
}
複製程式碼
- 然後建立一個變數用於儲存載入好的模組,建立一個變數收集所有執行require的地方。
// 存放所有註冊require的模組,收集他們的依賴,以及回撥
let reqs = {}
// 儲存載入好的模組
let modules = {}
複製程式碼
- 建立require方法,使用與amd類似的方式,接收兩個引數:1.依賴的模組陣列,2.模組載入完後將要執行的回撥。
function require(deps,callback) {
...
}
複製程式碼
將本次執行require看做一個任務,並在reqs物件中註冊下,模組載入完成後將執行本reqs中的所有任務。
// 任務名稱從0開始,最早註冊的任務為reqs[0],隨後++
let id = 0
// 建立執行模組
reqs[id] = {
deps,
id,
callback,
}
id++
複製程式碼
第一個require執行完後reqs物件將變為
{
0:{
callback:function(){..},
id:0,
deps:['apple','jquery']
}
}
複製程式碼
然後迴圈deps陣列,建立script標籤依次載入依賴的模組:
for(let item of deps) {
// 如果modules變數中還沒有儲存本模組,首先在模組中初始化本模組:1.建立模組name,2.建立watcher屬性用於記錄是哪個註冊reqs任務引用了我這個模組。然後建立script標籤非同步載入本模組。載入完成之後執行loadComplete方法。如果本模組在modoles裡已存在,說明本模組已載入過,那麼直接把reqs的任務id push到watchers裡。
if(!modules[name]) {
// 初始化模組,並記住哪個reqs任務引用了本模組。
modules[name] = {
// 存放依賴此模組的模組名
watchers:[id],
name:name,
}
var node = document.createElement('script');
node.type = 'text/javascript';
node.charset = 'utf-8';
node.setAttribute('data-requiremodule', name);
node.async = true;
document.body.appendChild(node)
node.addEventListener('load', loadComplete, false);
node.src = paths[name]
}else{
modules[name].watchers.push(id)
}
}
複製程式碼
- 按理說模組載入完成後就會執行loadComplete方法。
但需要注意的是,node.load方法會在下載好的js執行完之後才會執行。意思就是說如果載入的apple.js裡有console.log("apple模組載入好了")
,而loadComplete裡有console.log("執行script的onload方法")
,那麼執行順序是1.apple模組載入好了;2.執行script的onload方法。因為apple模組裡執行了define方法,所以先看define的定義。
- 建立define方法
本方法採用amd規範,接收三個變數:1.本模組名稱,2.本模組的依賴模組,3.本模組的執行結果。
function define(name, deps, callback){
...
}
複製程式碼
在modules變數中註冊本模組,如果本模組有依賴,執行require方法先載入依賴,等依賴載入完只有執行callback獲取模組的結果;如果本模組沒有依賴,執行本模組的callback方法得到本模組的結果。
modules[name].callback = callback
if(deps.length === 0) {
modules[name].result = callback()
}else{
// 如果有依賴,要先執行依賴
require(deps,function(){
modules[name].result = callback(...arguments)
})
}
複製程式碼
以orange模組為例,define方法執行完之後modules變數為
{
orange:{
result:{
name:'orange',
color:'white',
size:'small',
}
}
}
複製程式碼
-
定義模組是amd規範:
define.amd = true
-
下面真正到了loadComplete方法。也就是script的onload回撥。
本方法主要的任務是:執行以前註冊的那些依賴本模組的reqs任務。如果reqs任務的finish=true,說明模組已經執行過了,跳過。如果reqs任務沒有執行過,那麼拿到reqs任務deps屬性,也就是依賴哪些模組,如果所有的模組都有result(結果),執行本任務的callback,並將finish置為true.
function loadComplete(evt){
var node = evt.currentTarget || evt.srcElement;
node.removeEventListener('load', loadComplete);
let name = node.getAttribute('data-requiremodule')
modules[name].watchers.map((item)=>{
if(reqs[item].finish) return
let completed = true
let args = []
reqs[item].deps.map(item2=>{
if(!modules[item2].result) {
completed = false
}else{
args.push(modules[item2].result)
}
})
if(completed) {
reqs[item].callback(...args)
reqs[item].finish = true
reqs[item].completed = true
}
})
}
複製程式碼
參考:
更大的目標
仿照require1k實現類似requirejs的模組載入庫。程式碼
果然複雜的多,根據註釋走了一遍流程,基本上流程走的通。
參考: