Эта статья впервые появилась на моемЗнай колонку, отправлено в Наггетс. Если вам нужно использовать его в коммерческих целях, пожалуйста, получите мое согласие.
Респект каждому фронтенщику, который серьезно пишет статьи.В конце статьи дана справочная статья по моим идеям.
предисловие
Студенты, у которых есть доступ к этой статье, изначально хотели узнать, как написать механизм шаблонов для JavaScript. Чтобы позаботиться о некоторых учащихся, которые никогда не использовали механизм шаблонов, давайте сначала представим, что такое механизм шаблонов.
Если вы не использовали механизм шаблонов, но попытались отобразить список на странице, общий подход заключается в объединении строк следующим образом:
const arr = [{
"name": "google",
"url": "https://www.google.com"
}, {
"name": "baidu",
"url": "https://www.baidu.com/"
}, {
"name": "凯斯",
"url": "https://www.zhihu.com/people/Uncle-Keith/activities"
}]
let html = ''
html += '<ul>'
for (var i = 0; i < arr.length; i++) {
html += `<li><a href="${arr[i].url}">${arr[i].name}</a></li>`
}
html += '</ul>'
В приведенном выше коде я использовал синтаксис ES6 с обратной кавычкой (``) для динамического создания списка ul, который не выглядит сложным (если вы используете конкатенацию строк, это будет намного более громоздко), но здесь немного плохо предмет:Сильная связь данных и структуры. Проблема, вызванная этим, заключается в том, что если данные или структура изменяются, приведенный выше код необходимо изменить, что невыносимо в текущей разработке внешнего интерфейса.Нам нужна слабая связь между данными и структурой.
Если вы хотите добиться слабой связи, то структура должна быть структурирована.После того, как данные получены с сервера и организованы, данные отображаются через шаблон, чтобы мы могли сосредоточиться на JavaScript. Вот как вы это делаете с помощью шаблонизатора. следующим образом:
HTML-список
<ul>
<% for (var i = 0; i < obj.users.length; i++) { %>
<li>
<a href="<%= obj.users[i].url %>">
<%= obj.users[i].name %>
</a>
</li>
<% } %>
</ul>
JS-данные
const arr = [{
"name": "google",
"url": "https://www.google.com"
}, {
"name": "baidu",
"url": "https://www.baidu.com/"
}, {
"name": "凯斯",
"url": "https://www.zhihu.com/people/Uncle-Keith/activities"
}]
const html = tmpl('list', arr)
console.log(html)
Напечатанный результат
" <ul>
<li><a href="https://www.google.com">google</a>
</li>
<li><a href="https://www.baidu.com/">baidu</a>
</li>
<li><a href="https://www.zhihu.com/people/Uncle-Keith/activities">凯斯</a>
</li>
</ul> "
Как видно из приведенного выше кода, сплайсинга можно добиться, передав структуру и данные в функцию tmpl. А tmpl — это то, что мы называем шаблонизатором (функцией). Далее мы реализуем эту функцию.
Реализация шаблонизатора
Данные запихиваются в шаблон через функцию, а конкретная реализация внутри функции по-прежнему реализуется путем сращивания строк. Шаблонный метод может уменьшить увеличение временных затрат, вызванное ошибкой склейки строк.
Суть реализации функции шаблонизатора заключается в том, чтобыСтруктура HTML в шаблоне отделена от операторов и переменных JavaScript, а HTML-код с данными динамически генерируется с помощью конструктора функций + применения (вызова).И если вы хотите рассмотреть производительность, вы можете сделать шаблонРабота с кэшем.
Запомните суть сказанного выше и даже продекламируйте это.
Чтобы реализовать функцию механизма шаблонов, необходимо выполнить примерно следующие шаги:
- получение шаблона
- Структура HTML в шаблоне отделена от операторов и переменных JavaScript.
- Функция + apply(call) динамически генерирует код JavaScript
- Кэш шаблонов
Хорошо, давайте посмотрим, как это реализовать :)
- получение шаблона
При нормальных обстоятельствах мы запишем шаблон в тег script и назначим атрибут id для идентификации уникальности шаблона; назначим атрибут type='text/html' для идентификации его MIME-типа как HTML, как показано ниже.
<script type="text/html" id="template">
<ul>
<% if (obj.show) { %>
<% for (var i = 0; i < obj.users.length; i++) { %>
<li>
<a href="<%= obj.users[i].url %>">
<%= obj.users[i].name %>
</a>
</li>
<% } %>
<% } else { %>
<p>不展示列表</p>
<% } %>
</ul>
</script>
В механизме шаблонов <% xxx %> используется для идентификации операторов JavaScript, которые в основном используются для управления процессом без вывода, <% xxx %> используется для идентификации переменных JavaScript, которые используются для вывода данных в шаблон; остальные - HTML-коды. (аналогично EJS). Конечно, вы также можете использовать <@ xxx @>, , >, <*= ххх *> и т. д.@>%>%>
Первым параметром, передаваемым в функцию механизма шаблонов, может быть идентификатор или строка шаблона. На данный момент необходимо судить, является ли это строкой шаблона или идентификатором с помощью регулярных выражений. следующим образом
let tpl = ''
const tmpl = (str, data) => {
// 如果是模板字符串,会包含非单词部分(<, >, %, 等);如果是id,则需要通过getElementById获取
if (!/[\s\W]/g.test(str)) {
tpl = document.getElementById(str).innerHTML
} else {
tpl = str
}
}
2.Структура HTML отделена от операторов и переменных JavaScript.
Этот шаг является самым важным шагом в двигателе, и если он достигнут, это большой шаг. Поэтому мы используем два метода для его достижения. Если полученная строка шаблона выглядит следующим образом:
" <ul>
<% if (obj.show) { %>
<% for (var i = 0; i < obj.users.length; i++) { %>
<li>
<a href="<%= obj.users[i].url %>">
<%= obj.users[i].name %>
</a>
</li>
<% } %>
<% } else { %>
<p>不展示列表</p>
<% } %>
</ul> "
Давайте сначала рассмотрим первый метод, который в основном достигается заменой функции замены. Объясните основной процесс:
- Создайте массив arr, а затем соедините строку arr.push('
- При встрече с новой строкой и возвратом каретки замените ее пустой строкой.
- Когда встречается <%>
- Встречая >%, замените его на arr.push('
- При встрече <% xxx %> объедините шаги 3 и 4 и замените на '); arr.push(xxx); arr.push('%>
- Наконец, соедините строку '); return p.join(''); %>
В коде шаг 5 нужно писать перед шагами 2 и 3, потому что он имеет более высокий приоритет, иначе будет ошибка сопоставления. следующим образом
let tpl = ''
const tmpl = (str, data) => {
// 如果是模板字符串,会包含非单词部分(<, >, %, 等);如果是id,则需要通过getElementById获取
if (!/[\s\W]/g.test(str)) {
tpl = document.getElementById(str).innerHTML
} else {
tpl = str
}
let result = `let p = []; p.push('`
result += `${
tpl.replace(/[\r\n\t]/g, '')
.replace(/<%=\s*([^%>]+?)\s*%>/g, "'); p.push($1); p.push('")
.replace(/<%/g, "');")
.replace(/%>/g, "p.push('")
}`
result += "'); return p.join('');"
}
Смакуя каждый описанный выше шаг, вы можете соединять структуры HTML с операторами и переменными JavaScript. Код после склейки выглядит следующим образом (форматировать код, иначе нет разрыва строки)
" let p = [];
p.push('<ul>');
if (obj.show) {
p.push('');
for (var i = 0; i < obj.users.length; i++) {
p.push('<li><a href="');
p.push(obj.users[i].url);
p.push('">');
p.push(obj.users[i].name);
p.push('</a></li>');
}
p.push('');
} else {
p.push('<p>不展示列表</p>');
}
p.push('</ul>');
return p.join(''); "
Здесь следует отметить, что мы не можем помещать операторы JavaScript в массив, а существуем сами по себе. Потому что, если вы введете в виде оператора JS, будет сообщено об ошибке; если вы вставите в виде строки, это не сработает, например, цикл for и если суждение будет недействительным. Конечно, при проталкивании переменной JavaScript в массив следует учитывать, что она не может быть в виде строки, иначе она будет недействительна. Такие как
p.push('for(var i =0; i < obj.users.length; i++){') // 无效
p.push('obj.users[i].name') // 无效
p.push(for(var i =0; i < obj.users.length; i++){) // 报错
Из функции механизма шаблонов видно, что мы используем одинарные кавычки для объединения структуры HTML.Если немного подумать, если в шаблоне появятся одинарные кавычки, это повлияет на выполнение всей функции. Еще одна вещь: если есть обратная кавычка \, одинарная кавычка будет экранирована. Поэтому вам нужно оптимизировать одинарные кавычки и обратные кавычки.
- Обнаружено \ обратная кавычка в шаблоне, и его необходимо экранировать
- Обнаружена одинарная кавычка, ее нужно экранировать
Преобразованный в код, т.е.
str.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
Объединение частей выше, т.е.
let tpl = ''
const tmpl = (str, data) => {
// 如果是模板字符串,会包含非单词部分(<, >, %, 等);如果是id,则需要通过getElementById获取
if (!/[\s\W]/g.test(str)) {
tpl = document.getElementById(str).innerHTML
} else {
tpl = str
}
let result = `let p = []; p.push('`
result += `${
tpl.replace(/[\r\n\t]/g, '')
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/<%=\s*([^%>]+?)\s*%>/g, "'); p.push($1); p.push('")
.replace(/<%/g, "');")
.replace(/%>/g, "p.push('")
}`
result += "'); return p.join('');"
}
Функция механизма шаблонов здесь использует синтаксис и регулярные выражения ES6.Если вы запутались в регулярных выражениях, вы можете сначала изучить регулярные выражения, а затем вернуться к этой статье после того, как вы это поймете, и вы внезапно это поймете.
Хорошо, давайте рассмотрим второй метод реализации функций механизма шаблонов. В отличие от первого способа, это не просто замена с помощью функции replace. Кратко объясните мысль:
- Требуется регулярное выражение /<%>]+?)\s*%>/g, которое может соответствовать <% xxx %>, <% xxx %>%>%>%>
- Вспомогательный переменный курсор необходим для записи начальной позиции совпадения структуры HTML.
- Необходимо использовать функцию exec, и значение внутреннего индекса в процессе сопоставления будет динамически изменяться в зависимости от каждого успешного сопоставления.
- В остальном логика аналогична первому способу
Хорошо, давайте посмотрим на конкретный код
let tpl = ''
let match = '' // 记录exec函数匹配到的值
// 匹配模板id
const idReg = /[\s\W]/g
// 匹配JavaScript语句或变量
const tplReg = /<%=?\s*([^%>]+?)\s*%>/g
const add = (str, result) => {
str = str.replace(/[\r\n\t]/g, '')
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
result += `result.push('${string}');`
return result
}
const tmpl = (str, data) => {
// 记录HTML结构匹配的开始位置
let cursor = 0
let result = 'let result = [];'
// 如果是模板字符串,会包含非单词部分(<, >, %, 等);如果是id,则需要通过getElementById获取
if (!idReg.test(str)) {
tpl = document.getElementById(str).innerHTML
} else {
tpl = str
}
// 使用exec函数,每次匹配成功会动态改变index的值
while (match = tplReg.exec(tpl)) {
result = add(tpl.slice(cursor, match.index), result) // 匹配HTML结构
result = add(match[1], result) // 匹配JavaScript语句、变量
cursor = match.index + match[0].length // 改变HTML结果匹配的开始位置
}
result = add(tpl.slice(cursor), result) // 匹配剩余的HTML结构
result += 'return result.join("")'
}
console.log(tmpl('template'))
Вспомогательная функция add используется выше.Каждый раз, когда передается str, необходимо оптимизировать входящую строку шаблона, чтобы предотвратить появление в шаблоне недопустимых символов (перевод строки, возврат каретки, одинарная кавычка ', обратная кавычка \ и т. д.). строка.). После выполнения код форматируется следующим образом (на самом деле разрыва строки нет, т.к. он заменяется пустой строкой, для вида..).
" let result =[];
result.push('<ul>');
result.push('if (obj.show) {');
result.push('');
result.push('for (var i = 0; i < obj.users.length; i++) {');
result.push('<li><a href="');
result.push('obj.users[i].url');
result.push('">');
result.push('obj.users[i].name');
result.push('</a></li>');
result.push('}');
result.push('');
result.push('} else {');
result.push('<p>什么鬼什么鬼</p>');
result.push('}');
result.push('</ul>');
return result.join("") "
Из приведенного выше кода видно, что структура HTML помещается в результирующий массив в виде строки. Но также вставляется оператор JavaScript, и переменная вставляется в виде строки, причина та же, что и в первом способе, оператор должен быть вынесен отдельно, и переменная будет вставлена в массив сама по себе. Изменить код
let tpl = ''
let match = '' // 记录exec函数匹配到的值
// 匹配模板id
const idReg = /[\s\W]/g
// 匹配JavaScript语句或变量
const tplReg = /<%=?\s*([^%>]+?)\s*%>/g
const keyReg = /(for|if|else|switch|case|break|{|})/g // **** 增加正则匹配语句
const add = (str, result, js) => {
str = str.replace(/[\r\n\t]/g, '')
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
// **** 增加三元表达式的判断,三种情况:JavaScript语句、JavaScript变量、HTML结构。
result += js ? str.match(keyReg) ? `${str}` : `result.push(${str});` : `result.push('${str}');`
return result
}
const tmpl = (str, data) => {
// 记录HTML结构匹配的开始位置
let cursor = 0
let result = 'let result = [];'
// 如果是模板字符串,会包含非单词部分(<, >, %, 等);如果是id,则需要通过getElementById获取
if (!idReg.test(str)) {
tpl = document.getElementById(str).innerHTML
} else {
tpl = str
}
// 使用exec函数,每次匹配成功会动态改变index的值
while (match = tplReg.exec(tpl)) {
result = add(tpl.slice(cursor, match.index), result) // 匹配HTML结构
result = add(match[1], result, true) // **** 匹配JavaScript语句、变量
cursor = match.index + match[0].length // 改变HTML结果匹配的开始位置
}
result = add(tpl.slice(cursor), result) // 匹配剩余的HTML结构
result += 'return result.join("")'
}
console.log(tmpl('template'))
Код после выполнения форматируется следующим образом
" let result = [];
result.push('<ul>');
if (obj.show) {
result.push('');
for (var i = 0; i < obj.users.length; i++) {
result.push('<li><a href="');
result.push(obj.users[i].url);
result.push('">');
result.push(obj.users[i].name);
result.push('</a></li>');
}
result.push('');
} else {
result.push('<p>什么鬼什么鬼</p>');
}
result.push('</ul>');
return result.join("") "
Пока что наши требования удовлетворены.
Представлена реализация двух функций шаблонизатора, вот краткое содержание
- Оба метода используют массивы и объединяют их после завершения объединения.
- Первый метод заключается исключительно в использовании функции замены, и замена выполняется после успешного сопоставления.
- Второй метод использует функцию exec для захвата структур HTML, операторов JavaScript и переменных с динамически изменяющимся значением индекса.
Конечно, оба метода могут использовать конкатенацию строк, но я сравнивал в браузере Chrome, и массивы все равно намного быстрее, так что это тоже оптимизационное решение: использование конкатенации массивов на 50% быстрее, чем конкатенация строк. Ниже приведена проверка конкатенации строк и массивов.
console.log('开始计算字符串拼接')
const start2 = Date.now()
let str = ''
for (var i = 0; i < 9999999; i++) {
str += '1'
}
const end2 = Date.now()
console.log(`字符串拼接运行时间: ${end2 - start2}`ms)
console.log('----------------')
console.log('开始计算数组拼接')
const start1 = Date.now()
const arr = []
for (var i = 0; i < 9999999; i++) {
arr.push('1')
}
arr.join('')
const end1 = Date.now()
console.log(`数组拼接运行时间: ${end1 - start1}`ms)
Результат выглядит следующим образом:
开始计算字符串拼接
字符串拼接运行时间: 2548ms
----------------
开始计算数组拼接
数组拼接运行时间: 1359ms
3. Функция + применить (вызов) динамически генерировать HTML-код
В приведенных выше двух методах результатом является строка, как превратить ее в исполняемый код JavaScript? Конструктор Function используется здесь для создания функции (конечно, можно использовать и функцию eval, но это не рекомендуется)
В большинстве случаев для создания функции напрямую используется объявление функции или выражение функции.
function test () {}
const test = function test () {}
Сгенерированная таким образом функция становится экземпляром объекта конструктора Function.
test instanceof Function // true
Конечно, вы также можете напрямую использовать конструктор Function для непосредственного создания функции, которая будет иметь несколько худшую производительность (двойной синтаксический анализ, JavaScript анализирует код JavaScript, код содержится в строке, то есть его необходимо перезапустить, пока код JavaScript запускает синтаксический анализатор для синтаксического анализа нового кода. При создании экземпляра нового синтаксического анализатора возникают существенные накладные расходы, поэтому такой код намного медленнее, чем прямой синтаксический анализ.)
const test = new Function('arg1', 'arg2', ... , 'console.log(arg1 + arg2)')
test(1 + 2) // 3
Невозможно иметь одновременно рыбу и медвежью лапу, рендеринг удобен, но приносит некоторые потери в производительности.
Конструктор Function может передавать несколько параметров, и последний параметр представляет оператор, который необходимо выполнить. Так что мы можем сделать
const fn = new Funcion(result)
Если вам нужно передать параметры, вы можете использовать вызов или применить, чтобы изменить область, в которой выполняется функция.
fn.apply(data)
4. Кэш шаблонов
Причина использования шаблонов заключается не только во избежание ненужных ошибок, вызванных ручным объединением строк, но и в повторном использовании кода шаблона в некоторых сценариях. Чтобы избежать многократного объединения строк для одного и того же шаблона, шаблон можно кэшировать. Наш кеш здесь может кэшироваться при передаче идентификатора. Реализованная логика не сложна, как вы можете видеть в следующем коде.
Ну и соединив весь вышеназванный контент, даем окончательный код шаблонизатора реализованного двумя способами
первый метод:
let tpl = ''
// 匹配模板的id
let idReg = /[\s\W]/g
const cache = {}
const add = tpl => {
// 匹配成功的值做替换操作
return tpl.replace(/[\r\n\t]/g, '')
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/<%=\s*([^%>]+?)\s*%>/g, "'); p.push($1); p.push('")
.replace(/<%/g, "');")
.replace(/%>/g, "p.push('")
}
const tmpl = (str, data) => {
let result = `let p = []; p.push('`
// 如果是模板字符串,会包含非单词部分(<, >, %, 等);如果是id,则需要通过getElementById获取
if (!idReg.test(str)) {
tpl = document.getElementById('template').innerHTML
if (cache[str]) {
return cache[str].apply(data)
}
} else {
tpl = str
}
result += add(tpl)
result += "'); return p.join('');"
let fn = new Function(result) // 转成可执行的JS代码
if (!cache[str] && !idReg.test(str)) { // 只用传入的是id的情况下才缓存模板
cache[str] = fn
}
return fn.apply(data) // apply改变函数执行的作用域
}
Второй метод:
let tpl = ''
let match = ''
const cache = {}
// 匹配模板id
const idReg = /[\s\W]/g
// 匹配JavaScript语句或变量
const tplReg = /<%=?\s*([^%>]+?)\s*%>/g
// 匹配各种关键字
const keyReg = /(for|if|else|switch|case|break|{|})/g
const add = (str, result, js) => {
str = str.replace(/[\r\n\t]/g, '')
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
result += js ? str.match(keyReg) ? `${str}` : `result.push(${str});` : `result.push('${str}');`
return result
}
const tmpl = (str, data) => {
let cursor = 0
let result = 'let result = [];'
// 如果是模板字符串,会包含非单词部分(<, >, %, 等);如果是id,则需要通过getElementById获取
if (!idReg.test(str)) {
tpl = document.getElementById(str).innerHTML
// 缓存处理
if (cache[str]) {
return cache[str].apply(data)
}
} else {
tpl = str
}
// 使用exec函数,动态改变index的值
while (match = tplReg.exec(tpl)) {
result = add(tpl.slice(cursor, match.index), result) // 匹配HTML结构
result = add(match[1], result, true) // 匹配JavaScript语句、变量
cursor = match.index + match[0].length // 改变HTML结果匹配的开始位置
}
result = add(tpl.slice(cursor), result) // 匹配剩余的HTML结构
result += 'return result.join("")'
let fn = new Function(result) // 转成可执行的JS代码
if (!cache[str] && !idReg.test(str)) { // 只有传入的是id的情况下才缓存模板
cache[str] = fn
}
return fn.apply(data) // apply改变函数执行的作用域
}
наконец
Хах, в принципе закончил, и напоследок хочу немного подытожить
если! Если интервьюер попросит вас во время интервью кратко описать принцип работы механизма шаблонов JavaScript, тогда следующее резюме может вам помочь.
О.. Принцип реализации механизма шаблонов примерно состоит в том, чтобы отделить структуру HTML в шаблоне от оператора и переменных JavaScript, поместить структуру HTML в массив в виде строки, независимо извлечь оператор JavaScript и отправить код JavaScript. В массив, путем замены функции замены или обхода функции exec, конструируется HTML-код с данными, и, наконец, исполняемый код JavaScript генерируется с помощью конструктора функций + функции apply(call) .
Если ответ выходит, интервьюер вдруг находит Максиму в своем сердце: А, кажется, это очень легко? Тогда попробуйте:
- Зачем использовать массив? Могу ли я использовать строки? Какая разница между двумя?
- Простой взгляд на использование функций replace и exec?
- В чем разница между функциями exec и match?
- /<%>]+?)\s*%>/g Что означает это регулярное выражение?%>
- Кратко объясните разницу между функциями применения, вызова и связывания?
- Каковы недостатки использования конструктора функций?
- Разница между объявлением функции и выражением функции?
- ....
Этот абзац резюме тоже может вытянуть много очков знаний... Перевернись, Максима!
Хорошо, пока что здесь была представлена реализация простого механизма шаблонов JavaScript.Если читатели терпеливо и внимательно прочитают эту статью, я верю, что ваши успехи будут полными. Если вы все еще чувствуете себя смущенным после прочтения, если вы не возражаете, вы можете попробовать его еще несколько раз.
Справочная статья:
- Рекомендованная книга: "Продвинутое программирование на JavaScript, 3-е издание"
- Самый простой механизм шаблонов JavaScript
- Всего 20 строк кода Javascript! Научите вас, как написать механизм шаблонов страниц