Модульные принципы AMD и CMD (с исходным кодом)

внешний интерфейс

1. Введение

Теперь студенты, которые плохо знакомы с интерфейсом, начали использовать веб-пакет напрямую.Когда несколько лет назад не было узла или jquery, было очень сложно заставить разные файлы js ссылаться друг на друга и разрабатывать модульно без использования узла. или софт.хлопотная вещь.

Далее я представлю два известных инструмента, AMD (require.js) и CMD (sea.js), хотя они сейчас редко используются, вам все равно нужно знать процесс модульности, о чем также могут спросить на собеседованиях.

2. Формирование спроса

В начале над фронтендом особо работы не было, в html ввели js файл как показано на рисунке, который имеет много минусов:

  1. Вводить надо по порядку.Если в 1.js используется jquery, то ставить jquery.js выше 1.js.
  2. Загружайте каждый js синхронно и загружайте 2.js только после того, как 1.js загружен и выполнен.
  3. В каждом файле js может быть создано и изменено несколько глобальных переменных окна.
  4. ...и много минусов

Короче говоря, приведенная выше структура действительно запуталась, когда появляется все больше и больше внешнего контента, особенно тенденция ajax, разделение внешнего и внутреннего интерфейса, все больше и больше внимания интерфейсу, все больше и больше js. файлы и более сложные взаимные ссылки, поэтому необходим новый модульный инструмент.

Конечно, мы надеемся, что файлы можно будет импортировать и экспортировать друг в друга, как сейчас, но, к сожалению, это использование es6 с узлом, что требует, чтобы сервер поддерживал обработку файлов, и только модульность через статические файлы. с начала.

3. AMD

То есть определение асинхронного модуля, китайское названиеОпределение асинхронного модулязначение. Это спецификация модульной разработки, идея и концепция. Нам не нужно слишком беспокоиться о том, что это такое, просто нужно знать, что это нормативная концепция, а знаменитый require.js — ее конкретная реализация, и многие раньше использовали этот инструмент для разработки.

Использование require.js

Здесь представлено только простое использование.Студенты, которые никогда не использовали его, должны перейти к учебнику.

логин.html

Вводится файл require.js, а затем указывается файл входа login.js

~~

В файле входа login.js

логинмодуль.js

логинCtrl.js

Уведомление:

  1. На самом деле определены две функции глобальных переменных, одна require() и одна define().

  2. На самом деле функции и принципы работы этих двух функций схожи, можно подумать, что они похожи, за исключением их названий.

  3. Первый параметр функции определения — это массив, в который записываются зависимые модули, второй параметр — это функция обратного вызова, а параметры в функции обратного вызова соответствуют возвращаемым значениям модулей в зависимом массиве, например:

    es6 :

    import A from 'a.js'

    import B from 'b.js'

    require.js:

    define(['a.js', 'b.js'], function(A, B) {

    })

Самое главное это! ! ! :

Будь то CMD или AMD, это просто облегчает разработчикам написание кода, но для браузеров можно считать, что изменений не так много, сколько js-файлов нужно загрузить, а порядок js-файлов тот же как раньше. ! ! !

4. Принцип работы require.js

На данный момент мы, вероятно, знаем, что значение этих двух инструментов заключается в том, что разработчикам больше не нужно писать кучу тегов ', просто вставьте строку строки в dom, не говоря уже о js, представленном в строке, поэтому его нельзя использовать. Этот метод вставляет сценарий! ! !

2. Время загрузки каждого js-файла (временная последовательность тега скрипта, вставленного в документ)

Между файлами существуют зависимости, поэтому загрузка файлов js (вставка узлов скрипта в dom) должна быть по порядку, например: 1.js зависит от 2.js, 2.js зависит от 3.js

Итак, фактический порядок загрузки: 1.js, 2.js, 3.js.

Некоторые студенты могут спросить, само собой разумеется, что порядок загрузки должен быть обратным: 3.js, 2.js, 1.js. . . Это потому, что только после того, как 1.js будет загружен и запущен, вы узнаете, от чего зависит 1.js. . . Подумайте об этом внимательно, вы написали define([2.js]) в 1.js, значит ли это, что вам нужно загрузить и запустить 1.js, прежде чем вы сможете получить зависимость 2.js от параметров функции определения?

Уведомление:и на самом делемодуль работаетПорядок 3.js, 2.js, 1.js. . . так,Загрузка файлов, запуск файлов после загрузки, запуск модулей, это 3 вещи, не путайте сначала, я расскажу об этом в следующей части.

Продолжайте здесь, тогда вам нужно судить, когда будет загружен 1.js?

<script src="1.js" onload="alert()"></script>

Ключ в том, что,onloadЭта функция, ее роль, 1.jsзагружается и выполняетсяПосле этого выполните алерт в onload

Итак, чтобы выполнить 1.js Load 2.js после загрузки, просто сделайте следующее:


var node = document.createElement('script')
node.type = 'text/javascript'
node.src = '1.js'

// 给该节点添加onload事件,标签上onload,这里是load,见事件那里的知识点
// 1.js 加载完后onload的事件
node.addEventListener('load', function(evt) {
    // 开始加载 2.js
    var node2 = document.createElement('script')
    node2.type = 'text/javascript'
    node2.src = '2.js'
    // 插入 2.js script 节点
    document.body.appendChild(node2)
})
// 插入 1.js script 节点
document.body.appendChild(node)

Таким образом, ядром обработки зависимостей является использование события onload для непрерывного рекурсивного вложения зависимых от загрузки файлов. На самом деле, самое хлопотное здесь — иметь дело с зависимыми файлами, особенно когда от файла могут зависеть несколько файлов.

3. Время выполнения файлового модуля

Это нужно понимать, и это одно из отличий require.js от sea.js.

Я только что сказал 3 вещи,Загрузка файлов, запуск файлов после загрузки, запуск модулей, не путайте здесь, например:

// 1.js 中的代码
require([], functionA() {
    // 主要逻辑代码
})

После загрузки файла js файл будет выполнен мгновенно, затем

  • загрузка файла: Вставьте узел

  • Запуск файла после загрузки: После загрузки файла 1.js выполнить код в 1.js, то есть выполнить функцию require()! ! !

  • Работа модуля: Функция обратного вызова require, выше, основной логический код, где работает функция functionA! ! !

Итак, что мы скажем позжебегать, оба относятся кРабота модуля,а такжезапуск файлаПо умолчанию и загрузка вместе, считать не нужно. . . И логический код каждой страницы должен быть прописан в callback-функции require/define, в functionA. . .

! ! ! Обязательно тщательно продумайте порядок загрузки модулей и порядок их запуска?

Как и в es6, вы импортируете «2.js» в 1.js, разве 2.js не должен выполняться первым и возвращать вам значение? который:

Когда 1.js зависит от 2.js, то сначала загружается 1.js, но сначала запускается модуль 2.js (все же обратите внимание: это запуск модуля, а не файла!!!)

Общий порядок:

  • Порядок загрузки/запуска файла: 1.js, 2.js, 3.js

  • Порядок работы модуля: 3.js, 2.js, 1.js

5. Простая реализация кода require.js

Пример использования

// 1.js 中(入口用require,其他用define)
require(['2.js'], function(A) {
    // A得到的就是2.js模块的返回值
    // 主要的执行代码
    // 2.js 3.js都加载完,才执行1.js的这回调函数!!!!!!!!!!!!!!!
})

// 2.js 中
define(['3.js', 'xxxx.js'], functionA(B, C) {
    // B得到的就是3.js模块的返回值,C是xxxx.js的
    return aaaaa    // 2.js 模块的返回值
})

// 3.js 中
define([], functionA() {
    
    retrun {}   // 3.js 模块的返回值
})

принцип простого исходного кода require.js

Используя рекурсию для загрузки слоев вложенных зависимостей, сложность кода заключается в том, как определить конец рекурсии? То есть как судить, что все зависимости загружены?

var modules = {},	// 存放所有文件模块的信息,每个js文件模块的信息
    loadings = [];	//	存放所有已经加载了的文件模块的id,一旦该id的所有依赖都
                                加载完后,该id将会在数组中移除

// 上面说了,每个文件模块都要有个id,这个函数是返回当前运行的js文件的文件名,拿文件名作为文件对象的id
// 比如,当前加载 3.js 后运行 3.js ,那么该函数返回的就是 '3.js'
function getCurrentJs() {
	return document.currentScript.src
}
// 创建节点
function createNode() {
	var node = document.createElement('script')
	node.type = 'text/javascript'
	node.async = true;
	return node
}
// 开始运行
function init() {
    // 加载 1.js
    loadJs('1.js')
}	
// 加载文件(插入dom中),如果传了回调函数,则在onload后执行回调函数
function loadJs(url, callback) {
    var node = createNode()
    node.src = url;
    node.setAttribute('data-id', url)
    node.addEventListener('load', function(evt) {
    	var e = evt.target
    	setTimeout(() => {  // 这里延迟一秒,只是让在浏览器上直观的看到每1秒加载出一个文件
    		callback && callback(e)
    	}, 1000)
    }, false)
    
    document.body.appendChild(node)
}	
	
// 此时,loadJs(1.js)后,并没有传回调函数,所以1.js加载成功后只是自动运行1.js代码
// 而1.js代码中,是require( ['2.js', 'xxx.js'], functionA(B, C){} ),则执行的是require函数, 在下面是require的定义

window.require = function(deps, callback) {
    // deps 就是对应的 ['2.js', 'xxx.js']
    // callback 就是对应的 functionA
    // 在这里,是不会运行callback的(即模块的运行!),得等到所有依赖都加载完的啊
    // 所以得有个地方,把一个文件的所有信息都先存起来啊,尤其是deps和callback
    var id = getCurrentJs();// 当前运行的是1.js,所以id就是'1.js'
    if(!modules.id) {
    	modules[id] = { // 该模块对象信息
    		id: id,
    		deps: deps,
    		callback: callback, 
    		exports: null,  // 该模块的返回值return ,
    		就是functionA(B, C)运行后的返回值,仔细想想?在后面的getExports中详细讲
    		
    		status: 1, 
    		
    	}
    	loadings.unshift(id); // 加入这个id,之后会循环loadings数组,递归判断id所有依赖
    }
    
    loadDepsJs(id); // 加载这个文件的所有依赖,即去加载[2.js]
}

function loadDepsJs(id) {
    var module = modules[id]; // 获取到这个文件模块对象
    // deps是['2.js']
    module.deps.map(item => {   // item 其实是依赖的Id,即 '2.js'
        if(!modules[i]) {   // 如果这个文件没被加载过(注:加载过的肯定在modules中有)
        (1)    loadJs(item, function() {   // 加载 2.js,并且传了个回调,准备要递归了
                    // 2.js加载完后,执行了这个回调函数
                    loadings.unshift(item); // 此时里面有两个了, 1.js 和 2.js
                    // 递归。。。要去搞3.js了
                    loadDepsJs(item)// item传的2.js,递归再进来时,就去modules中取2.js的deps了
                    // 每次检查一下,是否都加载完了
                    checkDeps(); // 循环loadings,配合递归嵌套和modules信息,判断是否都加载完了
                })
        }
    })
}

// 上面(1)那里,加载了2.js后马上会运行2.js的,而2.js里面是
define(['js'], fn)
// 所以相当于执行了 define函数

window.define = function(deps,callback) {
    var id = getCurrentJs()
    if(!modules.id) {
        modules[id] = {
        	id: id,
        	deps: getDepsIds(deps),
        	callback: callback,
        	exports: null,
        	status: 1,
        	
        }
    }
}

// 注意,define运行的结果,只是在modules中添加了该模块的信息
// 因为其实在上面的loadDepsJs中已经事先做了loadings和递归deps的操作,
而且是一直不断的循环往复的进行探查,所以define里面就不需要再像require中写一次loadDeps了

// 循环loadings,查看loadings里面的id,其所依赖的所有层层嵌套的依赖模块是否都加载完了

function checkDeps() {
    for(var i = 0, id; i < loadings.length ; i++) {
	id = loadings[i]
	if(!modules[id]) continue
	
	var obj = modules[id], 
	deps = obj.deps
	
	// 下面那行为什么要执行checkCycle函数呢,checkDeps是循环loadings数组的模块id,而checkCycle是去判断该id模块所依赖的**层级**的模块是否加载完
	// 即checkDeps是**广度**的循环已经加载(但依赖没完全加载完的)的id
	// checkCycle是**深度**的探查所关联的依赖
	// 还是举例吧。。。假如除了1.js, 2.js, 3.js, 还有个4.js,依赖5.js,那么
	// loadings 可能 是 ['1.js', '4.js']
	// 所以checkDeps --> 1.js,  4.js
	// checkCycle深入内部 1.js --> 2.js --> 3.js ;;; 4.js --> 5.js
	// 一旦比如说1.js的所有依赖2.js、3.js都加载完了,那么1.js 就会在loadings中移出
	
	var flag = checkCycle(deps)
	
	if(flag) {
            console.log(i, loadings[i] ,'全部依赖已经loaded');
		
            loadings.splice(i,1);
            // !!!运行模块,然后同时得到该模块的返回值!!!
            getExport(obj.id)
            // 不断的循环探查啊~~~~
            checkDeps()
	}
	
    }
}
// 深层次的递归的去判断,层级依赖是否都加在完了
// 进入1.js的依赖2.js,再进入2.js的依赖3.js ......
function checkCycle(deps) {
   var flag = true
   
   function cycle(deps) {
        deps.forEach(item => {
             if(!modules[item] || modules[item].status == 1) {
                   flag = false
             } else if(modules[item].deps.length) {
//                         console.log('inner deps', modules[item].deps);
                   
                   cycle(modules[item].deps)
             }
                   
        })
   }
   
   cycle(deps)
   
   return flag
}

/*
    运行该id的模块,同时得到模块返回值,modules[id].export
*/
function getExport(id) {
/*
    先想一下,例如模块2.js, 这时 id == 2.js
    define(['3.js', 'xxxx.js'], functionA(B, C) {
        // B得到的就是3.js模块的返回值,C是xxxx.js的
        return aaaaa    // 2.js 模块的返回值
    })
    所以:
    1. 运行模块,就是运行 functionA (模块的callback)
    2. 得到模块的返回值,就是functionA运行后的返回值 aaaaa
    问题:
    1. 运行functionA(B, C)   B, C是什么?怎么来的?
    2. 有B, C 了,怎么运行functionA ?
    
*/
    // 解决问题1
    // B, C 就是该模块依赖 deps [3.js, xxxx.js]对应的返回值啊 
    // 那么循环deps 得到 依赖模块Id, 取模块的export。。。
   var params = [];
   var deps = modules[id].deps  
   
   for(var i = 0; i < deps.length; i++) {
        // 取依赖模块的exports即模块返回值,注意不要害怕取不到,因为你这个模块
        都进来打算运行了,那么你的所有依赖的模块早都进来过运行完了(还记得模块运行顺序不?)
        let depId = deps[i]
        params.push( modules[ depId ].exports ) 
        
   }
   
   // 到这里,params就是依赖模块的返回值的数组,也就是B,C对应的实参
   // 也就是 params == [3.js的返回值,xxxx.js的返回值]
   
   if(!modules[id].exports) {
        // 解决问题2: callback(functionA)的执行,用.apply,这也是为什么params是个数组了
        // 这一行代码,既运行了该模块,同时也得到了该模块的返回值export
        modules[id].exports = modules[id].callback.apply(global, params)
   }
  
}
	

Сложность кода заключается в checkDeps и рекурсии на загрузках, сложно объяснить внятно, нужно писать и практиковаться самому, и описать все это тут сложно. . .

В конце будет приведен простой рабочий пример

Не думайте, что это займет час или два, чтобы все сделать, это будет очень раздражать в начале, возвращайтесь несколько раз и изучайте это каждый раз, каждый раз это будет немного углубляться.

6. CMD

CMD — это общее определение модуля, определение общего модуля, sea.js — его реализация.

Sea.js написан великим богом Али. Он очень похож на require.js. Давайте сначала посмотрим на разницу в использовании.

// 只有define,没有require
// 和AMD那个例子一样,还是1依赖2, 2依赖3
1.js中
define(function() {
    
    var a = require('2.js')
    console.log(33333)
    var b = require('4.js')
})

2.js 中
define(function() {
    var b = require('3.js')
})
3.js 中
define(function() {
    // xxx
})

Выглядит немного лучше, чем require.js. . .

Разница между AMD и CMD

для зависимых модулейвремя исполненияДругое, обратите внимание: не время загрузки, время загрузки модуля одинаково! ! !

Порядок загрузки файлов:Оба сначала загружают 1.js, затем загружают 2.js и, наконец, загружают 3.js.

Порядок запуска модуля:

AMD:3.js, 2.js, 1.js, , то есть если модуль и зависимости модуля загружены, то выполнить. . . Например, после загрузки 3.js вы обнаружите, что у вас нет зависимостей, поэтому обратный вызов 3.js выполняется напрямую.После загрузки 2.js обнаруживается, что зависимый 3.js также загружен, затем 2.js выполняется собственный обратный вызов. . . . Основной модуль должен выполняться последним

Командная строка:1.js, 2.js, 3.js, то есть сначала выполняется основной модуль 1.js, а когда встречается require('2.js'), выполняется 2.js, и require(' 3. js') для выполнения 3.js

Я не пойму, как я могу контролировать, какой файловый модуль выполняется? Когда он будет исполнен?

Помнишь, я уже говорил,исполнительный модуль, относится к выполнению функции обратного вызова functionA, обратного вызова, тогда эта функция обратного вызова фактически назначается модулям через параметры при выполнении define() в начале, поэтому будь то CMD или AMD,исполнительный модуль, все выполняют модули[id].callback()

Поэтому в sea.js вы используете var a = require('2.js') для выполнения функции require в исходном коде, которая просто выполняет обратный вызов модуля.

7. Исходный код sea.js

Большая часть исходного кода очень похожа на require.js.Упомянутое выше время выполнения другое, и оно очень простое.Это контроль времени выполнения modules[id].callback.

Как я уже говорил, загрузка модулей почти одинакова, так как же sea.js контролирует загрузку 3.js и 2.js через require(3.js) и require(2.js)? ? ? Как упоминалось выше, функция require уже выполняет обратный вызов, поэтому функция require не может взять на себя функцию загрузки модулей Давайте посмотрим на это еще раз.

CMD определяет

Применение

define(function() {
    var a = require('2.js')
})

определить определение исходного кода

window.define = function(callback) {
    var id = getCurrentJs()
    var depsInit = s.parseDependencies(callback.toString())
    var a = depsInit.map(item => basepath + item)
    // 和require.js的define相比,就多了上面的2行代码
    // 1. 把传进来的函数给转换成字符串,'function (){var a = require("2.js")}'
    // 2. 利用一个正则函数,取出字符串中require中的2.js,最后拼成一个数组['2.js']返回来。
    // 3. 之后就和require.js差不多了啊。。。
    
    
    // 下面的都差不多
    if(!modules[id]) {
        modules[id] = {
            id: id,
            status: 1,
            callback: callback,
            deps: a,
            exports: null
        }
    }
    
    s.loadDepsJs(id)

    }

Итак, sea.js написал обычную функцию для запроса строки fn, переданной в define, а затем для получения массива зависимостей. . . Массив зависимостей require.js пишется и передается нами: define(['2.js']). . .

Этот обычный метод вам не нужно изучать, просто используйте его непосредственно во время практики.

8. Наконец

Простой адрес исходного кода:

my-require.js

my-sea.js

Содержание этой статьи действительно немного многословно, но я надеюсь, что его можно объяснить простым и понятным образом. В будущем я продолжу улучшать статью.Вы также можете оставить сообщение и задать вопросы.Пожалуйста, поправьте меня, если я ошибаюсь~~~ Я надеюсь, что это может быть полезно для всех.

Пожалуйста, укажите источник для перепечатки, спасибо~~