實現一個requirejs原型demo

哈哈TT發表於2018-01-31

前言

前幾天看了下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]
複製程式碼

實現流程

  1. 首先定義模組名稱與對應的路徑,也就是實現require.config功能,簡化版如下:
let paths = {
    apple:'./apple.js',
    orange:'./orange.js',
    jquery:'https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js',
}
複製程式碼
  1. 然後建立一個變數用於儲存載入好的模組,建立一個變數收集所有執行require的地方。
// 存放所有註冊require的模組,收集他們的依賴,以及回撥
let reqs = {}

// 儲存載入好的模組
let modules = {}
複製程式碼
  1. 建立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)
    }
}
複製程式碼
  1. 按理說模組載入完成後就會執行loadComplete方法。

但需要注意的是,node.load方法會在下載好的js執行完之後才會執行。意思就是說如果載入的apple.js裡有console.log("apple模組載入好了"),而loadComplete裡有console.log("執行script的onload方法"),那麼執行順序是1.apple模組載入好了;2.執行script的onload方法。因為apple模組裡執行了define方法,所以先看define的定義。

  1. 建立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',
        }
    }
}
複製程式碼
  1. 定義模組是amd規範:define.amd = true

  2. 下面真正到了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
        }
    })
}
複製程式碼

程式碼

參考:

瀏覽器載入 CommonJS 模組的原理與實現

更大的目標

仿照require1k實現類似requirejs的模組載入庫。程式碼

果然複雜的多,根據註釋走了一遍流程,基本上流程走的通。

參考:

requirejs 原始碼與架構分析

requirejs原始碼學習筆記(一)

require1k

相關文章